Helm Chart Validator & Analysis Toolkit
Comprehensive toolkit for validating, linting, testing, and analyzing Helm charts and their rendered Kubernetes resources. Use this skill when working with Helm charts, validating templates, debugging chart issues, working with Custom Resource Definitions (CRDs) that require documentation lookup, or checking Helm best practices.
npx skills add github:pantheon-org/tekhne --skill ci-cd--helm--validatorOverview
This skill provides a comprehensive validation and analysis workflow for Helm charts, combining Helm-native linting, template rendering, YAML validation, schema validation, CRD documentation lookup, and security best practices checking.
IMPORTANT: This is a READ-ONLY validator. It analyzes charts and proposes improvements but does NOT modify any files. All proposed changes are listed in the final summary for the user to review and apply manually or via the helm-generator skill.
Philosophy
- Validate in stages, because linting, rendering, schema checks and security checks each catch different faults.
- Stay read-only: analyse the chart and propose changes, and leave applying them to the user.
- Validate rendered output, not just chart sources, since templates can produce invalid manifests from valid-looking YAML.
- Prefer documented CRD schemas over guesses when a resource has no published schema.
- Continue through failing stages and report everything together in the final report.
When to Use
- Validating a Helm chart before release or deployment
- Debugging template rendering errors or lint failures
- Checking rendered manifests against Kubernetes schemas with kubeconform
- Looking up documentation for Custom Resource Definitions found in a chart
- Reviewing a chart’s securityContext, resource limits and image tags
When Not to Use
- Creating or scaffolding a new chart (use helm-generator)
- Modifying chart files; this skill is read-only
- Validating plain Kubernetes manifests with no Helm chart (use k8s-yaml-validator)
- Troubleshooting a running release in a cluster
Validation & Testing Workflow
Follow this sequential workflow. Each stage catches different types of issues.
Stage 1: Tool Check
bash scripts/setup_tools.sh
Required: helm (v3+), yamllint, kubeconform, kubectl (optional). If tools are missing, provide installation instructions and ask the user before proceeding.
Stage 2: Chart Structure Validation
bash scripts/validate_chart_structure.sh <chart-directory>
Validates required files (Chart.yaml, values.yaml, templates/) and recommended files (_helpers.tpl, NOTES.txt, .helmignore).
Stage 3: Helm Lint
helm lint <chart-directory> --strict
# Optional: --values <file>, --set key=value, --debug
Stage 4: Template Rendering
helm template <release-name> <chart-directory> \
--values <values-file> \
--debug \
--output-dir ./rendered
Useful flags: --validate, --include-crds, --is-upgrade, --kube-version 1.28.0, --show-only templates/<file>.yaml.
Stage 5: YAML Syntax Validation
yamllint -c assets/.yamllint ./rendered/*.yaml
Fix template-generated YAML issues in the source template, not the rendered output.
Stage 6: CRD Detection and Documentation Lookup
bash scripts/detect_crd_wrapper.sh <chart-directory>/crds/*.yaml
bash scripts/detect_crd_wrapper.sh ./rendered/*.yaml
Output:
[{"kind": "Certificate", "apiVersion": "cert-manager.io/v1", "group": "cert-manager.io", "version": "v1", "isCRD": true}]
For each detected CRD:
-
Try context7 MCP first (preferred):
mcp__context7__resolve-library-idwith the CRD project name (e.g."cert-manager")mcp__context7__get-library-docswith the resolved ID, topic (e.g."Certificate spec"), tokens: 5000
-
Fallback to WebSearch:
"<Kind>" "<group>" kubernetes CRD "<version>" documentation spec
Extract required fields, types, validation rules, and version-specific deprecations.
Stage 7: Schema Validation
kubeconform \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
-summary -verbose \
./rendered/*.yaml
Add -strict for production, -ignore-missing-schemas for internal CRDs, -kubernetes-version 1.28.0 for version pinning. “No schema found” for CRDs is expected, validate those manually using Stage 6 docs.
Stage 8: Cluster Dry-Run (if available)
helm install <release-name> <chart-directory> --dry-run --debug --values <values-file>
helm upgrade <release-name> <chart-directory> --dry-run --debug --values <values-file>
# With helm-diff plugin:
helm diff upgrade <release-name> <chart-directory>
Catches admission controller rejections, policy violations, quota issues, and webhook errors. Skip and document if no cluster access.
Stage 9: Security Best Practices Check (MANDATORY)
Analyze rendered Deployment/Pod templates:
grep -l "securityContext" ./rendered/*.yaml
grep -l "resources:" ./rendered/*.yaml
grep "image:.*:latest" ./rendered/*.yaml
Required checks:
- Pod securityContext:
runAsNonRoot,runAsUser,fsGroup - Container securityContext:
allowPrivilegeEscalation: false,readOnlyRootFilesystem,capabilities.drop: [ALL] - Resource limits/requests for cpu and memory
- No
:latestimage tags - Liveness and readiness probes present
Stage 10: Final Report (MANDATORY)
This stage is MANDATORY even if all validations pass.
Step 1: Load References (when issues found)
Read references/helm_best_practices.md
Read references/k8s_best_practices.md
Step 2: Validation Summary Table
| Stage | Status | Issues |
|-------|--------|--------|
| 1. Tool Check | ✅ Passed | All tools available |
| 2. Structure | ⚠️ Warning | Missing: .helmignore |
| 3. Helm Lint | ✅ Passed | 0 errors |
| 4. Template Render | ✅ Passed | 5 templates rendered |
| 5. YAML Syntax | ✅ Passed | No errors |
| 6. CRD Detection | ✅ Passed | 1 CRD documented |
| 7. Schema Validation | ✅ Passed | All resources valid |
| 8. Dry-Run | ✅ Passed | No cluster errors |
| 9. Security Check | ⚠️ Warning | Missing securityContext |
Step 3: Categorize Issues
- ❌ Errors (must fix): Template syntax errors, missing required fields, schema failures, dry-run failures
- ⚠️ Warnings (should fix): Deprecated APIs, missing securityContext, missing resource limits,
:latesttag, missing recommended files - ℹ️ Info: Missing values.schema.json, missing README.md, optimization opportunities
Step 4: Proposed Changes (DO NOT APPLY)
For each issue:
- File path and line number
- Before/after code blocks
- Explanation of why the change is recommended
Step 5: Automation Opportunities
Common fixes:
- Missing
_helpers.tpl:bash scripts/generate_helpers.sh <chart> - Missing
.helmignore: Copy fromassets/.helmignore - Missing
values.schema.json: Copy/customize fromassets/values.schema.json - Use
includeinstead oftemplatefor pipeline support - Add
nindentfor proper YAML indentation - Add
defaultfunction for optional values - Add
requiredfor critical values
Step 6: Final Summary
## Validation Summary
**Chart:** <chart-name>
**Status:** ⚠️ Warnings Found (or ✅ Ready for Deployment)
**Issues:** Errors: X Warnings: Y Info: Z
**Proposed Changes:** N changes recommended
**Next Steps:**
1. Review proposed changes above
2. Apply manually or use helm-generator skill
3. Re-run validation to confirm fixes
Helm Templating Reference
For complex templating tasks, load the dedicated reference:
Read references/template_functions.md
Standard helper patterns (templates/_helpers.tpl), including fullname, labels, and selectorLabels definitions, are documented in references/template_functions.md.
Key template functions: required, default, quote, include, tpl, toYaml, merge, lookup, see references/template_functions.md for full reference with examples.
macOS Extended Attributes Issue
Symptom: Helm reports “Chart.yaml file is missing” even though the file exists.
Diagnosis & Fix:
xattr /path/to/chart/Chart.yaml # check for attributes
xattr -cr /path/to/chart/ # remove all recursively
Prevention: Use helm create as a base, or create files with shell heredocs (cat > file << 'EOF').
Error Handling
- Tool not available: Run
scripts/setup_tools.sh, skip optional stages, document what was skipped. - Template errors: Show file and line number, check values.yaml definitions, use
--debug. - Cluster access issues: Fall back to
kubectl apply --dry-run=server -f ./rendered/, document limitations. - CRD schema not found: Use kubeconform CRD catalog; fall back to
kubectl explain <kind>. - Stage failures: Continue to next stage, collect all errors, present together in Stage 10.
Version Awareness
- Ensure
apiVersion: v2in Chart.yaml (Helm 3+) - Check for deprecated Kubernetes APIs
- Use
kubectl api-versionsto list available API versions - Set
kubeVersionconstraint in Chart.yaml when needed - Target specific K8s version with
--kube-versionin helm template / kubeconform
Anti-Patterns
NEVER treat helm lint passing as sufficient validation
WHY: helm lint only checks chart structure and basic YAML syntax; it does not validate rendered Kubernetes manifests against the API schema.
BAD: Ship a chart after helm lint passes with no manifest validation.
GOOD: Run helm template | kubeval or helm template | kubeconform to validate rendered output.
NEVER skip --set overrides when linting complex charts
WHY: Linting with only default values misses validation errors that only surface when required values are provided.
BAD: helm lint ./chart with no value overrides.
GOOD: helm lint ./chart --set image.tag=v1.0 --set ingress.enabled=true to exercise non-default code paths.
NEVER ignore helm diff output before helm upgrade
WHY: Upgrading without reviewing the diff can silently delete resources (e.g., removing a service selector key).
BAD: Run helm upgrade directly in CI without diffing.
GOOD: Run helm diff upgrade myapp ./chart and fail the pipeline if destructive changes are detected.
NEVER validate charts without checking sub-chart dependencies
WHY: helm dependency update failure silently falls back to cached or missing sub-charts, producing incorrect renders.
BAD: Run helm template ./chart without first running helm dependency update.
GOOD: Always run helm dependency update && helm template in sequence.
NEVER modify chart files from this skill
WHY: The validator is read-only by design. Unreviewed edits can change a release in ways the user did not approve.
BAD: Edit values.yaml to add a missing securityContext as soon as the check fails.
GOOD: List the change as a proposed before/after block in the final summary for the user to apply, manually or with the helm-generator skill.
NEVER fix rendered output instead of the template
WHY: Rendered files are regenerated on every run, so a fix applied there is lost and the template defect remains.
BAD: Hand-edit ./rendered/deployment.yaml to clear a yamllint error.
GOOD: Fix the issue in the source template under templates/ and re-render.
NEVER skip the final report when all stages pass
WHY: Stage 10 is mandatory; the summary table is the record of what ran and what was skipped, and a silent pass hides skipped stages.
BAD: Say “chart looks fine” after lint succeeds.
GOOD: Produce the validation summary table and final summary even when every stage passes.
NEVER accept the :latest image tag
WHY: :latest makes deployments non-reproducible, so the same chart can run different images over time.
BAD: image: nginx:latest in a rendered Deployment.
GOOD: Pin an explicit version tag, for example image: nginx:1.27, and report any :latest tag as a warning.
NEVER treat a missing CRD schema as a validation pass
WHY: kubeconform reports “No schema found” for CRDs it cannot check, which means the resource was not validated, not that it is valid.
BAD: Count a Certificate resource as validated because kubeconform raised no error.
GOOD: Look up the CRD documentation (Stage 6) and check required fields manually, using -ignore-missing-schemas only for the schema step.
NEVER abort the workflow at the first failing stage
WHY: Each stage catches a different class of issue; stopping early hides later problems and forces repeated runs.
BAD: Stop after helm lint fails and report one error.
GOOD: Continue to the next stage, collect all errors, and present them together in Stage 10.
References
scripts/
setup_tools.sh: Check/install required toolsvalidate_chart_structure.sh: Validate chart directory structuredetect_crd_wrapper.sh: Detect CRDs in YAML files (manages Python venv)detect_crd.py: Parse YAML to identify CRDs, output JSONgenerate_helpers.sh: Generate standard_helpers.tpl
references/
helm_best_practices.md: Chart structure, template conventions, values organizationk8s_best_practices.md: Metadata, labels, resource limits, security contexttemplate_functions.md: All built-in Helm/Sprig functions with examples, standard helper patterns
assets/
.helmignore: Standard ignore patterns for chart packaging.yamllint: Pre-configured yamllint rules for Kubernetes YAMLvalues.schema.json: Example JSON Schema template for values validation
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Helm Chart Best Practices
This reference provides comprehensive best practices for creating, maintaining, and validating Helm charts.
Chart Structure
Required Files
Every Helm chart must have:
mychart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
templates/ # Template directory
Chart.yaml Structure
Use apiVersion v2 for Helm 3+ charts:
apiVersion: v2
name: mychart
description: A Helm chart for Kubernetes
type: application # or 'library' for helper charts
version: 0.1.0 # Chart version (SemVer)
appVersion: "1.16.0" # Version of the app
# Optional but recommended
keywords:
- web
- application
home: https://github.com/example/mychart
sources:
- https://github.com/example/mychart
maintainers:
- name: Your Name
email: your.email@example.com
# Dependencies
dependencies:
- name: postgresql
version: "~11.6.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
# Kubernetes version constraint
kubeVersion: ">=1.21.0-0"
Template Best Practices
1. Use Named Templates (Helpers)
Define reusable templates in templates/_helpers.tpl:
Good:
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
Usage:
metadata:
name: {{ include "mychart.fullname" . }}
2. Use include Instead of template
Good:
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}
Bad:
metadata:
labels:
{{- template "mychart.labels" . }}
Why: include allows piping the output to other functions like nindent, indent, quote, etc.
3. Always Quote String Values
Good:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host | quote }}
Bad:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host }}
Why: Prevents YAML parsing issues with special characters and ensures strings are treated as strings.
4. Use required for Critical Values
Good:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
data:
password: {{ required "A valid .Values.password is required!" .Values.password | b64enc }}
Why: Fails early with a helpful error message if required values are missing.
5. Provide Defaults with default Function
Good:
replicas: {{ .Values.replicaCount | default 1 }}
Why: Makes charts more resilient and easier to use with minimal configuration.
6. Use nindent for Proper Indentation
Good:
spec:
template:
metadata:
labels:
{{- include "mychart.labels" . | nindent 8 }}
Bad:
spec:
template:
metadata:
labels:
{{ include "mychart.labels" . | indent 8 }}
Why: nindent adds a newline before indenting, which is usually what you want in YAML.
7. Use toYaml for Complex Structures
Good:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
Why: Allows users to specify complex YAML structures in values without template complexity.
8. Conditional Resource Creation
Good:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
# ... ingress definition
{{- end }}
Why: Allows users to optionally enable/disable resources.
9. Use with for Scoping
Good:
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 2 }}
{{- end }}
Why: Changes the scope of . to the specified value, making templates cleaner.
10. Template Comments
Good:
{{- /*
This helper creates the fullname for resources.
It supports nameOverride and fullnameOverride values.
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}
Bad:
# This creates the fullname
{{- define "mychart.fullname" -}}
{{- end }}
Why: Template comments ({{- /* */ -}}) are removed during rendering, while YAML comments (#) remain in output.
Values File Best Practices
1. Use Flat Structures When Possible
Good (Simple):
replicaCount: 1
imagePullPolicy: IfNotPresent
Good (Related Settings):
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.0"
Bad (Overly Nested):
app:
deployment:
pod:
container:
image:
repository: nginx
2. Document All Values
Good:
# replicaCount is the number of pod replicas for the deployment
replicaCount: 1
# image configures the container image
image:
# image.repository is the container image registry and name
repository: nginx
# image.pullPolicy is the image pull policy
pullPolicy: IfNotPresent
# image.tag overrides the image tag (default is chart appVersion)
tag: "1.21.0"
Why: Makes charts self-documenting and easier to use.
3. Provide Sensible Defaults
Good:
replicaCount: 1
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
Why: Charts should work out of the box with minimal configuration.
4. Use Boolean Flags for Feature Toggles
Good:
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
Why: Makes it clear when features are optional and how to enable them.
5. Group Related Configuration
Good:
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
6. Provide Empty Structures for Optional Config
Good:
nodeSelector: {}
tolerations: []
affinity: {}
Why: Shows users what optional configurations are available.
Kubernetes Resource Best Practices
1. Always Set Resource Limits and Requests
Good:
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
Why: Ensures proper scheduling and prevents resource exhaustion.
2. Use Proper Label Conventions
Good:
metadata:
labels:
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "mychart.chart" . }}
Why: Follows Kubernetes recommended labels for better tooling integration.
3. Use SecurityContext
Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
Why: Improves security posture.
4. Define Probes
Good:
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
Why: Ensures Kubernetes can properly manage application health.
5. Use ConfigMaps and Secrets Appropriately
Good:
# ConfigMap for non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
app.conf: |
{{- .Values.config | nindent 4 }}
# Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
type: Opaque
data:
password: {{ .Values.password | b64enc }}
Template Function Reference
String Functions
quote- Quote a stringsquote- Single quote a stringtrim- Remove whitespacetrimSuffix- Remove suffixtrimPrefix- Remove prefixupper- Convert to uppercaselower- Convert to lowercasetitle- Title casetrunc- Truncate stringrepeat- Repeat stringsubstr- Substringnospace- Remove all whitespace
Type Conversion
toYaml- Convert to YAMLfromYaml- Parse YAMLtoJson- Convert to JSONfromJson- Parse JSONtoString- Convert to stringtoStrings- Convert list to strings
Flow Control
default- Provide default valuerequired- Require a valuefail- Fail with error messagecoalesce- Return first non-empty value
Collections
list- Create a listdict- Create a dictionarymerge- Merge dictionariespick- Pick keys from dictionaryomit- Omit keys from dictionarykeys- Get dictionary keysvalues- Get dictionary values
Encoding
b64enc- Base64 encodeb64dec- Base64 decodesha256sum- SHA256 hash
Testing Best Practices
1. Create Test Resources
Good:
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "mychart.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
Run tests:
helm test <release-name>
2. Test with Multiple Values
# Test with production values
helm template my-release ./mychart -f values-prod.yaml
# Test with development values
helm template my-release ./mychart -f values-dev.yaml
# Test with overrides
helm template my-release ./mychart --set replicaCount=3
3. Validate Before Installing
# Lint the chart
helm lint ./mychart
# Dry-run install
helm install my-release ./mychart --dry-run --debug
# Validate against cluster
helm install my-release ./mychart --dry-run
Security Best Practices
1. Don’t Hardcode Secrets
Bad:
data:
password: cGFzc3dvcmQ= # Don't do this!
Good:
data:
password: {{ .Values.password | b64enc }}
2. Use RBAC
Good:
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "mychart.serviceAccountName" . }}
{{- end }}
3. Run as Non-Root
Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000
4. Drop Capabilities
Good:
securityContext:
capabilities:
drop:
- ALL
Performance Best Practices
1. Use .helmignore
Exclude unnecessary files from the chart package:
# .helmignore
.git/
.gitignore
*.md
.DS_Store
*.swp
test/
2. Minimize Template Complexity
Good:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
Bad:
resources:
{{- if .Values.resources.limits }}
limits:
{{- if .Values.resources.limits.cpu }}
cpu: {{ .Values.resources.limits.cpu }}
{{- end }}
{{- if .Values.resources.limits.memory }}
memory: {{ .Values.resources.limits.memory }}
{{- end }}
{{- end }}
# ... more nested conditionals
3. Use Helpers for Repeated Logic
Don’t repeat the same template logic in multiple places - extract it to a helper.
Version Control Best Practices
1. Use SemVer for Chart Versions
MAJOR.MINOR.PATCH- Increment MAJOR for breaking changes
- Increment MINOR for new features
- Increment PATCH for bug fixes
2. Maintain a CHANGELOG
Document changes between versions.
3. Tag Releases
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
Common Pitfalls to Avoid
1. Not Using - for Whitespace Control
Bad:
{{ if .Values.enabled }}
key: value
{{ end }}
Good:
{{- if .Values.enabled }}
key: value
{{- end }}
2. Not Truncating Resource Names
Kubernetes resource names must be <= 63 characters:
Bad:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
Good:
name: {{ include "mychart.fullname" . | trunc 63 | trimSuffix "-" }}
3. Using template Instead of include
Use include when you need to pipe the output to other functions.
4. Not Validating User Input
Bad:
replicas: {{ .Values.replicaCount }}
Good:
replicas: {{ required "replicaCount is required" .Values.replicaCount }}
5. Hardcoding Values in Templates
Bad:
image: nginx:1.21.0
Good:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
Upgrade and Migration Best Practices
1. Use Helm Hooks
metadata:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": hook-succeeded
Available hooks:
pre-installpost-installpre-upgradepost-upgradepre-deletepost-deletepre-rollbackpost-rollbacktest
2. Test Upgrades
# Show what would change
helm diff upgrade my-release ./mychart
# Dry-run upgrade
helm upgrade my-release ./mychart --dry-run --debug
3. Support Rolling Back
Ensure your charts support rollback by not using hooks that delete critical resources.
Documentation Best Practices
1. Create a Comprehensive README
Include:
- Chart description
- Prerequisites
- Installation instructions
- Configuration options (values)
- Examples
- Upgrade notes
2. Document Template Functions
Add comments to your _helpers.tpl:
{{- /*
mychart.fullname generates a fully qualified application name.
It supports fullnameOverride and nameOverride values.
Maximum length is 63 characters per DNS naming spec.
Usage: {{ include "mychart.fullname" . }}
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}
3. Provide NOTES.txt
Create templates/NOTES.txt with post-installation instructions:
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get all {{ .Release.Name }}
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
{{- end }}
{{- end }}
Packaging and Distribution
1. Package the Chart
helm package ./mychart
2. Create Chart Repository
# Create index
helm repo index .
# Serve repository
helm serve
3. Publish to Artifact Hub
Create artifacthub-repo.yml in your repository:
repositoryID: <uuid>
owners:
- name: Your Name
email: your.email@example.com
Summary Checklist
Before releasing a chart, verify:
- Chart.yaml has all required fields
- values.yaml has sensible defaults
- All values are documented
- Templates use helpers for repeated logic
- Resource names are properly truncated
- Labels follow Kubernetes conventions
- Resources have limits and requests
- SecurityContext is defined
- Probes are configured
- Secrets are parameterized, not hardcoded
-
helm lintpasses -
helm templaterenders successfully - Dry-run install succeeds
- Tests are defined and pass
- README.md is comprehensive
- NOTES.txt provides helpful post-install info
- Chart version follows SemVer
- .helmignore excludes unnecessary files
Kubernetes YAML Best Practices
This reference provides comprehensive best practices for creating, maintaining, and validating Kubernetes resources in Helm charts.
General YAML Best Practices
Formatting and Style
- Use 2 spaces for indentation (not tabs)
- Keep lines under 120 characters when possible
- Use lowercase for keys
- Quote string values containing special characters
- Always specify apiVersion and kind
- Include metadata.name for all resources
- Use consistent naming conventions (lowercase, hyphens for separators)
Resource Organization
- One resource per file for clarity (unless logically grouped)
- Use
---to separate multiple resources in a single file - Name files descriptively:
<resource-type>-<name>.yaml - Group related resources together (e.g., deployment + service + configmap)
Metadata Best Practices
Standard Metadata Structure
metadata:
name: my-app
namespace: production
labels:
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: my-app-prod
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/component: backend
app.kubernetes.io/part-of: my-system
app.kubernetes.io/managed-by: helm
helm.sh/chart: my-app-1.0.0
annotations:
description: "Backend service for my-app"
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
Kubernetes Recommended Labels
Always include these standard labels for better tooling integration:
| Label | Description | Example |
|---|---|---|
app.kubernetes.io/name |
Application name | nginx |
app.kubernetes.io/instance |
Unique instance identifier | nginx-prod |
app.kubernetes.io/version |
Application version | 1.21.0 |
app.kubernetes.io/component |
Component within architecture | frontend |
app.kubernetes.io/part-of |
Higher-level application | wordpress |
app.kubernetes.io/managed-by |
Tool managing the resource | helm |
Helm-Specific Labels
labels:
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
Labels and Selectors
Selector Best Practices
- Selectors must match pod labels exactly
- Use immutable selectors (they cannot be changed after creation)
- Keep selector labels minimal but unique
Good:
spec:
selector:
matchLabels:
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: my-app-prod
template:
metadata:
labels:
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: my-app-prod
app.kubernetes.io/version: "1.0.0" # Additional labels OK
Bad:
spec:
selector:
matchLabels:
app: my-app
version: "1.0.0" # Version in selector prevents rolling updates!
Label Key Conventions
- Use DNS subdomain format for prefixed keys:
prefix/key - Keys without prefix are private to the user
- Standard prefixes:
app.kubernetes.io/,helm.sh/,kubernetes.io/
Resource Limits and Requests
Why They Matter
- Requests: Minimum resources guaranteed to the container
- Limits: Maximum resources the container can use
- Required for proper scheduling, QoS class assignment, and cluster stability
Recommended Configuration
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "500m"
Guidelines by Resource Type
| Resource Type | Requests | Limits | Notes |
|---|---|---|---|
| CPU | Set always | Optional | Consider burstable workloads |
| Memory | Set always | Set always | Prevents OOM kills |
| Ephemeral Storage | Optional | Recommended | For disk-intensive apps |
QoS Classes
Kubernetes assigns Quality of Service classes based on resource settings:
- Guaranteed: requests == limits for all containers
- Burstable: At least one container has requests < limits
- BestEffort: No requests or limits set (not recommended)
# Guaranteed QoS
resources:
requests:
memory: "128Mi"
cpu: "500m"
limits:
memory: "128Mi"
cpu: "500m"
Memory Guidelines
- Set memory limits to prevent OOM kills affecting other pods
- Memory is incompressible - exceeding limits causes termination
- Monitor actual usage before setting production values
CPU Guidelines
- CPU is compressible - exceeding limits causes throttling, not termination
- Consider not setting CPU limits for burstable workloads
- Use millicores:
100m= 0.1 CPU core
Probes Configuration
Liveness Probe
Determines if the container should be restarted:
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Health-Check
value: liveness
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
Readiness Probe
Determines if the container can receive traffic:
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
Startup Probe
For slow-starting containers (Kubernetes 1.18+):
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30 # 30 * 10 = 300s max startup time
successThreshold: 1
Probe Types
| Type | Use Case | Example |
|---|---|---|
httpGet |
HTTP endpoints | REST APIs, web apps |
tcpSocket |
TCP connections | Databases, message queues |
exec |
Custom scripts | Complex health checks |
grpc |
gRPC services | gRPC health protocol |
Probe Best Practices
- Liveness: Check if app is running, not dependencies
- Readiness: Check if app can serve traffic (including dependencies)
- Startup: Use for slow-starting apps instead of long
initialDelaySeconds - Set appropriate timeouts to prevent false positives
- Don’t make probes too aggressive (high CPU overhead)
Security Context
Pod-Level Security Context
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
fsGroupChangePolicy: "OnRootMismatch"
seccompProfile:
type: RuntimeDefault
Container-Level Security Context
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if needed
seccompProfile:
type: RuntimeDefault
Security Context Fields Explained
| Field | Level | Description |
|---|---|---|
runAsNonRoot |
Pod/Container | Prevents running as root |
runAsUser |
Pod/Container | Specifies UID to run as |
runAsGroup |
Pod/Container | Specifies GID to run as |
fsGroup |
Pod | Group ownership for volumes |
readOnlyRootFilesystem |
Container | Makes root filesystem read-only |
allowPrivilegeEscalation |
Container | Prevents privilege escalation |
capabilities |
Container | Linux capabilities management |
seccompProfile |
Pod/Container | Syscall filtering |
Recommended Security Baseline
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
Image Management
Image Specification Best Practices
containers:
- name: app
image: registry.example.com/my-app:v1.2.3
imagePullPolicy: IfNotPresent
Image Pull Policy
| Policy | When to Use |
|---|---|
Always |
For :latest tags or mutable tags |
IfNotPresent |
For immutable tags (recommended) |
Never |
For pre-loaded images (rare) |
Image Best Practices
- Always use specific tags: Never use
:latestin production - Use digest for immutability:
image@sha256:abc123... - Use private registries: For security and reliability
- Scan images: Implement vulnerability scanning in CI/CD
# Recommended: Specific tag
image: nginx:1.21.6
# Better: Digest for immutability
image: nginx@sha256:2834dc507516af02784808c5f48b7cbe38b8ed5d0f4837f16e78d00deb7e7767
# Avoid: Mutable tags
image: nginx:latest # Don't do this in production
Pod Disruption Budgets
Why PDBs Matter
Pod Disruption Budgets ensure high availability during voluntary disruptions like:
- Node drains
- Cluster upgrades
- Deployment rollouts
PDB Configuration
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
# Option 1: Minimum available pods
minAvailable: 2
# Option 2: Maximum unavailable pods (use one, not both)
# maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: my-app
PDB Best Practices
- Set PDB for all production workloads with multiple replicas
- Use
minAvailablewhen you need minimum capacity guarantee - Use
maxUnavailablewhen you want to limit disruption rate - Don’t set
minAvailableequal to replicas (blocks all disruptions)
# Good: Allows 1 pod to be unavailable
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "mychart.fullname" . }}-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
Horizontal Pod Autoscaler
HPA Configuration
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
HPA Best Practices
- Always set resource requests (required for CPU/memory-based scaling)
- Set appropriate
minReplicasfor base capacity - Use stabilization windows to prevent flapping
- Consider custom metrics for business-specific scaling
- Don’t use HPA with
replicasfield in Deployment (conflicts)
Helm Template for HPA
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "mychart.fullname" . }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "mychart.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
Network Policies
Default Deny All
Start with a default deny policy, then allow specific traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: my-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow Specific Ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Allow Egress to External Services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-to-database
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8
ports:
- protocol: TCP
port: 5432
# Allow DNS resolution
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
Network Policy Best Practices
- Start with default deny, then allow specific traffic
- Always allow DNS egress (UDP port 53)
- Use namespace selectors for cross-namespace communication
- Label namespaces for policy targeting
- Test policies in staging before production
Service Configuration
Service Types
| Type | Use Case |
|---|---|
ClusterIP |
Internal cluster communication (default) |
NodePort |
External access via node ports (30000-32767) |
LoadBalancer |
Cloud provider load balancers |
ExternalName |
DNS CNAME for external services |
Service Best Practices
apiVersion: v1
kind: Service
metadata:
name: my-app
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mychart.selectorLabels" . | nindent 4 }}
Named Ports
Always use named ports for clarity:
# In Deployment
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
# In Service
ports:
- name: http
port: 80
targetPort: http # References named port
ConfigMaps and Secrets
ConfigMap Best Practices
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}-config
data:
# For simple key-value pairs
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
# For file content
app.properties: |
server.port=8080
server.host=0.0.0.0
Secret Best Practices
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}-secret
type: Opaque
data:
# Base64 encoded values
password: {{ .Values.password | b64enc | quote }}
api-key: {{ .Values.apiKey | b64enc | quote }}
stringData:
# Plain text (will be encoded)
config.yaml: |
database:
host: {{ .Values.database.host }}
Mounting as Environment Variables
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: my-config
key: LOG_LEVEL
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-secret
key: password
Mounting as Files
volumes:
- name: config
configMap:
name: my-config
- name: secrets
secret:
secretName: my-secret
defaultMode: 0400
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
- name: secrets
mountPath: /etc/secrets
readOnly: true
Common Validation Issues
Missing Required Fields
| Issue | Fix |
|---|---|
Missing apiVersion |
Add appropriate API version |
Missing kind |
Add resource kind |
Missing metadata.name |
Add resource name |
Missing spec.selector |
Add pod selector for Deployments/Services |
Empty containers |
Add at least one container |
Selector Mismatches
# Error: Selector doesn't match pod labels
spec:
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
application: my-app # Wrong label key!
Invalid Values
| Field | Valid Format | Example |
|---|---|---|
| CPU | Millicores or decimal | 500m, 0.5 |
| Memory | Binary units | 512Mi, 1Gi |
| Port | 1-65535 | 8080 |
| DNS names | Lowercase alphanumeric with hyphens | my-app-service |
Namespace Issues
- Not all resources are namespaced (e.g., ClusterRole, PersistentVolume)
- Services must be in the same namespace as pods they target
- Default namespace is “default” if not specified
Deprecation Warnings
Deprecated APIs
| Old API | New API | K8s Version |
|---|---|---|
extensions/v1beta1 Deployment |
apps/v1 |
1.16+ |
extensions/v1beta1 Ingress |
networking.k8s.io/v1 |
1.19+ |
networking.k8s.io/v1beta1 Ingress |
networking.k8s.io/v1 |
1.19+ |
policy/v1beta1 PodDisruptionBudget |
policy/v1 |
1.21+ |
policy/v1beta1 PodSecurityPolicy |
Removed (use PSA) | 1.25+ |
autoscaling/v2beta1 HPA |
autoscaling/v2 |
1.23+ |
batch/v1beta1 CronJob |
batch/v1 |
1.21+ |
Checking API Versions
# List available API versions
kubectl api-versions
# Check if resource supports specific version
kubectl api-resources | grep deployments
CRD-Specific Considerations
API Version Compatibility
- Check the CRD version installed in the cluster
- Use the correct apiVersion for the CRD
- Be aware of deprecations (e.g., v1alpha1 → v1beta1 → v1)
Required Fields
- CRDs often have custom required fields in spec
- Check the CRD documentation for field requirements
- Use
kubectl explain <kind>to see field documentation
Validation
- CRDs may have custom validation rules
- OpenAPI schema validation is stricter in newer K8s versions
- Use dry-run to catch validation errors before applying
# Explain CRD fields
kubectl explain certificate.spec
kubectl explain certificate.spec.issuerRef
Anti-Patterns to Avoid
1. Running as Root
# Bad
securityContext:
runAsUser: 0
# Good
securityContext:
runAsNonRoot: true
runAsUser: 1000
2. Using Latest Tag
# Bad
image: nginx:latest
# Good
image: nginx:1.21.6
3. No Resource Limits
# Bad - No limits
containers:
- name: app
image: my-app:1.0
# Good - With limits
containers:
- name: app
image: my-app:1.0
resources:
limits:
memory: "256Mi"
cpu: "500m"
4. Privileged Containers
# Bad
securityContext:
privileged: true
# Good
securityContext:
privileged: false
allowPrivilegeEscalation: false
5. No Probes
# Bad - No health checks
containers:
- name: app
# Good - With probes
containers:
- name: app
livenessProbe:
httpGet:
path: /health
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
Summary Checklist
Before deploying Kubernetes resources, verify:
Required
-
apiVersionandkindare correct -
metadata.namefollows naming conventions - Labels include recommended Kubernetes labels
- Selectors match pod labels exactly
- At least one container is defined
Recommended
- Resource requests and limits are set
- Liveness and readiness probes are configured
- Security context is defined (non-root, read-only fs)
- Image uses specific tag (not
latest) - Secrets are used for sensitive data (not ConfigMaps)
Production
- Pod Disruption Budget is configured
- Network Policies are in place
- HPA is configured for scalable workloads
- Service accounts are properly scoped
- Pod anti-affinity for high availability
Helm Template Functions Reference
This reference provides a comprehensive guide to Helm template functions, including built-in functions and Sprig library functions.
Essential Helm Functions
include
Includes a named template and allows piping the output to other functions.
Syntax:
{{ include "template.name" . }}
Examples:
# Include and indent
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}
# Include and quote
value: {{ include "mychart.value" . | quote }}
# Include with custom context
{{- include "mychart.container" (dict "root" . "container" .Values.mainContainer) }}
When to use: Prefer include over template when you need to manipulate the output with functions.
tpl
Evaluates a string as a template, allowing dynamic template rendering.
Syntax:
{{ tpl <string> <context> }}
Examples:
# Render a value as template
{{ tpl .Values.customConfig . }}
# Render external file as template
{{ tpl (.Files.Get "config/app.conf") . }}
# values.yaml
customConfig: |
server:
host: {{ .Values.server.host }}
port: {{ .Values.server.port }}
When to use: When users need to provide template strings in values or external files.
required
Enforces that a value must be provided, failing with a custom error message if missing.
Syntax:
{{ required "error message" .Values.path }}
Examples:
# Require a critical value
apiVersion: v1
kind: Service
metadata:
name: {{ required "A valid service name is required!" .Values.service.name }}
# Require database password
data:
password: {{ required "database.password must be set" .Values.database.password | b64enc }}
# Require multiple values
env:
- name: API_KEY
value: {{ required "apiKey must be provided" .Values.apiKey | quote }}
When to use: For critical values that have no sensible default.
lookup
Queries existing Kubernetes resources in the cluster during template rendering.
Syntax:
{{ lookup "apiVersion" "kind" "namespace" "name" }}
Examples:
# Look up existing secret
{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
# Secret exists, use existing password
password: {{ $secret.data.password }}
{{- else }}
# Create new password
password: {{ randAlphaNum 16 | b64enc }}
{{- end }}
# List all pods in namespace
{{- $pods := lookup "v1" "Pod" .Release.Namespace "" }}
# Get specific resource
{{- $cm := lookup "v1" "ConfigMap" "default" "my-config" }}
⚠️ Cautions:
- Only works during
helm installandhelm upgrade, not withhelm template - Requires cluster access
- Can slow down rendering
- Creates tight coupling between chart and cluster state
When to use: When you need to check for existing resources or migrate from existing deployments.
String Functions
quote / squote
Wraps a string in double or single quotes.
Examples:
# Double quotes
env:
- name: HOST
value: {{ .Values.host | quote }} # Output: "localhost"
# Single quotes
value: {{ .Values.name | squote }} # Output: 'myapp'
default
Provides a fallback value if the input is empty.
Examples:
# Simple default
replicas: {{ .Values.replicaCount | default 1 }}
# Chain with other functions
image: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
# Default for nested values
{{ .Values.server.port | default 8080 }}
trim / trimSuffix / trimPrefix
Removes whitespace or specific strings.
Examples:
# Remove whitespace
name: {{ .Values.name | trim }}
# Remove suffix
name: {{ .Release.Name | trimSuffix "-dev" }}
# Remove prefix
name: {{ .Values.fullName | trimPrefix "app-" }}
# Common pattern for resource names
name: {{ include "mychart.fullname" . | trunc 63 | trimSuffix "-" }}
upper / lower / title
Changes string case.
Examples:
# Uppercase
env: {{ .Values.environment | upper }} # Output: PRODUCTION
# Lowercase
name: {{ .Values.name | lower }} # Output: myapp
# Title case
label: {{ .Values.label | title }} # Output: My Application
trunc
Truncates a string to a specified length.
Examples:
# Truncate to 63 chars (K8s DNS limit)
name: {{ .Release.Name | trunc 63 | trimSuffix "-" }}
# Truncate to 20 chars
shortName: {{ .Values.name | trunc 20 }}
repeat
Repeats a string N times.
Examples:
# Repeat string
value: {{ "=" | repeat 10 }} # Output: ==========
# Create separator
comment: {{ "#" | repeat 20 }} # Output: ####################
replace
Replaces occurrences of a substring.
Examples:
# Replace underscores with hyphens
name: {{ .Values.name | replace "_" "-" }}
# Replace spaces
label: {{ .Values.label | replace " " "-" | lower }}
# Chart label (replace + with _)
chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
substr
Extracts a substring.
Examples:
# Get first 10 characters
short: {{ .Values.name | substr 0 10 }}
# Get characters 5-15
middle: {{ .Values.name | substr 5 15 }}
nospace
Removes all whitespace from a string.
Examples:
# Remove all spaces
compact: {{ .Values.value | nospace }} # "hello world" → "helloworld"
contains / hasPrefix / hasSuffix
Checks if a string contains, starts with, or ends with a substring.
Examples:
# Check if contains
{{- if contains "prod" .Values.environment }}
# Production configuration
{{- end }}
# Check prefix
{{- if hasPrefix "app-" .Values.name }}
name: {{ .Values.name }}
{{- else }}
name: {{ printf "app-%s" .Values.name }}
{{- end }}
# Check suffix
{{- if hasSuffix "-service" .Values.name }}
# Already has suffix
{{- end }}
Type Conversion Functions
toYaml / fromYaml
Converts between Go objects and YAML strings.
Examples:
# Convert to YAML
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
# Parse YAML string
{{- $config := .Values.configYaml | fromYaml }}
{{- $config.database.host }}
toJson / fromJson
Converts between Go objects and JSON strings.
Examples:
# Convert to JSON
data:
config.json: |
{{- .Values.config | toJson | nindent 4 }}
# Parse JSON
{{- $data := .Values.jsonString | fromJson }}
{{- $data.key }}
toString
Converts any value to a string.
Examples:
# Convert number to string
port: {{ .Values.port | toString | quote }}
# Convert boolean to string
enabled: {{ .Values.enabled | toString }}
List/Array Functions
list
Creates a list.
Examples:
# Create list
{{- $myList := list "a" "b" "c" }}
# Pass multiple arguments to template
{{- include "mychart.template" (list . "arg1" "arg2") }}
append / prepend
Adds elements to a list.
Examples:
# Append to list
{{- $list := list "a" "b" }}
{{- $list = append $list "c" }} # ["a", "b", "c"]
# Prepend to list
{{- $list := list "b" "c" }}
{{- $list = prepend $list "a" }} # ["a", "b", "c"]
first / rest / last
Gets elements from a list.
Examples:
# Get first element
{{- $first := first $myList }}
# Get all but first
{{- $rest := rest $myList }}
# Get last element
{{- $last := last $myList }}
has
Checks if a list contains an element.
Examples:
{{- if has "production" .Values.environments }}
# Production configuration
{{- end }}
compact
Removes empty/nil elements from a list.
Examples:
{{- $list := list "a" "" "b" nil "c" }}
{{- $cleaned := compact $list }} # ["a", "b", "c"]
uniq
Removes duplicate elements.
Examples:
{{- $list := list "a" "b" "a" "c" "b" }}
{{- $unique := uniq $list }} # ["a", "b", "c"]
sortAlpha
Sorts list alphabetically.
Examples:
{{- $sorted := .Values.items | sortAlpha }}
Dictionary/Map Functions
dict
Creates a dictionary.
Examples:
# Create dict
{{- $myDict := dict "key1" "value1" "key2" "value2" }}
# Pass custom context to template
{{- include "mychart.template" (dict "root" . "custom" "value") }}
# Complex context
{{- $ctx := dict "top" . "container" .Values.mainContainer "port" .Values.service.port }}
{{- include "mychart.container" $ctx }}
merge / mergeOverwrite
Merges dictionaries.
Examples:
# Merge dictionaries (dest, src1, src2, ...)
{{- $defaults := dict "replicas" 1 "port" 80 }}
{{- $overrides := dict "replicas" 3 }}
{{- $final := merge $overrides $defaults }}
# Result: {"replicas": 3, "port": 80}
# mergeOverwrite (right-most wins)
{{- $result := mergeOverwrite $dict1 $dict2 }}
keys / values
Gets keys or values from a dictionary.
Examples:
# Get all keys
{{- $keys := keys .Values.config }}
# Get all values
{{- $vals := values .Values.config }}
# Iterate over keys
{{- range $key := keys .Values.labels | sortAlpha }}
{{ $key }}: {{ index $.Values.labels $key }}
{{- end }}
pick / omit
Selects or excludes keys from a dictionary.
Examples:
# Pick specific keys
{{- $subset := pick .Values.config "host" "port" }}
# Omit specific keys
{{- $filtered := omit .Values.config "password" "secret" }}
hasKey
Checks if a dictionary has a key.
Examples:
{{- if hasKey .Values "database" }}
{{- if hasKey .Values.database "password" }}
# Password is configured
{{- end }}
{{- end }}
pluck
Gets a value by key from multiple dictionaries.
Examples:
# Get "name" from first dict that has it
{{- $name := pluck "name" .Values.override .Values.defaults | first }}
Encoding Functions
b64enc / b64dec
Base64 encode/decode.
Examples:
# Encode secret
apiVersion: v1
kind: Secret
data:
password: {{ .Values.password | b64enc }}
# Decode existing secret
{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
{{- $decoded := $secret.data.password | b64dec }}
{{- end }}
sha256sum
Generates SHA256 hash.
Examples:
# Create checksum annotation to trigger rolling update
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
# Hash password
data:
passwordHash: {{ .Values.password | sha256sum }}
uuidv4
Generates a random UUID v4.
Examples:
# Generate unique ID
id: {{ uuidv4 }}
Mathematical Functions
add / sub / mul / div / mod
Basic arithmetic operations.
Examples:
# Addition
replicas: {{ add .Values.baseReplicas 2 }}
# Subtraction
port: {{ sub .Values.maxPort 100 }}
# Multiplication
memory: {{ mul .Values.memoryPerPod .Values.replicas }}
# Division
cpuPerPod: {{ div .Values.totalCpu .Values.replicas }}
# Modulo
remainder: {{ mod .Values.value 10 }}
max / min
Gets maximum or minimum value.
Examples:
# Ensure at least 1 replica
replicas: {{ max 1 .Values.replicaCount }}
# Cap at 10 replicas
replicas: {{ min 10 .Values.replicaCount }}
floor / ceil / round
Rounding functions.
Examples:
# Round down
value: {{ floor 3.7 }} # 3
# Round up
value: {{ ceil 3.2 }} # 4
# Round to nearest
value: {{ round 3.5 }} # 4
Date Functions
now
Gets current time.
Examples:
# Current timestamp
annotations:
timestamp: {{ now | date "2006-01-02T15:04:05Z" }}
date
Formats a date/time.
Examples:
# Format date
date: {{ now | date "2006-01-02" }} # 2024-01-15
# Full timestamp
timestamp: {{ now | date "2006-01-02T15:04:05Z07:00" }}
# Custom format
generated: {{ now | date "Monday, 02-Jan-06 15:04:05 MST" }}
dateModify
Modifies a date.
Examples:
# Add 24 hours
tomorrow: {{ now | dateModify "24h" }}
# Subtract 7 days
lastWeek: {{ now | dateModify "-168h" }}
Comparison Functions
eq / ne
Equality and inequality.
Examples:
{{- if eq .Values.environment "production" }}
# Production settings
{{- end }}
{{- if ne .Values.replicaCount 1 }}
# Multiple replicas
{{- end }}
lt / le / gt / ge
Less than, less than or equal, greater than, greater than or equal.
Examples:
{{- if gt .Values.replicaCount 1 }}
# Multiple replicas
{{- end }}
{{- if le .Values.maxConnections 100 }}
# Low connection count
{{- end }}
and / or / not
Logical operations.
Examples:
{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled }}
# Ingress with TLS
{{- end }}
{{- if or (eq .Values.env "dev") (eq .Values.env "staging") }}
# Non-production environment
{{- end }}
{{- if not .Values.production }}
# Development mode
{{- end }}
Flow Control Functions
fail
Fails template rendering with an error message.
Examples:
{{- if not .Values.required }}
{{- fail "required value is not set" }}
{{- end }}
{{- if lt .Values.replicas 1 }}
{{- fail "replicas must be at least 1" }}
{{- end }}
coalesce
Returns the first non-empty value.
Examples:
# Use first non-empty value
name: {{ coalesce .Values.nameOverride .Values.name .Chart.Name }}
# Multiple fallbacks
host: {{ coalesce .Values.database.host .Values.defaultHost "localhost" }}
ternary
Inline if-then-else.
Examples:
# Ternary operator
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.production }}
# With comparison
replicas: {{ ternary 3 1 (eq .Values.env "production") }}
Indentation Functions
indent
Indents each line by N spaces.
Examples:
# Indent by 4 spaces
metadata:
labels:
{{ include "mychart.labels" . | indent 4 }}
nindent
Adds a newline then indents.
Examples:
# Newline + indent (preferred)
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}
Why prefer nindent: Most YAML structures need a newline before the indented content, making nindent the right choice in most cases.
Random Functions
randAlphaNum
Generates random alphanumeric string.
Examples:
# Generate random password
{{- $password := randAlphaNum 16 }}
# Generate unique suffix
name: {{ printf "%s-%s" .Release.Name (randAlphaNum 5) }}
randAlpha / randNumeric
Generates random alphabetic or numeric string.
Examples:
# Random letters only
code: {{ randAlpha 8 }}
# Random numbers only
id: {{ randNumeric 6 }}
randAscii
Generates random ASCII string.
Examples:
# Random ASCII characters
token: {{ randAscii 32 }}
File Functions
Files.Get
Reads a file from the chart.
Examples:
# Read configuration file
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
config.yaml: |
{{- .Files.Get "config/app.yaml" | nindent 4 }}
Files.GetBytes
Reads a file as bytes (for binary files).
Examples:
# Include binary file
data:
image.png: {{ .Files.GetBytes "files/image.png" | b64enc }}
Files.Glob
Reads multiple files matching a pattern.
Examples:
# Include all YAML files
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
{{- range $path, $content := .Files.Glob "config/*.yaml" }}
{{ base $path }}: |
{{- $content | nindent 4 }}
{{- end }}
Files.Lines
Reads a file line by line.
Examples:
# Process file line by line
{{- range .Files.Lines "config/servers.txt" }}
- {{ . }}
{{- end }}
Files.AsConfig / Files.AsSecrets
Creates ConfigMap or Secret data from files.
Examples:
# ConfigMap from files
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
{{- (.Files.Glob "config/*").AsConfig | nindent 2 }}
# Secret from files
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
data:
{{- (.Files.Glob "secrets/*").AsSecrets | nindent 2 }}
Path Functions
base / dir / ext / clean
Path manipulation functions.
Examples:
# Get filename
{{- $filename := base "/path/to/file.yaml" }} # file.yaml
# Get directory
{{- $dir := dir "/path/to/file.yaml" }} # /path/to
# Get extension
{{- $ext := ext "file.yaml" }} # .yaml
# Clean path
{{- $clean := clean "/path//to/../file" }} # /path/file
Regex Functions
regexMatch
Tests if a string matches a regex.
Examples:
{{- if regexMatch "^[0-9]+$" .Values.port }}
# Port is numeric
{{- end }}
regexFind / regexFindAll
Finds regex matches.
Examples:
# Find first match
{{- $match := regexFind "[0-9]+" .Values.version }}
# Find all matches
{{- $matches := regexFindAll "[A-Z]+" .Values.name -1 }}
regexReplaceAll
Replaces regex matches.
Examples:
# Replace all digits
name: {{ regexReplaceAll "[0-9]" .Values.name "" }}
# Replace pattern
clean: {{ regexReplaceAll "[^a-z0-9-]" .Values.name "" }}
regexSplit
Splits string by regex.
Examples:
# Split by delimiter
{{- $parts := regexSplit ":" .Values.imageTag -1 }}
Semantic Version Functions
semver / semverCompare
Work with semantic versions.
Examples:
# Parse semantic version
{{- $version := semver "1.2.3" }}
{{- $version.Major }} # 1
{{- $version.Minor }} # 2
{{- $version.Patch }} # 3
# Compare versions
{{- if semverCompare ">=1.20.0" .Capabilities.KubeVersion.Version }}
# Kubernetes 1.20 or higher
{{- end }}
Advanced Patterns
Custom Context Passing
{{- define "mychart.container" -}}
{{- $root := .root }}
{{- $container := .container }}
{{- $port := .port }}
name: {{ $container.name }}
image: {{ $container.image }}
ports:
- containerPort: {{ $port }}
{{- end }}
# Usage
{{- include "mychart.container" (dict "root" . "container" .Values.mainContainer "port" 8080) }}
Multi-Stage Processing
{{- $config := .Values.configYaml | fromYaml }}
{{- $merged := merge .Values.overrides $config }}
{{- $filtered := omit $merged "internalKey" }}
{{- toYaml $filtered | nindent 2 }}
Conditional Value Selection
{{- $value := "" }}
{{- if .Values.custom }}
{{- $value = .Values.custom }}
{{- else if .Values.default }}
{{- $value = .Values.default }}
{{- else }}
{{- $value = "fallback" }}
{{- end }}
Pipeline Composition
# Chain multiple functions
value: {{ .Values.name | trim | lower | replace " " "-" | trunc 63 | trimSuffix "-" | quote }}
# Multi-line pipeline
{{- .Values.config
| toYaml
| indent 2
| trim }}
Function Combination Examples
Resource Name Generation
{{- define "mychart.resourceName" -}}
{{- $name := include "mychart.fullname" . -}}
{{- $suffix := .suffix | default "" -}}
{{- if $suffix }}
{{- printf "%s-%s" $name $suffix | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
Safe Value Extraction
{{- $password := "" }}
{{- if and .Values.database (hasKey .Values.database "password") }}
{{- $password = .Values.database.password }}
{{- else }}
{{- $password = randAlphaNum 16 }}
{{- end }}
Configuration Merging
{{- $defaults := .Files.Get "config/defaults.yaml" | fromYaml }}
{{- $overrides := .Values.config | default (dict) }}
{{- $final := merge $overrides $defaults }}
config: |
{{- $final | toYaml | nindent 2 }}
Performance Tips
- Cache template results - Use variables to avoid recalculating:
{{- $fullname := include "mychart.fullname" . }}
name: {{ $fullname }}
matchLabels:
app: {{ $fullname }}
- Minimize lookups -
lookupqueries are expensive:
{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
# Use $secret multiple times
{{- end }}
- Use with for scoping - Reduces template complexity:
{{- with .Values.ingress }}
{{- if .enabled }}
host: {{ .host }}
{{- end }}
{{- end }}
Debugging Functions
printf
Formatted string output for debugging.
Examples:
# Debug values
{{- printf "Debug: name=%s, replicas=%d" .Values.name .Values.replicas | fail }}
toYaml for inspection
# Inspect values
{{- toYaml .Values | fail }}
Common Gotchas
- Nil vs Empty String
# This fails if value is nil
{{- if .Values.optional }} # Error if nil!
# This works
{{- if .Values.optional | default "" }} # Safe
- Type Conversion
# Port is integer in values but needs string comparison
{{- if eq (.Values.port | toString) "80" }}
- Pipeline Precedence
# Wrong - quote applies to "true", not the result
{{- if .Values.enabled | quote }}
# Right - use parentheses
{{- if (.Values.enabled | quote) }}
- Whitespace in Conditionals
# Creates extra whitespace
{{ if .Values.enabled }}
value: true
{{ end }}
# Better - chomp whitespace
{{- if .Values.enabled }}
value: true
{{- end }}