Helm vs Kustomize
“Helm or Kustomize” is the most-asked configuration-management question in the Kubernetes ecosystem, and it is usually asked as if the two were mutually exclusive — which is the wrong frame. Helm and Kustomize sit on opposite sides of a single design axis: Helm parameterizes manifests by running them through a Go-templating language driven by values, and additionally provides packaging, distribution, versioning, and a release lifecycle; Kustomize transforms plain manifests by structurally merging patches and overlays into them, introducing no templating language at all and providing no packaging story. Each tool’s greatest strength is the direct cause of its greatest weakness. This note lays out the comparison honestly — the templating-vs-overlay trade-off, the packaging-vs-no-packaging trade-off, the readability and safety arguments on each side — and then states the conclusion the question’s framing obscures: the two are not mutually exclusive, and the most common mature production pattern uses both. The tools themselves are documented in Helm (and Helm Chart Anatomy) and Kustomize; this note is the decision aid.
Mental Model
flowchart LR subgraph HELM["Helm — parameterize + package"] HT["Go templates + values<br/>{{ .Values.replicas }}"] HP["packaging · versioning ·<br/>distribution · release lifecycle ·<br/>rollback · chart ecosystem"] HW["weakness: template soup;<br/>'string substitution → YAML';<br/>output not valid until rendered"] end subgraph KUST["Kustomize — overlay + merge"] KT["structured patches + overlays<br/>(no templating language)"] KP["kubectl-native · output always<br/>structurally valid YAML ·<br/>git-friendly diffs"] KW["weakness: no packaging/<br/>distribution/versioning;<br/>no free parameters, only patches"] end AXIS["Design axis:<br/>TEMPLATE/SUBSTITUTE ←————→ STRUCTURED MERGE"] HELM -.-> AXIS KUST -.-> AXIS BOTH["Production reality:<br/>NOT exclusive — use both:<br/>helm template | kustomize,<br/>or Kustomize helmCharts inflator,<br/>or Helm for 3rd-party + Kustomize for own"] HELM --> BOTH KUST --> BOTH
What this diagram shows. The two tools occupy the ends of one axis — “substitute values into templated text” versus “structurally merge patches into plain objects.” Each box lists the tool’s distinctive strengths and the weakness those strengths cause: Helm’s template power is the template-soup readability problem; Kustomize’s no-language safety is the no-parameters limitation. The bottom box is the load-bearing insight: experienced teams stop treating this as an either/or. The three “use both” compositions shown there are all standard in production. The takeaway to extract: choose by what each tool is good at for a given artifact, not by tribal allegiance.
Mechanical Walk-through
Helm — the templating-and-packaging side
Helm’s model (full detail in Helm): a chart is a directory of Go-templated manifests plus a values.yaml; the user overrides values with -f and --set; helm install renders templates and records a numbered release.
What Helm uniquely provides:
- Parameterization — arbitrary value substitution. A hostname, a replica count, a feature flag, a whole resource block can be a
{{ .Values.x }}. High-cardinality variation (a value that differs per tenant across dozens of tenants) is one--setaway. - Packaging and distribution — a chart is a single versioned
.tgzpublishable to a chart repository or OCI registry. This is the entire reason the public chart ecosystem (Artifact Hub) exists; installing third-party software — Prometheus, cert-manager, an ingress controller — ishelm install. - Release lifecycle — numbered revisions,
helm history, one-commandhelm rollback,helm uninstall. Helm tracks what it deployed.
What Helm is criticized for:
- Template-soup readability. Go templates are text templating — they do not understand YAML structure. A misplaced
nindent, an unquoted value, arangewhitespace bug yields a manifest that fails to parse, and the error surfaces only at render time. The standing critique is that Helm is “string substitution that happens to produce YAML.” - Debuggability. You cannot read a chart’s
templates/deployment.yamland know what gets deployed without rendering it against a specific values set (helm template).
Kustomize — the overlay side
Kustomize’s model (full detail in Kustomize): plain, valid Kubernetes manifests in a base/; per-environment overlays/ that patch the base via kustomization.yaml transformations; kubectl apply -k (Kustomize is built into kubectl).
What Kustomize uniquely provides:
- Structural safety. Inputs and outputs are always real Kubernetes objects. The merge engine operates on parsed object trees, so a structurally-invalid output is not expressible. There is no render-time YAML-parse class of bug.
- kubectl-native. Nothing to install, nothing extra to vet or keep patched. The output of a Kustomize change is a plain-YAML diff a reviewer can read directly.
- Git-friendly. Bases and overlays map cleanly onto a Git repo — the canonical GitOps layout.
What Kustomize is criticized for:
- No packaging, distribution, or versioning. There is no Kustomize equivalent of a chart
.tgz, no registry, nokustomize install some/registry/thing. Kustomize customizes manifests you already have; it does not deliver software you don’t. - No parameters, only patches. Kustomize deliberately has no input-variable concept. Configuration that varies value-by-value across many dimensions forces one overlay per combination — exactly where Helm’s
--setshines.
The honest decision table
| Concern | Helm | Kustomize |
|---|---|---|
| Customization mechanism | Go templates + values substitution | Structured strategic-merge / JSON6902 patches |
| Templating language to learn | Yes (Go templates + Sprig) | No |
| Output validity | Only valid after rendering | Always structurally valid YAML |
| Packaging artifact | Versioned chart .tgz | None |
| Distribution | Chart repos + OCI registries | None (it’s just files in your repo) |
| Versioning | Chart version + appVersion (semver) | Whatever your Git history gives you |
| Release lifecycle / rollback | Yes — numbered revisions, helm rollback | None — Kustomize doesn’t track deployments |
| Install third-party software | The standard way (Artifact Hub) | Not its job |
| Free-form parameters | Yes (--set, -f) | No — patches only |
| Tooling footprint | Separate helm binary | Built into kubectl |
| Project governance | CNCF graduated project (since 2020) | Kubernetes SIG-CLI project (kubernetes-sigs/kustomize); not a CNCF project |
| High-cardinality value variation | Natural (--set) | Awkward (overlay explosion) |
| Reviewability of a change | Render to diff | Direct YAML diff |
Configuration / API Surface — the same change, both ways
Bumping a replica count and an image tag for prod.
Helm — the chart’s templates/deployment.yaml is parameterized once:
# templates/deployment.yaml (authored once, in the chart)
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"# values-prod.yaml — the environment differs only in values
replicaCount: 10
image:
tag: "2.7.1"helm upgrade --install web ./chart -f values-prod.yamlKustomize — the base is plain YAML; the overlay patches it:
# base/deployment.yaml — plain, valid, deployable as-is
spec:
replicas: 1
template:
spec:
containers:
- name: web
image: registry.example.com/web:dev# overlays/prod/kustomization.yaml — structured transforms, no templating
resources:
- ../../base
replicas:
- { name: web, count: 10 }
images:
- { name: registry.example.com/web, newTag: "2.7.1" }kubectl apply -k ./overlays/prodNote what each example reveals: the Helm deployment.yaml is not valid Kubernetes YAML on its own (it has {{ }} in it) — it must be rendered. The Kustomize base/deployment.yaml is valid YAML and could be applied directly. That is the structural-safety argument made concrete.
The “use both” compositions
The conclusion the either/or framing hides — the two compose, and production teams routinely combine them:
-
helm template | kustomize(post-render). Render a third-party chart to plain YAML withhelm template, then run Kustomize over the output to apply local patches the chart doesn’t expose as values (an extra label, a sidecar, a security-context tweak). Helm provides the chart; Kustomize provides the last-mile customization the chart author didn’t anticipate. -
Kustomize’s Helm chart inflator. A
kustomization.yamlcan declare ahelmCharts:list (kubectl.docs.kubernetes.io — helmCharts);kustomize buildinvokes thehelmbinary as a subprocess to inflate (render) each chart and folds the output into the resource list, where overlay transformations then apply. Kustomize is the top-level driver; Helm is a sub-renderer. A complete entry looks like this (kubernetes-sigs/kustomize — chart.md example):helmCharts: - name: minecraft # chart name repo: https://itzg.github.io/minecraft-server-charts version: 3.1.3 # omit → latest releaseName: moria # sets .Release.Name during render includeCRDs: false # default false; render the chart's crds/ too? valuesInline: # values supplied inline (or valuesFile:/additionalValuesFiles:) minecraftServer: eula: trueThe documented fields are
name,repo,version,releaseName,namespace,valuesFile,valuesInline,additionalValuesFiles,includeCRDs,apiVersions,nameTemplate, andskipTests(kubectl.docs.kubernetes.io — kustomize builtins). The build must be run askustomize build --enable-helm— the flag exists, per the docs, “to have the user acknowledge that kustomize is running an external program as part of thebuildstep,” and ahelmbinary must be onPATH. The inflator deliberately implements only a limited subset of Helm — for example, it does not support private-repository or registry authentication. The critical operational gotcha: the kustomize that is embedded inkubectl(kubectl apply -k/kubectl kustomize) does not accept--enable-helmand will reject it as an unknown flag, sohelmChartscannot be inflated throughkubectldirectly (kubernetes/kubectl #1144, #1162). The standard workaround is the standalone binary piped to apply:kustomize build --enable-helm . | kubectl apply -f -. -
Helm for third-party, Kustomize for your own. Use
helm installfor software you didn’t write (Prometheus, cert-manager, ingress-nginx) — where packaging and the public catalog are the whole point — and use Kustomize bases/overlays for your own application manifests — where templating buys little and structural safety buys a lot. This is arguably the single most common mature pattern.
Failure Modes
-
Treating the choice as tribal. Picking one tool for everything — Helm-ifying your own simple app manifests, or Kustomize-ing your way around lacking a third-party chart catalog — forces the wrong tool’s weakness onto you. The failure is the either/or framing itself.
-
Helm template soup in your own charts. Authoring a heavily-
{{ if }}-branched chart for an in-house app you fully control imports Helm’s worst weakness for a use case Kustomize handles cleanly. If the manifests are yours and vary by patching, Kustomize is usually the better fit. -
Overlay explosion in Kustomize. Modelling a value that genuinely varies across many dimensions (per-tenant hostnames for 50 tenants) as 50 overlays is the Kustomize anti-shape; that is a parameterization problem, and Helm’s
--set(or a Helmrange) is the right tool. -
Double-customization confusion. When combining the tools, two layers can both touch the same field (a chart value and a Kustomize patch). Keep each field owned by exactly one layer, or the effective value becomes hard to reason about.
-
GitOps drift from rendering differences. A GitOps controller configured to render with Helm and a developer rendering locally with
helm templatecan diverge on.Capabilities-dependent output. Pin--kube-version/--api-versionsso renders are reproducible.
Alternatives and When to Choose Them
- Raw
kubectl apply -f— neither templating nor overlays; fine for a handful of static manifests, collapses into YAML Sprawl Anti-Pattern beyond that. - Jsonnet / Tanka — replaces both templating and patching with a real configuration language; maximum expressiveness, steepest learning curve, smallest community.
- CDK8s / Pulumi — generate manifests from a general-purpose programming language; full expressiveness, a real build toolchain, type checking.
- cdk8s + Helm/Kustomize — even these compose; the axis of “express config in a programming language vs in declarative data” is orthogonal to the Helm/Kustomize axis.
Production Notes
- The “Helm for third-party, Kustomize for first-party” split is widely reported as the equilibrium large platform teams settle into — Helm’s packaging is indispensable for consuming the ecosystem, Kustomize’s structural safety and kubectl-nativity are preferable for authoring your own manifests.
- Project governance differs, and it matters for procurement and longevity arguments. Helm is a CNCF graduated project (graduated 1 May 2020) with the full CNCF governance, security-audit, and trademark apparatus behind it (CNCF — Helm project page); its current release line is Helm 4 (November 2025, helm.sh). Kustomize, by contrast, is not a CNCF project at all — it is a Kubernetes SIG-CLI subproject living in
kubernetes-sigs/kustomize, vendored intokubectlitself. Neither status implies “more production-ready”; they are different governance models, and conflating CNCF maturity tiers with either tool’s release maturity is a common error. - ArgoCD and Flux support both natively. ArgoCD auto-detects
Chart.yamlvskustomization.yamlper Application; Flux ships both aHelmRelease/helm-controller and aKustomization/kustomize-controller. The GitOps layer is deliberately tool-agnostic — see GitOps, ArgoCD, Flux. - Helm’s post-render hook (
--post-renderer) lets you pipe Helm’s rendered output through any binary — frequentlykustomize— formalizing composition pattern (1) above as a singlehelm upgradeinvocation. - Interview framing. This comparison is a standard platform-engineering interview question; the strong answer is not “I prefer X” but the design-axis articulation plus the “they compose” conclusion — demonstrating you understand why each tool exists rather than which one your last team happened to use.
See Also
- Helm — the templating-and-packaging tool in full
- Helm Chart Anatomy — Helm chart internals
- Kustomize — the overlay-and-merge tool in full
- GitOps — the delivery layer that consumes either or both
- ArgoCD / Flux — GitOps engines supporting both natively
- Declarative vs Imperative Configuration — the strategic-merge semantics both tools build on
- YAML Sprawl Anti-Pattern — the duplication problem both tools address
- Kubernetes MOC — umbrella index (§15 Application Lifecycle and Delivery)