Software Engineering Wiki

Kubernetes

Helm

Rendering, installing and rolling back releases, plus the template behaviour that explains most chart surprises.

Cheatsheet #

TaskCommand
See the YAML before applyinghelm template myapp ./chart -f values.yaml
Diff against what is installedhelm diff upgrade myapp ./chart -f values.yaml
Install or upgrade idempotentlyhelm upgrade --install myapp ./chart -f values.yaml
Values actually in effecthelm get values myapp --all
Manifests actually appliedhelm get manifest myapp
Release historyhelm history myapp
Roll back one revisionhelm rollback myapp
Wait for readinesshelm upgrade --install myapp ./chart --wait --timeout 5m
Validate chart syntaxhelm lint ./chart
Resolve dependencieshelm dependency update ./chart
Show a chart’s default valueshelm show values oci://registry/chart --version 1.2.3
Delete and keep historyhelm 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 \
  --atomic

Later -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 myapp

Rolling 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 --wait

Rollback 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 revision

another 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 }}
BehaviourDetail
{{- and -}}Trim whitespace left or right; the cause of most YAML indentation failures
| nindent 4Indent a block by 4, newline first — use it, not manual spacing
required "msg" .Values.xFail rendering with a readable message instead of producing invalid YAML
default{{ .Values.x | default "y" }} — empty string, 0 and false count as unset
toYamlEmit a values sub-tree verbatim: {{- toYaml .Values.resources | nindent 12 }}
lookupRead live cluster state; returns empty during helm template
.Release.IsUpgradeBranch between install and upgrade
tplRender a string from values as a template, for user-supplied snippets
Hookshelm.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.enabled
helm dependency update ./chart     # writes Chart.lock and charts/*.tgz
helm dependency build ./chart      # install exactly what Chart.lock pins

Subchart 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

Last updated 15 September 2026 · Edit this page