Helm vs Kustomize: Templates and Releases or Template-Free Overlays
7 min readBytePatterns
Helm vs Kustomize: charts render YAML from values and keep releases you can roll back; Kustomize patches plain YAML bases with overlays, inside kubectl.
Helm vs Kustomize in one line: Helm is a package manager that renders Kubernetes YAML from templates and a values file, installs the result as a named release and keeps a revision history you can roll back; Kustomize has no templates at all, it takes plain YAML as a base and layers overlays and patches on top, and it ships inside kubectl as apply -k. Helm answers "how do I install and upgrade someone else's software with my settings?". Kustomize answers "how do I keep dev, staging and prod variants of my own manifests without copying them?". Facts come from the Helm and Kubernetes documentation listed at the end, as of October 2026.
| Question | Helm | Kustomize |
|---|---|---|
| How variants are made | Go templates filled from values.yaml, -f, --set | overlays patching a base: strategic merge or JSON 6902 |
| State in the cluster | a release with numbered revisions; helm rollback | none beyond the objects; history lives in git |
| Distribution | charts in repositories or OCI registries | directories, local or in a remote repo |
The problem it solves
The checkout API from the design lesson needs the same Deployment, Service, PodDisruptionBudget and HPA in three environments, with different replica counts, image tags, resource limits and hostnames. Copying the YAML three times means every fix is made three times, and the copies drift. Meanwhile the cluster also needs software you did not write, such as an ingress controller or a metrics stack, which arrives as dozens of objects that must be installed, upgraded and removed together. Helm and Kustomize are the two standard answers, and they solve different halves of the problem.
The intuition
Helm: text with holes, plus a release record. A chart is a directory of templates, a values.yaml of defaults and a Chart.yaml with metadata. helm install renders the templates with your values and sends the objects to the cluster as a release. Values come from the chart's defaults, then each -f file (the rightmost wins), then --set, which wins over files. Every install, upgrade and rollback adds a revision, starting at 1; by default the release record is stored as Secrets in the release's namespace. helm history lists the revisions and helm rollback <release> <revision> re-applies an old one. Charts can depend on other charts and run hooks before or after an install.
The catch is that a template is text. You can only change what the chart author exposed as a value: if the chart has no hole for a sidecar or a memory limit, you fork it or wait for upstream. And because the engine pastes strings, indentation and quoting are your problem, which is why chart templates are full of toYaml, nindent and quote.
Kustomize: real YAML, patched as data. A base is a directory with a kustomization.yaml that lists plain, valid manifests. An overlay is another directory that points at the base and adds changes: a namePrefix or namespace, labels, a new image tag, generated ConfigMaps and Secrets, and patches, either strategic merge patches that look like partial objects or JSON 6902 patches for precise edits. The base knows nothing about its overlays, so one base serves many. Because it operates on parsed objects, any field can be patched, and because kubectl has supported it since 1.14, kubectl apply -k overlays/prod is all you need. There is no release object: what is deployed is whatever your last apply sent, and history is your git log.
Combining them is normal. Render a third-party chart with helm template, commit or pipe the output, and let a Kustomize overlay patch the fields the chart did not expose. Kustomize also has a limited helmCharts generator, enabled with kustomize build --enable-helm, and Helm can run a post-renderer over its output, for which Kustomize is the usual choice.
When to use which
- Installing third-party software: Helm. Upstream projects publish charts, and the release gives you upgrade, history and rollback as one unit.
- Your own services across environments: Kustomize. Plain YAML that
kubectlvalidates, with small per-environment overlays, and no template language to learn. - Distributing your software to other teams or customers: Helm, because a chart with documented values is an installable product.
- A chart that does not expose the field you need:
helm templatethe chart, then patch the output with a Kustomize overlay.
Watch it run
The lesson's animation designs a production Deployment piece by piece: an Ingress terminating TLS, a Service, three replicas spread across zones, a zone failure the other two survive, probes, requests and limits, an HPA adding replicas, config from a ConfigMap and a Secret, a managed database, a rolling update with maxUnavailable: 0, and a PodDisruptionBudget guarding a node drain. Every one of those boxes is a manifest. Helm and Kustomize do not change what the boxes are; they decide how one set of those manifests becomes a dev, a staging and a prod copy.
Design a Deployment on Kubernetes
Step 1 of 13
The brief: a checkout API that stays up, scales with traffic, and updates safely. Start at the front door.
The same interactive animation as the lesson — step through it with the controls.
The code
The same environment difference expressed both ways: Helm by values, Kustomize by an overlay that patches a base:
# illustrative: Helm. Only fields the chart templated can change
# values-prod.yaml -> helm upgrade --install checkout ./chart -f values-prod.yaml
replicaCount: 3
image: { tag: "1.5.0" }
# templates/deployment.yaml has: replicas: {{ .Values.replicaCount }}
---
# illustrative: Kustomize. overlays/prod/kustomization.yaml -> kubectl apply -k overlays/prod
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base] # plain YAML that also applies on its own
namespace: checkout-prod
images: [{ name: registry.example.com/checkout, newTag: "1.5.0" }]
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata: { name: checkout }
spec:
replicas: 3
template:
spec:
containers:
- name: checkout
resources: { limits: { memory: 512Mi } } # no hole needed
A toy model of templating, not Helm. A template renders whatever holes it has, a field without a hole cannot be set from values, and a value is pasted as text, so a value with a newline quietly adds a second replicas key to the YAML:
from string import Template
# a chart template is text with holes ({{ .Values.x }} in Helm, $x here)
DEPLOY = Template("""\
spec:
replicas: $replicas
template:
spec:
containers:
- name: api
image: registry.example.com/api:$tag""")
values = {"replicas": 3, "tag": "1.5.0"} # values.yaml
print(DEPLOY.substitute(values).splitlines()[1]) # replicas: 3
# prod needs a memory limit, but the author left no hole for it
print("resources" in DEPLOY.template) # False
# and a value is pasted as text: the engine has no idea it is YAML
print(DEPLOY.substitute(values, tag="1.5.0\n replicas: 50").splitlines()[-2:])
# [' image: registry.example.com/api:1.5.0', ' replicas: 50']
A toy strategic merge, the idea behind Kustomize patches: maps merge by key, lists of containers merge by name, scalars take the last layer. Base, prod and an EU overlay stack into one container with all three changes plus a new sidecar. Order matters, since the last layer wins, but grouping does not, which a seeded check confirms on 2,000 random trees, and that is why overlays of overlays behave:
import copy, random
def merge(base, patch):
"""Toy strategic merge: maps merge by key, lists of named items merge by name."""
out = copy.deepcopy(base)
for k, v in patch.items():
old = out.get(k)
if isinstance(v, dict) and isinstance(old, dict):
out[k] = merge(old, v)
elif isinstance(v, list) and isinstance(old, list): # e.g. containers, by name
items = {i["name"]: i for i in old}
for i in v:
items[i["name"]] = merge(items.get(i["name"], {}), i)
out[k] = list(items.values())
else:
out[k] = copy.deepcopy(v) # scalars: last layer wins
return out
BASE = {"replicas": 1, "containers": [{"name": "api", "image": "api:1.5.0"}]}
PROD = {"replicas": 3, "containers": [{"name": "api", "resources": {"memory": "512Mi"}}]}
EU = {"containers": [{"name": "api", "env": {"REGION": "eu"}},
{"name": "proxy", "image": "proxy:1.9"}]}
for c in merge(merge(BASE, PROD), EU)["containers"]:
print(c)
# {'name': 'api', 'image': 'api:1.5.0', 'resources': {'memory': '512Mi'}, 'env': {'REGION': 'eu'}}
# {'name': 'proxy', 'image': 'proxy:1.9'}
print(merge(BASE, PROD)["replicas"], merge(PROD, BASE)["replicas"]) # 3 1
def tree(rng, depth=2): # a key keeps one type everywhere: a, b leaves; c, d maps
return {k: (tree(rng, depth - 1) if k in "cd" and depth else rng.randint(0, 9))
for k in rng.sample("abcd" if depth else "ab", 2)}
rng = random.Random(1)
trials = [[tree(rng) for _ in range(3)] for _ in range(2000)]
print(all(merge(merge(a, b), c) == merge(a, merge(b, c)) for a, b, c in trials)) # True
The check needs one condition: a key keeps the same type in every layer. A patch that turns a map into a scalar breaks grouping, which is one reason Kustomize offers JSON 6902 patches for edits a merge cannot express.
The complexity
- Learning cost: Helm adds a template language, chart structure, hooks and the release lifecycle. Kustomize adds one file format on top of the YAML you already write.
- Upgrades of third-party software: Helm wins: one command moves every object to the new chart version and records a revision. With Kustomize you re-vendor the upstream YAML and re-check your patches.
- Review: a Kustomize diff is a YAML diff. A Helm change is a values diff whose effect is only visible after rendering, so render it with
helm templatein review and diff the output.
Where it goes wrong
- Editing a Helm release by hand.
kubectl editon a chart's object is overwritten or fights the nexthelm upgrade. Change values instead. - Values that are not exposed. Teams fork a chart for one missing field and then own the fork forever. Patch the rendered output instead.
- Secrets in values files. Values are stored in the release record and often committed. Keep credentials in a secret store and reference them.
- Deep overlay chains. Overlays of overlays of overlays are hard to read; keep one base and one overlay per environment, and use components for shared optional pieces.
- Assuming Kustomize tracks releases. It does not delete objects you remove from the base, and it has no rollback. Use
kubectl apply --prunecarefully, or a GitOps controller that prunes.
When it shows up in interviews
As a follow-up in a deployment design: "how do you manage this across environments?" or "how would you roll back?". A strong answer separates the two jobs, packaging and release management versus per-environment configuration, and mentions that they combine. How the rendered files then reach the cluster is in kubectl apply vs create vs replace, and the objects being templated are the ones walked through in the Design a Deployment on Kubernetes lesson.
How to say it in an interview
"Helm is a package manager: a chart is templates plus default values, helm install renders it with my values and records a release, and every upgrade is a revision I can roll back with helm rollback. Kustomize is template-free: a base of plain YAML and per-environment overlays that patch it, built into kubectl apply -k, with git as the history. I use Helm for third-party software and for anything I distribute, Kustomize for my own services' environments, and when a chart lacks a field I render it with helm template and patch the output with an overlay."
Frequently asked questions
Can I use Helm and Kustomize together?
Yes, and it is common. Render the chart with helm template and list the output as a resource in a kustomization, or use Kustomize's helmCharts generator with kustomize build --enable-helm, or have Helm run Kustomize as a post-renderer. Either way, Kustomize patches the fields the chart did not expose.
Does Kustomize support rollback like Helm?
No. Kustomize only produces YAML; it keeps no record in the cluster. Rolling back means applying the previous commit of your overlays, or using kubectl rollout undo for a Deployment. Helm stores each release revision, by default as Secrets in the release namespace, so helm rollback can restore any recorded revision.
Is Kustomize built into kubectl?
Yes. Since Kubernetes 1.14, kubectl kustomize <dir> prints the rendered objects and kubectl apply -k <dir> applies them. The standalone kustomize binary also exists, and the helmCharts generator is documented with kustomize build --enable-helm.
Sources
- Using Helm — Helm Documentation
- Advanced Helm Techniques: post rendering and storage backends — Helm Documentation
- Charts — Helm Documentation
- Declarative Management of Kubernetes Objects Using Kustomize — Kubernetes Documentation
- Kustomization patches — Kubernetes SIG CLI
- Kustomization helmCharts — Kubernetes SIG CLI