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
- Helm - a command-line tool that installs apps into Kubernetes as a unit.
- chart - a directory (or
.tgzarchive of it) with the app's Kubernetes YAML written as templates, plus default settings. - template (Helm sense) - a YAML file with blanks like
{{ .Values.replicaCount }}that Helm fills in. The template language is Go's (Helm is written in Go); lesson 25.24 teaches it. - values - the settings that fill the blanks: defaults in the chart's
values.yaml, overridden per environment (lesson 25.21). - render - fill in the templates with values, producing plain YAML.
- release - one installed copy of a chart in a namespace, under a name.
- revision - one numbered version of a release; every change adds one.
Two jobs in one tool
Helm does two unrelated things, and most confusion comes from mixing them up:
- Templating. A chart is a directory of templates plus default values.
helm templaterenders it to plain Kubernetes YAML. Nothing touches a cluster. - Release management.
helm installrenders the chart and records what it applied as a release: a name, a namespace, and a numbered list of revisions. Everyupgradeand everyrollbackadds 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:
- Deleting the namespace deletes the release history.
helm listin another namespace will never show it - releases are per namespace, always pass-n. - Anyone who can read Secrets in the namespace can read every value you ever passed to the release, including the ones you thought were secret.
helm upgrade --history-max(default 10) prunes old revisions; you can only roll back to what is still stored.- There is no Helm server. Two people running
helm upgradeat once race on these Secrets; the loser seesanother operation (install/upgrade/rollback) is in progress.
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:
--atomicis deprecated in favour of--rollback-on-failure(same idea: on failure, roll back automatically; it implies waiting).--forceis--force-replace.--waitis a strategy: without it Helm waits only for hooks (hookOnly); bare--waitmeanswatcher- wait until every resource reports ready (the rules for "ready" come from a library called kstatus). A failed wait says exactly which object:resource Deployment/pay-lab/payments-api not ready. status: InProgress, message: ....- New installs are applied server-side: the API server itself merges the change and records which tool owns each field (the field manager, here
helm). If another controller owns a field, Helm gets a conflict instead of silently overwriting it.
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
- Tell Helm's two jobs apart: rendering a chart, and managing a release.
- Name a chart's files and say what
versionandappVersioneach mean. - Install, upgrade, roll back and inspect a release, and find where Helm stores it.