Helm: charts and values
Helm is the Kubernetes package manager. A chart templates manifests with Go-template placeholders; values override them per environment; and install/upgrade/rollback track a named, versioned release that raw kubectl apply never gives you.
You have one service to run in three environments. The dev deployment wants 1 replica and the :dev image; staging wants 2 and :rc; prod wants 6, a different resource budget, and the pinned :1.8.3 tag. Everything else in the manifest is identical. So the team copies deployment.yaml into three folders, and now a change to the readiness probe has to be made in three places by hand — and someone always misses one. Worse, when the prod rollout goes bad at 2am, kubectl apply has no “undo.” There is no record of what the previous good manifest was, no single command to put it back. You are diffing YAML against your shell history.
The problem: raw YAML duplicates, and kubectl apply has no undo
Plain Kubernetes YAML has two structural gaps once you run the same app in more than one place. First, duplication: environments differ by a handful of values — replica count, image tag, resource limits, a hostname — but YAML has no parameters, so you copy the whole manifest set per environment and keep them in sync by hand. Drift is inevitable. Second, no release unit: kubectl apply -f pushes the current files to the cluster and forgets. It does not record “this is version 4 of my-api,” it cannot bundle a Deployment + Service + ConfigMap + Ingress as one atomic thing, and it has no concept of rolling back to the previous version. If you want to undo, you have to reconstruct the old YAML yourself.
Helm — the de-facto package manager for Kubernetes — closes both gaps. It introduces a packaging unit (the chart), a parameterisation mechanism (values), and a stateful, versioned deploy unit (the release).
A chart is a package: templates + default values + metadata
A chart is a directory (or a packaged .tgz) with a fixed layout. The three parts that matter day one:
my-api/
Chart.yaml # metadata: name, version, appVersion, dependencies
values.yaml # default values (the public API of the chart)
templates/
deployment.yaml # K8s manifests, but with Go-template placeholders
service.yaml
_helpers.tpl # reusable template snippetstemplates/ holds ordinary Kubernetes manifests with Go-template placeholders punched into the spots that vary. Helm renders these templates against a values tree to produce final, plain YAML, then sends that to the cluster.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
limits:
memory: {{ .Values.resources.limits.memory }}values.yaml ships the defaults — and doubles as the chart’s documented public interface:
# values.yaml (defaults)
replicaCount: 1
image:
repository: registry.example.com/my-api
tag: dev
resources:
limits:
memory: 256MiValues override the templates per environment
The same chart renders different manifests because you feed it different values. You keep one small file per environment and pass it at install/upgrade time with -f, or override single keys with --set. Precedence is layered: the chart’s values.yaml is the base, each -f file overrides it left-to-right, and --set flags win over everything.
# values-prod.yaml — only what differs from the defaults
replicaCount: 6
image:
tag: "1.8.3"
resources:
limits:
memory: 1Gi# one chart, three environments — values are the only difference
helm upgrade --install my-api ./my-api -f values-prod.yaml
helm upgrade --install my-api ./my-api -f values-staging.yaml --set image.tag=rc-42This is the load-bearing idea: values are the environment boundary. The template encodes the shape of your deployment once; the values carry the per-environment facts. There is exactly one readiness-probe definition to maintain, and dev/staging/prod can no longer drift apart in the parts that should be identical.
▸Why this works
Keep environment-specific values files thin — only the keys that genuinely differ from values.yaml. If you copy the whole defaults file per environment, you have just reinvented the duplication problem Helm exists to solve, only now it is hidden one level down. The discipline is: the chart’s values.yaml is the single source of shape and sane defaults; each per-environment values file is a short diff against it.
Releases: install, upgrade, rollback as one versioned unit
When you helm install (or helm upgrade --install) a chart, Helm creates a named release and records a numbered revision of exactly what it deployed, stored as a Secret in the cluster. Every helm upgrade bumps the revision. Because Helm remembers each revision, helm rollback my-api 4 is a single command that re-applies a known-good past state — the “undo” kubectl apply never had. helm uninstall removes the whole release’s resources together. This turns a deploy from “a pile of YAML I shoved at the API server” into an atomic, versioned, revertible transaction over a named bundle.
helm upgrade --install my-api ./my-api -f values-prod.yaml # -> revision 5
helm history my-api # see all revisions
helm rollback my-api 4 # atomic undo to a known-good statekubectl apply vs Helm, at a glance
| Capability | Raw kubectl apply -f | Helm |
|---|---|---|
| Templating / parameters | None — static YAML, copy per env | Go-template placeholders filled from values |
| Multi-environment | Duplicate manifests, manual sync, drift | One chart, thin values-<env>.yaml per env |
| Release / rollback | No release unit, no built-in undo | Named release, numbered revisions, helm rollback |
| Packaging / sharing | Ad-hoc files in a repo | .tgz chart, versioned, pushed to a chart repo |
Sharing and reuse: repositories and subcharts
Why write a Postgres StatefulSet from scratch when production-grade charts already exist, are battle-tested, and carry sensible defaults? This is where the chart ecosystem pays off.
Charts are distributable artifacts. Public chart repositories — Bitnami being the best-known — publish production-grade charts for off-the-shelf infrastructure (Postgres, Redis, nginx), so you do not hand-write a stateful-set for a database. You can also compose: a chart can declare dependencies (subcharts) in Chart.yaml, and a parent’s values can override a subchart’s values, so an umbrella chart wires your app together with its dependencies as one release.
▸lesson.inset.warn
Helm’s --reuse-values flag carries forward the previous release’s values during an upgrade, which sounds convenient but silently masks changes you made to values.yaml and to defaults in the new chart version. Prefer passing your -f files explicitly on every upgrade so the rendered output is a pure function of the chart + the values you can see, not of accumulated hidden state from prior revisions.
Senior notes: never upgrade blind
Two practices separate a safe Helm workflow from a dangerous one. Preview before you apply. helm upgrade --dry-run renders the manifests without touching the cluster; the widely-used helm diff upgrade plugin shows the exact change against the live release, so you see “this will delete a PVC” before it happens, not after. Version everything. A chart carries two versions in Chart.yaml: version (the chart’s own version, bump on any chart change) and appVersion (the app it ships). Treat charts like code — pin the chart version your environments use, and bump it deliberately. And mind the upgrade-vs-recreate gotcha: editing an immutable field (a Job’s spec, a Service’s clusterIP, certain volume fields) cannot be patched in place; the upgrade fails or, depending on the resource, forces a recreate that can mean downtime. Read the diff, know which fields are immutable, and plan the rollout — helm rollback is your safety net, not your strategy.
You run one service across dev, staging, and prod, differing only in replica count, image tag, and memory limit. You need painless multi-env config and a real rollback path. Pick the approach.
A prod helm upgrade ships a bad config and the service starts erroring. What is the fastest correct recovery, and why is it possible?
In a chart, where does the per-environment difference between dev and prod (replicas, image tag) belong?
- 01Explain how one Helm chart produces different manifests for dev, staging, and prod, naming the three parts of a chart and how values precedence works.
- 02Why can Helm roll back a bad deploy when kubectl apply cannot, and what should you run before an upgrade to avoid needing the rollback?
Raw Kubernetes YAML has two gaps once an app runs in more than one place: it duplicates everything that is identical across environments, and kubectl apply has no release or rollback unit. Now when you inherit a cluster where someone has been doing kubectl apply -f across three environments with copied manifests, you know the path out: one chart, one thin values file per environment, and a release ledger that makes helm rollback your 2am lifeline. Helm — the Kubernetes package manager — closes both. A chart is a package: Chart.yaml metadata, templates/ holding manifests with Go-template placeholders, and values.yaml carrying the defaults and the chart’s public interface. The same chart renders different manifests because values override the templates per environment — a thin per-environment values file passed with -f, or single keys via --set, layered base-to-override-to-flag. Values are the environment boundary: shape lives in the template once, per-env facts live in small values files, so dev/staging/prod cannot drift. Each deploy is a named, numbered release, so helm upgrade bumps a revision Helm stores and helm rollback re-applies a known-good one atomically — the undo apply never had. Reuse off-the-shelf infra from public chart repos like Bitnami and compose with subchart dependencies. And never upgrade blind: helm —dry-run and helm diff preview the change, version your charts via Chart.yaml’s version and appVersion, and watch the upgrade-vs-recreate gotcha where immutable fields force a disruptive recreate.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.