Packaging and Deployment: Helm and Kustomize
· tech
📑 Contents
- Two philosophies: fill in a template vs stack patches
- Helm: turning a K8s app into an installable, versioned, rollback-able package
- Kustomize: plain YAML, base + overlay per environment
- Which one?
- Reflections
- ”Template” and “overlay” are two kinds of mental load; pick the one you can bear
- Packaging is the last mile of “externalised config”; only both layers together give one build that runs everywhere
- Don’t Helm-ify simple things to look professional
Every previous post taught you to write YAML, but practice has an unavoidable pain: the same app goes to Development, Staging and Production, and ninety percent of the YAML for the three environments is identical, with only a few differences — replica count, image tag, resource sizes, the external hostname. If you copy-paste three sets and maintain each separately, changing one shared field means syncing three times, and sooner or later something slips. This post covers the two mainstream tools for this: Helm and Kustomize, and the two very different worldviews behind them.
Two philosophies: fill in a template vs stack patches
Helm and Kustomize both solve “one source + per-environment differences”, but their approaches point in opposite directions. One treats YAML as a template with variables to fill in; the other treats YAML as data and stacks patches on it:
Helm: turning a K8s app into an installable, versioned, rollback-able package
Helm is often called “the package manager for K8s”, and the analogy to apt / npm fits well. Its core is the chart — a folder holding Chart.yaml (package info), values.yaml (defaults) and templates/ (YAML templates with variables). Deploying is three actions:
helm install web ./mychart -f values-prod.yaml # install a release, applying prod values
helm upgrade web ./mychart -f values-prod.yaml # new version: upgrade the same release
helm rollback web 3 # one-step rollback to revision 3
What it gives you beyond plain YAML is what a package manager should have: every install/upgrade is a numbered release, and when it breaks helm rollback takes you back in seconds; you can also declare dependencies (my app needs a Redis). The best part is the ready-made ecosystem — want Redis, Prometheus, cert-manager in your cluster? One helm install line, instead of hand-assembling hundreds of lines of YAML. The price is that Go template language: {{ }}, indentation, conditionals and loops pile up, and a complex chart can be hard to read and debug (rendering it first with helm template to see the result is the life-saving move).
Kustomize: plain YAML, base + overlay per environment
Kustomize takes the other road: no templates, no variables, everything is valid YAML. You write a base/ (complete YAML that kubectl apply could take as is), then for each environment an overlays/<env>/ holding only the small patch this environment changes:
# overlays/prod/kustomization.yaml
resources: [../../base] # stacked on base
replicas:
- name: web
count: 10 # Production gets 10 copies, everything else inherited from base
images:
- name: app
newTag: "1.4" # swap the image tag
It’s built into kubectl (kubectl apply -k overlays/prod), no extra tool to install, and comes with a set of handy transformers: namePrefix, commonLabels, images (change tags), replicas, and configMapGenerator — the last appends a content hash to the ConfigMap’s name, so when the content changes the name changes, so the Pod template changes, and a rolling update triggers automatically. That neatly disposes of the pit from post three: editing a ConfigMap doesn’t restart Pods by itself — with Kustomize’s generator, you get that right for free.
Which one?
It isn’t either/or, but the direction is clear:
| Situation | Leans Helm | Leans Kustomize |
|---|---|---|
| Installing third-party ready-made apps (Redis, Prometheus) | ✓ one-line install, ecosystem | |
| Managing your own app manifests | ✓ plain YAML, easy to read and review | |
| Need versioning / one-step rollback / dependency management | ✓ the release mechanism | |
| Heavy parameterisation, shipping to others | ✓ values are the parameter panel | |
| Don’t want another tool, don’t want to learn a template language | ✓ built into kubectl |
In practice many teams use both: Helm for third-party packages, Kustomize to layer environments for their own app; some even run Helm’s output through Kustomize via a post-renderer. First ask whether you’re solving “installing someone else’s package” or “splitting my own YAML by environment” — that one question basically decides which to pick up first.
Reflections
”Template” and “overlay” are two kinds of mental load; pick the one you can bear
The difference between Helm and Kustomize is tools on the surface and, underneath, whether you’re willing to treat YAML as code or as data. Helm turns YAML into a template with variables and logic — powerful, but you carry an extra cognitive tax of a template language, and when things go wrong you have to “render” in your head to know what it actually looks like. Kustomize insists everything stays valid YAML; you can kubectl apply -k at any time and see the final result, the mental model is clean, and the price is that highly dynamic parameterisation ties your hands. My own preference is Kustomize whenever Kustomize will do — the readability and reviewability of plain YAML is badly undervalued as a long-term asset on a team; only when “the parameters get so numerous it feels like programming” do I concede that Helm’s templates are the right tool.
Packaging is the last mile of “externalised config”; only both layers together give one build that runs everywhere
This post is really an extension of the ConfigMap/Secret post. That one covered “digging the config out of the image”, so the same image can run in every environment; this one covers “pulling the environment differences out of the YAML”, so the same deployment source can produce each environment’s manifests. The two layers are the first and second half of one ideal: build once, externalise all config and deployment differences, one artefact runs from Dev to Production. Drop either layer and somewhere you fall back to copy-paste. Once that clicked, my view of CI/CD changed too — a good pipeline isn’t “build once per environment”; it’s “build once, and place it into different environments through config and overlays”.
Don’t Helm-ify simple things to look professional
The customary cold water to finish. I’ve seen small projects with two or three environments start out with a homemade Helm chart full of {{ }}, and then wrestle the template language every time they change a replica count — wrapping something a three-line diff would solve into a package that has to be maintained. Packaging tools exist to reduce cross-environment repetition and risk, not to show off the tech stack. My priority is always confirm the pain first, then bring in the heavy weapons: few environments, small differences, raw YAML or a thin layer of Kustomize is enough; only with genuinely many environments, many teams, and packages to publish for shared use does Helm’s machinery earn its keep. The weight of the tool has to match the weight of the problem — after walking the whole K8s series, that’s the one line I most want to leave behind.