OnCallReady

Lesson 25.19 · CI/CD Pipelines & Helm · 16 min read

Helm: charts, releases and revisions

In plain words

Think of a Lego set. The box has instructions with gaps: "use N wheels", "pick a colour". You fill in the gaps and build it. Now imagine a notebook next to the model where you write each change: "version 1: red car, 4 wheels", "version 2: blue car". If version 2 falls apart, you look at the notebook and rebuild version 1 exactly.

A Helm chart is the instruction booklet: templates plus default values.yaml. helm template just prints the instructions filled in. helm install builds it in the cluster and starts the notebook: a release, with numbered revisions, each stored as a Secret like sh.helm.release.v1.payments-api.v2. helm rollback rebuilds an old page, but writes it as a new page, never erasing history.

The problem

The payments app is not one YAML file: a Deployment, a Service, a ConfigMap, maybe an Ingress - and dev needs 1 replica, prod 3, with different URLs. Copying the files per environment and editing numbers by hand drifts fast, and kubectl apply does not remember what it applied, so it cannot clean up an object you removed or go back to "what we had yesterday". Helm solves both.

What you need to know already: 15.40 (kubectl apply and diff), 15.16 (Deployments), 15.29 and 15.33 (ConfigMaps, Secrets and base64), 25.2 (semantic versions).

Words for this lesson

Two jobs in one tool

Helm does two unrelated things, and most confusion comes from mixing them up:

  1. Templating. A chart is a directory of templates plus default values. helm template renders it to plain Kubernetes YAML. Nothing touches a cluster.
  2. Release management. helm install renders the chart and records what it applied as a release: a name, a namespace, and a numbered list of revisions. Every upgrade and every rollback adds a revision. That record is what lets Helm compute what to delete on the next upgrade, roll back to an exact previous state, and tell you who deployed what.

Everything in this part of the chapter is one of those two. When a deployment goes wrong, first ask which one failed: did the chart render the wrong YAML, or did the release get into a state Helm will not move from?

A chart, file by file

New words in this listing: JSON Schema is a standard way to describe what shape a JSON (or YAML) document must have; named templates are reusable snippets (25.24); hooks and tests run at set moments (25.26); CRDs (CustomResourceDefinitions) add new object kinds to the cluster; dependencies are other charts this chart includes (25.29).

payments-api/
  Chart.yaml            name, version (of the CHART), appVersion (of the APP), dependencies
  values.yaml           the defaults - the chart's public API
  values.schema.json    optional: JSON Schema the merged values must satisfy
  templates/            every *.yaml here is rendered and applied
    _helpers.tpl        files starting with _ are not manifests: named templates live here
    deployment.yaml
    service.yaml
    configmap.yaml
    NOTES.txt           rendered and printed after install/upgrade, not applied
    tests/              pods annotated helm.sh/hook: test (helm test)
  charts/               dependencies, as directories or .tgz (helm dependency update fills it)
  crds/                 CRDs: installed once, before everything, never upgraded or deleted
  .helmignore           what helm package leaves out

Chart.yaml for our service:

apiVersion: v2                 # v2 = the chart format of Helm 3 and later
name: payments-api
description: The payments API (Spring Boot)
type: application              # or "library": only named templates, cannot be installed
version: 0.4.0                 # the chart's own SemVer - bump it on ANY chart change
appVersion: "1.4.0"            # informational: which app version this chart ships by default

version vs appVersion is a classic interview question. version identifies the package: two different charts must never share a version, because repositories and caches key on it. appVersion is just metadata - here the chart also uses it as the default image tag ({{ .Values.image.tag | default .Chart.AppVersion }} reads "the image tag from the values, or if none is set, the chart's appVersion"). A new app release with no template change can bump only appVersion and the patch version; a template change bumps version even if the app is the same.

A release, and where it lives

helm install NAME CHART installs the chart at path CHART as release NAME. -n pay-lab the namespace; --create-namespace create it if missing; --wait return only when the pods are ready (otherwise Helm returns as soon as the objects are sent to the API server). Helm prints the release's status and its NOTES.txt:

# from the directory that holds the chart (the next mission)
helm install payments-api ./payments-api -n pay-lab --create-namespace --wait
NAME: payments-api
LAST DEPLOYED: Thu Sep 24 10:02:11 2026
NAMESPACE: pay-lab
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
payments-api 1.4.0 is installed as release payments-api in pay-lab.

The record is a Secret in the release's namespace, one per revision:

# after an install and one upgrade (-l owner=helm: only Secrets with that label)
kubectl -n pay-lab get secrets -l owner=helm
NAME                                 TYPE                 DATA   AGE
sh.helm.release.v1.payments-api.v1   helm.sh/release.v1   1      2m
sh.helm.release.v1.payments-api.v2   helm.sh/release.v1   1      40s

Its labels carry name, owner=helm, status and version; its data is the whole release (chart, the values you supplied, the rendered manifest - the final YAML -, hooks, notes) compressed with gzip and base64-encoded. Consequences worth knowing:

The verbs

In this table NAME is the release name, CHART a chart directory, archive or repository reference, REV a revision number:

helm install NAME CHART        revision 1 (fails if NAME exists in the namespace)
helm upgrade NAME CHART        revision N+1; -i/--install creates it if missing
helm rollback NAME [REV]       revision N+1 whose CONTENT is REV (default: the previous one)
helm uninstall NAME            deletes the objects; --keep-history keeps the Secrets
helm list [-a]                 releases in a namespace (-A: all namespaces)
helm history NAME              every revision: status, chart, app version, description
helm status NAME               the current revision, its resources and NOTES
helm get values NAME [--all]   what YOU supplied (--all: the merged result)
helm get manifest NAME         exactly what was applied (--revision N for older ones)

A rollback never goes back in the revision numbers: rolling back revision 3 to revision 1 creates revision 4 with revision 1's content and the description Rollback to 1. The history is an append-only log, which is what auditors want.

Statuses you will meet: deployed (current), superseded (an older one that was deployed), failed, pending-install / pending-upgrade / pending-rollback (an operation started and has not finished - or its process died), uninstalling, uninstalled (only with --keep-history).

Helm 4, in one screen

Helm 4.0 shipped in November 2025; the lab runs 4.3. Charts are still apiVersion: v2 and a Helm 3 chart installs unchanged. What changed for you:

Everything below works the same on a Helm 3.18 client except where noted.

Reading before touching

Three commands you run before any change to a release you did not create:

helm -n pay-prod history payments-api      # what happened, in order
helm -n pay-prod get values payments-api   # what someone passed on the command line
helm -n pay-prod get manifest payments-api | kubectl diff -f -   # has anyone kubectl'd over it?

The first shows every revision; the second prints the values someone passed at install or upgrade time; the third pipes (|) the manifest Helm stored into kubectl diff -f - (-f - = read the YAML from standard input).

The last one is the drift check (drift = the live objects no longer match what was deployed): helm get manifest is what Helm thinks is deployed, kubectl diff compares it with what is deployed. A non-empty diff means somebody edited objects by hand, and your next upgrade will fight them.

Later (Ch 26): GitOps makes this check permanent: a controller compares git with the cluster all the time.

What you can now do

Why it helps

When a deploy goes wrong, the first split to make is "did the chart render the wrong YAML, or is the release stuck?" That decides whether you run helm template locally or helm history against the cluster. The three commands before touching someone else's release, history, get values and get manifest | kubectl diff -f -, tell you what happened, what someone passed by hand, and whether anyone edited objects with kubectl.

Knowing where releases live explains odd moments: helm list shows nothing because you forgot -n, a namespace deletion wiped release history, anyone with Secret read access can see every value passed. And "chart version vs appVersion" is one of the most common Helm interview questions. Helm 4 changes such as --rollback-on-failure and server-side apply will also show up in newer runbooks and job tasks.

FAQ

What is the difference between a chart's version and appVersion?

version is the chart package's own SemVer; bump it on any change to the chart, because repositories and caches key on it and two different charts must never share a version. appVersion is metadata saying which application version the chart ships by default; many charts use it as the default image tag. A new app release may bump appVersion and the patch version; a template change always bumps version.

Where does Helm store release information?

In Secrets in the release's namespace, one per revision, named sh.helm.release.v1.<name>.v<revision> with label owner=helm. Each holds the chart, the supplied values, the rendered manifest, hooks and notes, gzipped and base64-encoded. There is no server component. Releases are therefore per namespace (always pass -n), deleting the namespace deletes history, and Secret read access reveals every value ever passed.

Does helm rollback go back to the old revision number?

No. Rolling back from revision 3 to 1 creates revision 4 with revision 1's content and the description Rollback to 1. History only grows. Rollback can only reach revisions still stored; --history-max (default 10) prunes older ones. Note that hooks and CRDs are not rolled back, and a database migration run by a hook stays applied.

What is the difference between helm template, helm install --dry-run and helm install?

helm template renders the chart locally to YAML; no cluster needed, and lookup returns empty. helm install --dry-run=server renders against the cluster, so lookup works and the API server validates the objects, but nothing is saved. helm install renders, applies the objects and records the release. Use template in CI to review output, --dry-run=server to validate against a real cluster.

What changed for me in Helm 4?

Charts are still apiVersion: v2, and Helm 3 charts install unchanged. The flags you use changed: --atomic is deprecated in favour of --rollback-on-failure, and --force became --force-replace. --wait became a strategy, watcher by default when given, using kstatus, with clearer errors naming the object not ready. And installs use server-side apply with field manager helm, so conflicts with other controllers surface instead of being silently overwritten.

In an interview Mid

What is Helm and what problems does it solve?

Helm installs an app's Kubernetes objects as one unit. It does two separate jobs:

  1. Templating - a chart is a directory of templates plus default values.yaml; per-environment values fill the blanks, so one chart serves dev (1 replica) and prod (3) without copied YAML. helm template renders it without touching a cluster.
  2. Release management - helm install records a release (name + namespace) with numbered revisions; every upgrade and rollback adds one. That record lets Helm delete objects a new version no longer renders (kubectl apply cannot), roll back to an exact earlier state (helm rollback creates a new revision with the old content), and show who deployed what (helm history).

Details interviewers like: version is the chart package's version, appVersion the app's; the release record is a Secret per revision in the namespace (so releases are per namespace, and anyone reading Secrets reads every value passed); --history-max 10 limits rollbacks. Before touching a release: helm history, helm get values, and helm get manifest | kubectl diff -f - to catch drift.

Also asked: What is the difference between a chart's version and its appVersion? · Someone says helm upgrade succeeded but the change is not in the cluster. How do you investigate? · Where does Helm store release state, and what follows from that?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.