Tools · Helm

Helm

Helm is the package manager for Kubernetes: it bundles the dozen-or-more YAML objects a real application needs into one versioned, parameterised artefact called a chart, and it remembers every version of that artefact it installed so you can upgrade or roll back with a single command. It solves the platform problem of distribution — how do a hundred teams install the same ingress controller, the same monitoring stack, the same internal service template, correctly, in five environments, without copy-pasting eight thousand lines of YAML that immediately drift apart?

☺ Explain it like I’m 10

Imagine a flat-pack furniture kit. The box holds all the planks and screws (the YAML), plus an instruction sheet with blanks: “choose your colour, choose how many shelves.” That box is a chart. When you actually build one and put it in your room, that built thing is a release — and Helm keeps a little notebook of every version you ever built, so if the new shelf wobbles you say “go back to version 3” and it rebuilds the old one. The clever bit is that anyone can hand you a box: you don’t have to know how the furniture works to put it up.

🦫Your host for this topic: Benny the Beaver — Benny is the one who packs the box. He builds the chart, labels every knob, writes the instructions on the lid, and makes sure the thing can be un-built again if it turns out wrong.

What Helm is and the problem it solves

☺ Like you’re 10: It puts a whole app’s YAML in one labelled box with knobs on the front, so you can install it by turning knobs instead of writing YAML.

Kubernetes has no concept of “an application.” It has Deployments, Services, ConfigMaps, ServiceAccounts, Ingresses, PodDisruptionBudgets and HorizontalPodAutoscalers — and it happily lets you have some of them and not others. A realistic web service is eight to fifteen objects; a monitoring stack is several hundred. Helm invents the missing noun. A chart is a versioned directory containing all of those objects as templates plus a file of default settings, and Helm can fetch it from a repository, render it, install it, track it, upgrade it and remove it as one unit.

The problem before packaging

Without a package layer you get one of two failure modes, and most organisations get both. Either every team hand-writes near-identical YAML — so the security team’s “always set runAsNonRoot” lands in nine of eleven services — or someone copies the whole folder per environment, and within a quarter dev/ and prod/ have silently diverged. Helm answers both: one templated source of truth, many parameterised instantiations, and a versioned artefact other teams consume rather than fork.

Chart, release, revision — the three nouns

Almost every Helm misunderstanding is a confusion between these. A chart is the package: inert, versioned, distributable, like a .deb. A release is one installed instance of a chart, with a name and a namespace — you can install the same chart three times under three release names in one cluster. A revision is a numbered snapshot of that release: revision 1 is the install, each successful helm upgrade makes revision 2, 3, 4, and helm rollback re-applies an old one as a new revision. Say those three words correctly and the CLI stops feeling arbitrary.

◆ Key idea

Helm 3 has no server-side component. The old Helm 2 “Tiller” pod — a cluster-wide, over-privileged daemon that everybody hated — was deleted in Helm 3. The modern helm binary is a pure client: it renders templates locally, talks to the Kubernetes API with your kubeconfig and your RBAC, and records what it did in an ordinary Kubernetes Secret. That is why “installing Helm” means putting a binary on your laptop, not deploying anything.

What it is not

Helm is a renderer and a release tracker, not a reconciler. It does nothing between commands: if someone hand-edits a Deployment Helm installed, Helm neither notices nor cares until your next upgrade. Continuous correction is GitOps’ job, via Argo CD or Flux. It is also not a secret manager — templating a password into a chart still puts the plaintext in your values file, so see Secrets Management and External Secrets Operator. And it is not a competitor to Kustomize so much as a different answer to the same question, compared head-to-head in Configuration & Templating.

Where it fits in a platform

☺ Like you’re 10: Helm sits just before the cluster door: it turns your knob settings into real YAML that something else then delivers.

In the layered platform model Helm belongs to the configuration and delivery plane — specifically to the rendering step, upstream of whatever actually applies manifests to the Kubernetes substrate. It is the layer that turns “we want checkout, six replicas, prod resource limits” into concrete API objects. On most platforms it plays two quite different roles at once, and it is worth naming them separately.

Two jobs: consuming charts and publishing them

The first is consumption: nearly every platform component you install — the ingress controller, cert-manager, Prometheus, Grafana, Kyverno, Crossplane, Cilium, Velero — ships as an upstream chart, and installing it any other way means maintaining a fork of someone else’s YAML forever. The second is publication: the platform team writes one paved-road chart (or a library chart of shared helpers), publishes it internally, and every application team consumes it with a fifteen-line values file. That second job is a genuine self-service and developer-experience lever — the golden path arrives as a version number teams can bump.

Its neighbours

Upstream, CI builds an image and bumps a tag in a values file. Beside Helm sits Kustomize, often used to patch the output of a chart you don’t control. Downstream sit the reconcilers: Argo CD renders charts with helm template inside its repo-server, while Flux’s helm-controller drives the real Helm SDK through a HelmRelease — a distinction that produces one of the sharpest gotchas below. Admission policy from Kyverno or OPA Gatekeeper still inspects everything a chart emits, so a chart that omits resource limits is rejected regardless of how nicely it templates.

CNPE domain relevance

Helm is not on the official CNPE tool list — but it is everywhere in the material the list does cover, because it is how most platform components get installed. Expect it to show up as connective tissue in GitOps & Continuous Delivery (25%) and Platform Architecture & Infrastructure (15%) rather than as a task’s headline subject: a scenario may hand you a chart to render, a values file to correct, or a released component to inspect. The primary lesson on this site is Configuration & Templating; the exam guide has the full blueprint weighting.

How it works — anatomy and the render pipeline

☺ Like you’re 10: Helm reads your knob settings, fills in the blanks on the instruction sheets, and posts the finished pages to the cluster — then writes down what it posted.

Helm has no CRDs of its own and installs no controllers. Its “architecture” is a pipeline that runs entirely on your machine, plus one storage convention inside the cluster. Understanding that pipeline is most of the debugging skill.

Chart Chart.yaml values.yaml templates/ · crds/ charts/ (subcharts) Your overrides -f values-prod.yaml --set image.tag=1.4.3 merge values --set beats -f -f beats defaults render Go text/template + Sprig · _helpers.tpl text in, text out Kubernetes API 3-way merge patch your RBAC, your kubeconfig Release Secret sh.helm.release.v1. checkout.v7 apply record All of this runs on your machine — Helm 3 has no in-cluster server. Only the last two arrows touch the cluster.

The chart directory

A chart is a directory whose layout Helm knows by convention. Every file has a job, and the two that people forget — crds/ and values.schema.json — are the two that cause the most pain later.

checkout/
├── Chart.yaml            # name, version, appVersion, dependencies — the only mandatory file
├── values.yaml           # the DEFAULT knob settings; also your user-facing documentation
├── values.schema.json    # optional JSON Schema — rejects bad values BEFORE rendering
├── templates/
│   ├── deployment.yaml   # Go templates → rendered to manifests
│   ├── service.yaml
│   ├── configmap.yaml    # hashed into a pod annotation by the checksum trick below
│   ├── ingress.yaml
│   ├── _helpers.tpl      # leading underscore = NOT rendered as a manifest; named templates
│   ├── NOTES.txt         # templated; printed to the user after install/upgrade
│   └── tests/
│       └── connection.yaml   # a Pod annotated helm.sh/hook: test, run by `helm test`
├── crds/                 # PLAIN YAML (no templating). Installed first, NEVER upgraded.
├── charts/               # vendored subcharts, populated by `helm dependency update`
├── Chart.lock            # pinned dependency versions + digest
└── .helmignore           # what to exclude when packaging

The render pipeline and the built-in objects

Rendering is text substitution over Go’s text/template language, extended with the Sprig function library (default, quote, b64enc, sha256sum, trunc, randAlphaNum, and a hundred more) plus a short list of functions Helm itself adds (include, required, toYaml, tpl, lookup). Shared logic lives in _helpers.tpl as define blocks and is pulled in with include — never template, because only include returns a string you can pipe into nindent. Everything a template can see comes from six built-in objects:

ObjectHoldsYou reach for it when
.ValuesThe merged values — defaults plus every -f and --setAlways. This is the user’s knob panel.
.Release.Name, .Namespace, .Revision, .Service, .IsInstall, .IsUpgradeNaming resources, or doing something only on first install
.ChartThe parsed Chart.yaml — notably .Name, .Version, .AppVersionDefaulting the image tag, and standard labels
.Capabilities.KubeVersion and .APIVersions.Has "…"One chart must work across several cluster versions
.FilesNon-template files inside the chart (.Get, .Glob, .AsConfig)Stuffing a real config file into a ConfigMap verbatim
.Template.Name and .BasePath of the template being renderedThe checksum/config pod-roll trick below

Where a release actually lives

After a successful install or upgrade, Helm stores a gzipped, base64-encoded copy of the release — rendered manifests, supplied values, chart metadata, status — as a Kubernetes Secret of type helm.sh/release.v1 in the release’s namespace, named sh.helm.release.v1.<release>.v<revision>. That is the whole database. helm list is a Secret query; helm history lists those Secrets; helm rollback reads an old one and re-applies its manifests. Delete the Secrets and Helm forgets the release exists while the workloads keep running quite happily — which is exactly what makes the release history both useful and fragile.

🦆 Dot’s-eye view

“I have never opened a chart’s templates/ folder. My whole interface is a twenty-line values.yaml in my service repo — image tag, replica count, two env vars, a hostname. When the platform team ships chart 2.4.0 with better probes and a PodDisruptionBudget, I get all of it by bumping one number. That is the version of ‘paved road’ I actually like.”

Dependencies, subcharts and library charts

Charts compose. Declaring dependencies in Chart.yaml and running helm dependency update vendors those charts into charts/ and pins them in Chart.lock. Four knobs matter: condition (a values path — if it is false the subchart is skipped entirely, the usual “bundle Postgres in dev, use RDS in prod” switch), tags (turn groups of subcharts on and off together), alias (install the same subchart twice under different names), and import-values (lift a subchart’s values up into the parent). Values flow downward: a parent may set postgresql.auth.username to override its subchart, and anything under the reserved global: key is visible to every chart in the tree — the correct place for a shared image registry or environment name.

A library chart (type: library in Chart.yaml) is the composition move platform teams underuse. It renders nothing of its own — only defined templates — and other charts depend on it to inherit a house style: standard labels, security context, probe block. Bump its version and every consumer picks up the new standard on their next upgrade. That is how “always set runAsNonRoot” becomes a property of the platform rather than a code-review nag.

The resources you will actually write

☺ Like you’re 10: Three files do nearly all the work: the label on the box, the knobs, and one instruction sheet.

Here is a small but genuinely production-shaped chart. Read the comments — they mark the decisions reviewers actually argue about.

Chart.yaml and values.yaml

# Chart.yaml
apiVersion: v2                 # v2 = the Helm 3+ chart API. (v1 charts are the Helm 2 era format.)
name: checkout
description: The Acme checkout service
type: application              # or "library" for a helpers-only chart
version: 2.4.0                 # the CHART version — bump on every chart change (SemVer)
appVersion: "1.4.3"            # the APP version shipped by default; a string, always quote it
kubeVersion: ">=1.27.0-0"      # refuse to install on older clusters

dependencies:
  - name: postgresql
    version: "~15.5.0"                # a SemVer RANGE; Chart.lock pins the exact resolved version
    repository: https://charts.example.com   # whichever upstream chart repo you use
    condition: postgresql.enabled     # false in prod → subchart skipped entirely
  - name: acme-common                 # a LIBRARY chart of shared labels/securityContext
    version: "1.2.0"
    repository: oci://ghcr.io/acme/charts
---
# values.yaml — the defaults, and your user-facing documentation. Comment every knob.
replicaCount: 2
image:
  repository: ghcr.io/acme/checkout
  tag: ""                      # empty → falls back to .Chart.AppVersion
  pullPolicy: IfNotPresent
logLevel: info
resources:
  requests: { cpu: 100m, memory: 128Mi }
  limits:   { memory: 512Mi }  # no CPU limit on purpose — avoids throttling surprises
podSecurityContext:
  runAsNonRoot: true
  seccompProfile: { type: RuntimeDefault }
ingress:
  enabled: false
  host: ""
postgresql:
  enabled: false               # dev overrides this to true; prod uses the managed DB
global:
  environment: dev             # visible to EVERY chart in the dependency tree

A template that survives review

Two habits separate charts that age well from charts that rot: pipe structured values through toYaml | nindent instead of hand-indenting, and put every name and label behind a named template so you can change the convention in one place.

# templates/_helpers.tpl
{{- define "checkout.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "checkout.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
acme.io/environment: {{ .Values.global.environment }}
{{- end -}}
---
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "checkout.fullname" . }}
  labels: {{- include "checkout.labels" . | nindent 4 }}   # include+nindent, never `template`
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      annotations:
        # roll the pods whenever the ConfigMap content changes — the classic checksum trick
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels: {{- include "checkout.labels" . | nindent 8 }}
    spec:
      securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: checkout
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: LOG_LEVEL
              value: {{ .Values.logLevel | quote }}          # quote, or `false`/`on` become booleans
            - name: DB_HOST
              value: {{ required "dbHost is required in prod" .Values.dbHost | quote }}
          readinessProbe:
            httpGet: { path: /healthz, port: http }
          resources: {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector: {{- toYaml . | nindent 8 }}              # `with` skips the key entirely if empty
      {{- end }}

values.schema.json and NOTES.txt

A chart consumed by other teams should refuse bad input rather than emit broken YAML. values.schema.json is plain JSON Schema — draft-07 is the dialect Helm 3’s validator understands, so target that unless you know your Helm build supports a newer one. Helm validates the merged values against it during install, upgrade, lint and template, and fails with a readable error instead of a 400 from the API server three seconds later. NOTES.txt is the other half of the contract — it is rendered with the same values and printed after install, so use it to tell the user what to do next rather than to congratulate them.

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["image", "replicaCount"],
  "properties": {
    "replicaCount": { "type": "integer", "minimum": 1, "maximum": 50 },
    "logLevel":     { "type": "string", "enum": ["debug", "info", "warn", "error"] },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string", "pattern": "^ghcr\\.io/acme/" },
        "tag":        { "type": "string" }
      }
    }
  }
}
⚠ A schema is worth more than a wiki page

Without a schema, a typo in a values key is silent: replicasCount: 6 is simply an unused key, Helm renders the default 2, and you find out during the incident. With a schema and "additionalProperties": false on the objects you own, the typo is a hard failure at render time. If your chart is consumed by more than your own team, ship a schema.

Day-to-day commands

☺ Like you’re 10: Six commands cover almost everything: add, update, install, see-what-would-change, upgrade, undo.

Repositories, search and OCI charts

# classic HTTP repositories (an index.yaml over HTTP)
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm repo update                       # refresh every repo's index — forget this and you get stale versions
$ helm search repo prometheus --versions # what's available locally
$ helm search hub ingress-nginx          # search Artifact Hub instead
$ helm show values prometheus-community/kube-prometheus-stack > /tmp/defaults.yaml
$ helm show chart prometheus-community/kube-prometheus-stack   # metadata only

# OCI registries (stable since Helm 3.8) — charts live beside your images
$ helm registry login ghcr.io -u acme-bot            # credentials, not `repo add`
$ helm package ./checkout                            # → checkout-2.4.0.tgz
$ helm push checkout-2.4.0.tgz oci://ghcr.io/acme/charts
$ helm pull oci://ghcr.io/acme/charts/checkout --version 2.4.0 --untar
# NOTE: with OCI there is no `helm repo add` and no index.yaml. The URL IS the address:
$ helm upgrade --install checkout oci://ghcr.io/acme/charts/checkout --version 2.4.0

Render, lint, install, upgrade

$ helm lint ./checkout -f values-prod.yaml       # schema + chart structure checks, no cluster needed
$ helm dependency update ./checkout             # resolve deps into charts/ and write Chart.lock
# RENDER LOCALLY — the single most useful command: no cluster contact, no release created
$ helm template checkout ./checkout \
    -f values-prod.yaml | less
$ helm template checkout ./checkout -f values-prod.yaml | kubectl apply --dry-run=server -f -

# the idempotent workhorse: install if absent, upgrade if present. Use this, not `helm install`.
# precedence: later -f files beat earlier ones, and --set beats every -f file.
# --atomic rolls the release back automatically if the upgrade fails or times out.
$ helm upgrade --install checkout ./checkout \
    --namespace checkout --create-namespace \
    -f values.yaml -f values-prod.yaml \
    --set image.tag=1.4.4 \
    --atomic --timeout 5m

$ helm upgrade --install checkout ./checkout -f values-prod.yaml --dry-run --debug  # see the plan
$ helm test checkout                            # run the chart's test hooks
◆ Key idea — values precedence, in one line

--set / --set-string / --set-json beats -f files (rightmost file wins) beats a parent chart’s values for its subcharts beats the chart’s own values.yaml. One more rule catches everyone: on helm upgrade, values are not remembered unless you ask. Re-supply every -f and --set each time, or pass --reuse-values (keep the last release’s values, merge new ones in) or --reset-values (throw them away and start from chart defaults). Forgetting this is how a production replica count silently reverts to 2.

Inspect, diff, roll back, remove

$ helm list -A                                   # every release in every namespace
$ helm status checkout -n checkout               # current revision, status, NOTES.txt
$ helm history checkout -n checkout              # every revision, with the reason it exists
$ helm get values checkout -n checkout           # the values YOU supplied
$ helm get values checkout -n checkout -a        # ALL computed values incl. chart defaults
$ helm get manifest checkout -n checkout         # exactly what Helm applied — the ground truth
$ helm get hooks checkout -n checkout            # hook resources, often the cause of a stuck upgrade

# helm-diff is a PLUGIN, not built in — install it on day one
$ helm plugin install https://github.com/databus23/helm-diff
$ helm diff upgrade checkout ./checkout -f values-prod.yaml   # review before you ship

$ helm rollback checkout 6 -n checkout           # re-apply revision 6's manifests AS A NEW REVISION
$ helm rollback checkout -n checkout             # omit the number → previous revision
$ helm uninstall checkout -n checkout            # deletes resources; NOT CRDs, NOT PVCs
$ helm uninstall checkout -n checkout --keep-history   # leaves history so you can roll back later

Gotchas and failure modes

☺ Like you’re 10: Helm is cutting and pasting text, so spaces matter enormously — and a few things it installs, it never cleans up.

It is string surgery, not YAML surgery

The engine has no idea it is producing YAML. It concatenates text, and only the API server ever finds out the result was malformed. Hence the indentation rules that define most of the debugging experience: indent N pads every line by N spaces, while nindent N emits a newline first and then pads — which is what you want after key:. Get it wrong and a whole block silently becomes a sibling of the key it should have nested under. Whitespace chomping ({{- eats preceding whitespace, -}} eats following) keeps control structures from leaving blank-line debris. Unquoted scalars are the other classic: value: {{ .Values.logLevel }} yielding on or 1.10 produces a boolean and a float, and a Deployment that rejects both. Pipe through | quote by reflex. When output looks insane, stop guessing — run helm template, or --debug for the failing render.

CRDs in crds/ are a one-way door

This is the single highest-impact Helm behaviour to memorise. Files in crds/ are installed before anything in templates/, are not templated (no {{ }} at all), and — critically — are never upgraded and never deleted. helm upgrade leaves an existing CRD exactly as it was, so a chart that adds a field to its CRD in v3 will render v3 custom resources against a v2 CRD, and the API server will strip or reject the new fields. helm uninstall leaves the CRDs behind forever. The fix is operational, not clever: upgrade CRDs yourself with kubectl apply -f from the new chart version before running the Helm upgrade, and read the chart’s upgrade notes every time. Symptoms of getting this wrong land in workload triage looking like a mysteriously ignored spec field.

Stuck releases, lost values and history limits

An interrupted helm upgrade leaves the release in pending-upgrade, and every later attempt fails with “another operation (install/upgrade/rollback) is in progress.” There is no lock to break — the state is in the release Secret. helm rollback to the last good revision normally clears it; in the worst case you delete the pending sh.helm.release.v1.* Secret. Related traps: Helm keeps only the last ten revisions by default (--history-max), so your rollback target may have aged out; and because each revision is a Secret holding the full rendered manifest, very large charts can bump into etcd’s ~1 MiB object limit. Meanwhile helm upgrade computes a three-way merge across the old manifest, the new manifest and live state — which is what lets it leave alone a field an HPA owns, but also means a hand-edited resource produces a surprising diff. Delivery-side symptoms belong in delivery triage; the broader method is in the troubleshooting playbook.

Why GitOps setups render charts to plain manifests

Helm’s release model and a reconciler’s desired-state model overlap awkwardly. Argo CD runs helm template internally and owns the resulting objects itself — so no Helm release exists, helm list shows nothing, and helm rollback is unavailable; you roll back by reverting a commit. Flux’s helm-controller instead drives the real Helm SDK from a HelmRelease, so genuine releases and revisions do exist. A third camp commits the rendered output (the “rendered manifests pattern”): CI runs helm template and commits plain YAML to an environment branch, which the reconciler applies. It costs a build step and buys reviewable diffs in pull requests, no template surprises at apply time, and immunity to non-deterministic charts — the last trap here. Charts using randAlphaNum, now or lookup render differently every pass: under Argo CD that is permanent meaningless drift, and under helm template lookup always returns empty because nothing contacts the cluster.

🦫 Benny’s workshop · 20 min

On a throwaway kind or minikube cluster: run helm create demo, then helm template demo ./demo and read every rendered line against the template that produced it. Break it on purpose — change a nindent 8 to indent 8 and render again to watch the YAML collapse. Install with helm upgrade --install, then kubectl get secret -l owner=helm and just look: your release lives there. Now upgrade with --set replicaCount=4, upgrade again without that flag, and watch the count fall back to the default — the values-are-not-remembered lesson in ninety seconds. Finish with helm history demo and helm rollback demo 1.

Alternatives and when to choose it

☺ Like you’re 10: Other tools also turn settings into YAML — they just make different trades between power and safety.

The real question is rarely “Helm or not.” You will consume Helm charts regardless, because that is how third-party software ships. The question is what you use to configure your own workloads.

The comparison that decides it

OptionModelBest whenCosts you
HelmText-templated, versioned, distributable package with a release historyYou must distribute a chart to other teams or consume third-party software; you want one artefact, many environments, and rollbackString templating blind to YAML structure; values sprawl in deep charts; a second state store (release Secrets) beside Kubernetes itself
KustomizeTemplate-free structural overlays patching a shared baseConfig you own and change often; you want readable, valid YAML at every step; it is built into kubectlNo packaging, no versioned artefact, no distribution story, no rollback of its own; deep overlay chains get hard to trace
Helm + Kustomize togetherRender the chart, then post-patch the outputYou must adjust a third-party chart that lacks the knob you needTwo mental models in one pipeline; patches break silently when the upstream chart changes
Plain YAMLWrite it out, once per environmentFewer than about five objects and one environmentDuplication and drift arrive the day you add a second environment
CUE / jsonnet / Pulumi / cdk8sTyped or programmatic generation in a real languageStrong typing and abstraction matter more than ecosystem reachA new language for every reviewer; almost no third-party ecosystem ships in these formats
Crossplane compositionsA Kubernetes API that composes resources server-sideYou want a self-service API, not a package developers must renderDifferent problem, different layer — it does not distribute third-party software

A practical rule

Use Helm when the artefact must travel — across teams, clusters or organisations — and when you want a version number to be the unit of change. Use Kustomize when the config is yours, lives next to the code, and benefits from being plain readable YAML. Most mature platforms do exactly that: upstream Helm charts for infrastructure, one internal Helm or library chart as the paved road, Kustomize overlays for per-environment app config. See The Tool Landscape for how this sits among the named projects, and Configuration & Templating for the full head-to-head.

🎬 At the Platform Guild
🦊

Foxy: My upgrade went through fine but prod dropped from six replicas to two. Helm ate my config.

🦫

Benny: Helm didn’t eat anything — it was never told. Values aren’t remembered between upgrades. You passed --set replicaCount=6 last week and nothing this week, so it rendered the chart default.

🐢

Timmy: And don’t reach for --reuse-values — put it in values-prod.yaml, commit it, and let the reconciler upgrade from Git. Then “what is prod set to?” has a permanent answer instead of living in someone’s shell history.

👺

Gizmo: Just kubectl edit the Deployment back to six. Two seconds. Nobody will ever know. 🤑

🦫

Benny: And the next helm upgrade three-way-merges it away and we’re back here on Thursday. Fix the values file, Gizmo.

🦆

Dot: Genuinely all I want is helm diff upgrade in the pull request. Show me the eleven lines that change and I’ll approve it in a minute.

Exam relevance and going further

☺ Like you’re 10: Helm isn’t the star of the exam, but it’s how everything else gets installed — and its website is locked on exam day.

Helm does not appear on the official CNPE tool list, so no task is likely to be titled “Helm.” It shows up as plumbing: the way a platform component was installed, a chart you must render and inspect, a values file with a mistake in it, a released component you must identify. Read it as background competence rather than a headline skill, and spend your revision minutes accordingly — the named tools in the tool landscape earn more marks per minute.

The documentation allowlist — read this twice

⚠ helm.sh is not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, any task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. helm.sh/docs and Artifact Hub are not on that list. What you do still have is the binary itself: helm --help, helm install --help and helm upgrade --help are on the machine and are surprisingly complete. Practise reaching for built-in help rather than a browser tab, and drill the manifests you cannot look up on Know Cold.

⚖ CNPA vs CNPE — That allowlist is a CNPE-only mechanic: CNPA has no allowlist at all, because it is a fully closed-book multiple-choice exam — zero external resources, zero lookups of any kind, on anything. That’s stricter than CNPE, not looser. Even so, the chart/release/revision model and the values-precedence rules on this page are worth knowing cold, since this concept-level knowledge still matters for CNPA’s closed-book recall.

What to be able to do without notes

Write a minimal Chart.yaml from an empty file (apiVersion: v2, name, version, and the version-vs-appVersion distinction). Explain chart / release / revision in a sentence each. State the values precedence order, and that upgrades do not remember values. Render with helm template and say why that is the safe first move. Name where release state lives (a Secret in the release namespace, one per revision). Say what crds/ does and — more importantly — what it refuses to do on upgrade. And know the CLI spine cold: helm repo add/update, search repo, show values, template, lint, upgrade --install, list -A, history, get values -a, get manifest, rollback, uninstall — all collected in the command reference, with any unfamiliar term in the glossary.

Official resources for after the exam

When you are not sitting the exam, the canonical sources are the Helm documentation at helm.sh/docs — the Chart Template Guide and Chart Best Practices pages repay reading end to end — the chart index at artifacthub.io, the Sprig function reference at masterminds.github.io/sprig, the source at github.com/helm/helm, and the CNCF project page at cncf.io/projects/helm. Pair this page with Configuration & Templating for the comparison against Kustomize, GitOps Workflows for how charts get delivered, and Platform Best Practices for the chart conventions worth enforcing.

🐢 Timmy’s checkpoint

1. Distinguish a chart, a release and a revision in one sentence each. 2. Where does Helm 3 store release state, and what did it replace? 3. Put these in precedence order: chart values.yaml, --set, -f values-prod.yaml. 4. You run helm upgrade without repeating last week’s --set flags. What happens, and what two flags relate to it? 5. Your chart ships a CRD in crds/ and you add a field to it in the next version. What does helm upgrade do about it? 6. Why do many GitOps setups run helm template and commit the output? 7. During the exam, where can you look up helm upgrade’s flags?

Check your answers
  1. A chart is the versioned package (inert, distributable). A release is one named, installed instance of a chart in a namespace. A revision is a numbered snapshot of that release — install is 1, each successful upgrade adds one, and a rollback re-applies an old one as a new revision.
  2. In a Kubernetes Secret of type helm.sh/release.v1 in the release’s namespace, named sh.helm.release.v1.<release>.v<revision> — one per revision. It replaced Helm 2’s in-cluster, over-privileged Tiller server, which Helm 3 removed entirely.
  3. Lowest to highest: chart values.yaml-f values-prod.yaml (rightmost file wins among several) → --set. Parent-chart values also override their subcharts’ values.
  4. Those values are forgotten — Helm re-renders from chart defaults plus whatever you passed this time, so a replica count or image tag silently reverts. The flags are --reuse-values (merge into the previous release’s values) and --reset-values (discard them and use chart defaults). The better fix is to keep values in a committed file.
  5. Nothing. Resources in crds/ are installed only if absent and are never upgraded or deleted. You must apply the new CRD yourself (kubectl apply -f crds/) before the Helm upgrade, or your new fields will be ignored.
  6. To get reviewable plain-YAML diffs in pull requests, to catch template errors at build time rather than apply time, and to neutralise non-deterministic charts (randAlphaNum, now, lookup) that would otherwise cause permanent drift. It also avoids keeping a second state store (Helm releases) beside the reconciler’s desired state.
  7. Not on the web — helm.sh/docs is not on the allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). Use the binary’s own help: helm upgrade --help.