Kubernetes
Helm
Rendering, installing and rolling back releases, plus the template behaviour that explains most chart surprises.
Cheatsheet #
| Task | Command |
|---|---|
| See the YAML before applying | helm template myapp ./chart -f values.yaml |
| Diff against what is installed | helm diff upgrade myapp ./chart -f values.yaml |
| Install or upgrade idempotently | helm upgrade --install myapp ./chart -f values.yaml |
| Values actually in effect | helm get values myapp --all |
| Manifests actually applied | helm get manifest myapp |
| Release history | helm history myapp |
| Roll back one revision | helm rollback myapp |
| Wait for readiness | helm upgrade --install myapp ./chart --wait --timeout 5m |
| Validate chart syntax | helm lint ./chart |
| Resolve dependencies | helm dependency update ./chart |
| Show a chart’s default values | helm show values oci://registry/chart --version 1.2.3 |
| Delete and keep history | helm uninstall myapp --keep-history |
Render before installing #
Helm is a templating engine with a release ledger. helm template runs the engine locally and prints manifests; helm install does the same and posts the result to the API server, recording it in a Secret named sh.helm.release.v1.<release>.v<revision>.
helm template myapp ./chart -f values.yaml | less
helm template myapp ./chart -f values.yaml | kubectl apply --dry-run=server -f -
helm upgrade --install myapp ./chart -f values.yaml --dry-run=server
helm diff upgrade myapp ./chart -f values.yaml # plugin: helm-diff--dry-run=server sends the manifests through admission control, which catches schema errors, webhooks and quota problems that local rendering cannot see.
Installing and upgrading #
helm upgrade --install myapp ./chart \
-n myns --create-namespace \
-f values.yaml -f values.prod.yaml \
--set image.tag=1.4.2 \
--wait --timeout 10m \
--atomicLater -f files override earlier ones key by key; --set beats every file. Lists replace rather than merge, so a single --set ingress.hosts[0].host=... discards the rest of the list.
--wait blocks until pods, PVCs and Services report ready. --atomic implies --wait and rolls back automatically on failure, which is what you want in CI and not what you want when you need the failed state to debug.
helm list -A # releases and revisions
helm get values myapp --all # user values merged with chart defaults
helm get manifest myapp | kubectl diff -f - # drift between release and cluster
helm status myappRolling back #
Every revision stores the rendered manifests, so rollback re-applies an earlier snapshot rather than re-rendering the chart.
helm history myapp
helm rollback myapp 4 --waitRollback does not undo side effects: schema migrations, external resources created by hooks and deleted PVCs stay as they are.
When a release is stuck #
A release in pending-install, pending-upgrade or uninstalling means the Helm process died holding the lock. Nothing is corrupted; the ledger just never got its final write.
helm history myapp # find the last deployed revision
helm rollback myapp <last-good> # usually enough
kubectl get secret -n myns -l owner=helm,name=myapp # the ledger itself
kubectl delete secret sh.helm.release.v1.myapp.v7 -n myns # last resort: drop the bad revisionanother operation (install/upgrade/rollback) is in progress with no running Helm process is the same condition.
Writing a chart #
chart/
Chart.yaml # name, version, appVersion, dependencies
values.yaml # defaults, every key documented
values.schema.json # optional JSON Schema, validated on install
templates/
_helpers.tpl # named templates: fullname, labels, selectors
deployment.yaml
NOTES.txt # printed after install{{- define "chart.labels" -}}
app.kubernetes.io/name: {{ include "chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}| Behaviour | Detail |
|---|---|
{{- and -}} | Trim whitespace left or right; the cause of most YAML indentation failures |
| nindent 4 | Indent a block by 4, newline first — use it, not manual spacing |
required "msg" .Values.x | Fail rendering with a readable message instead of producing invalid YAML |
default | {{ .Values.x | default "y" }} — empty string, 0 and false count as unset |
toYaml | Emit a values sub-tree verbatim: {{- toYaml .Values.resources | nindent 12 }} |
lookup | Read live cluster state; returns empty during helm template |
.Release.IsUpgrade | Branch between install and upgrade |
tpl | Render a string from values as a template, for user-supplied snippets |
| Hooks | helm.sh/hook: pre-upgrade runs a Job before the manifests are applied |
Selector labels must be stable: matchLabels is immutable, so including app.kubernetes.io/version there makes every chart upgrade fail.
Dependencies #
# Chart.yaml
dependencies:
- name: postgresql
version: "16.x.x"
repository: oci://registry-1.docker.io/bitnamicharts
condition: postgresql.enabledhelm dependency update ./chart # writes Chart.lock and charts/*.tgz
helm dependency build ./chart # install exactly what Chart.lock pinsSubchart values are set under the subchart’s name; global: is the only key both parent and children see. Commit Chart.lock and let dependency build enforce it, or builds drift as upstream publishes new versions.
Helm with GitOps #
Argo CD and Flux render charts themselves rather than running helm install, so no release ledger exists in the cluster and helm list shows nothing. Debug with helm template locally and the controller’s own diff, not helm get.
Oneliners #
# Every release across the cluster, with chart versions
helm list -A -o json | jq -r '.[] | [.namespace, .name, .chart, .app_version, .status] | @tsv'
# Releases that are not deployed
helm list -A --all -o json | jq -r '.[] | select(.status!="deployed") | [.namespace,.name,.status] | @tsv'
# What changed between two revisions
diff <(helm get manifest myapp --revision 6) <(helm get manifest myapp --revision 7)
# Images a chart would run, without installing
helm template myapp ./chart -f values.yaml | grep -E '^\s+image:' | sort -u
# Render just one template
helm template myapp ./chart -s templates/deployment.yaml
# Fail fast on undefined values
helm template myapp ./chart 2>&1 | grep -n 'nil pointer\|<no value>'
# Pull a chart to inspect it
helm pull oci://registry-1.docker.io/bitnamicharts/postgresql --version 16.0.0 --untar
# Values a release was installed with, as a reusable file
helm get values myapp -o yaml > values.recovered.yaml
# Clean up old release ledger secrets
kubectl get secret -n myns -l owner=helm --sort-by=.metadata.creationTimestamp | head -n -10