Helm vs Kustomize: how to choose
Helm templates YAML with Go templates and packages the result as a versioned chart with a release lifecycle. Kustomize takes valid YAML and applies structured overlays to it. The distinction is templating versus patching, and it decides how your manifests read when something goes wrong.
Helm's strength is distribution. If you are shipping software for other people to install, a chart with values and a version is the format the ecosystem expects. Its weakness is that a chart with heavy conditional logic becomes a program written in Go template syntax, and debugging one means rendering it and reading the output rather than reading the source.
Kustomize's strength is that every input is real YAML you can read, validate, and diff. Its weakness is that expressing genuine variation gets verbose fast, and there is no packaging or release concept — no version to roll back to, no record of what was installed.
The same change, expressed in both tools
The clearest way to feel the difference is to make one real change both ways: run three replicas in production with a pinned image tag. In Helm, the Deployment is a template and the change lives in a values file. In Kustomize, the Deployment is plain YAML and the change is a patch in the production overlay.
# Helm — templates/deployment.yaml (excerpt)
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
# Helm — values-prod.yaml
replicaCount: 3
image:
repository: registry.example.com/app
tag: "1.42.0"
# helm upgrade --install app ./chart -f values-prod.yaml
# ---------------------------------------------------------------
# Kustomize — overlays/production/kustomization.yaml
resources:
- ../../base
images:
- name: registry.example.com/app
newTag: "1.42.0"
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: 3
target:
kind: Deployment
name: app
# kubectl apply -k overlays/productionDecision matrix: which one fits your situation
| Your situation | Use | Why |
|---|---|---|
| Internal application, one team owns the manifests | Kustomize | Plain YAML with explicit per-environment overlays is easier to read, review, and diff than a chart only you consume. |
| Distributing software for other teams to install | Helm | A versioned chart with a values contract is the format the ecosystem expects, and release history gives consumers rollback. |
| Installing third-party software | Helm | Almost everything ships as a chart. Patch vendor charts with Kustomize post-rendering when a value you need is not exposed. |
| Many environments that differ in small, reviewable ways | Kustomize | Overlays make the exact production-vs-staging delta visible in one diff instead of scattered template conditionals. |
| Install-time logic: optional components, loops over regions, conditional RBAC | Helm | Kustomize deliberately has no conditionals; expressing real variation as patches gets unreadable past a point. |
| GitOps with Argo CD or Flux | Either | Both are first-class in both controllers; the choice reverts to the ownership and distribution questions above. |
The hybrid pattern most teams land on
In practice this is rarely an either/or decision. The most common production arrangement uses Helm for third-party software — because that is how it is distributed — and Kustomize for first-party manifests, with Kustomize also post-processing rendered chart output when a vendor chart needs a change it does not expose as a value.
Kustomize can inflate a chart directly with the helmCharts field (kustomize build --enable-helm), which renders the chart and then applies your patches to the output. Under GitOps the same idea appears as Flux's postRenderers on a HelmRelease, or Argo CD running Kustomize on top of a rendered chart.
helmCharts:
- name: ingress-nginx
repo: https://kubernetes.github.io/ingress-nginx
version: 4.11.3
releaseName: ingress-nginx
namespace: ingress-nginx
valuesFile: values.yaml
patches:
# The chart exposes no value for this — patch the rendered output.
- patch: |-
- op: add
path: /spec/template/spec/topologySpreadConstraints
value:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
target:
kind: Deployment
name: ingress-nginx-controller
# kustomize build --enable-helm .Migrating between them
Helm to Kustomize is mechanical: helm template renders the chart to plain YAML, which becomes your Kustomize base, and per-environment values files become overlays. What you lose is the release ledger — helm rollback and helm history have no equivalent, so rollback becomes a git revert plus a re-apply, which is exactly how GitOps treats it anyway. What you gain is that every manifest in the repository is now real YAML that tools can validate.
Kustomize to Helm is the expensive direction. There is no generator that turns overlays into a well-factored chart; you are writing templates and a values schema by hand and deciding which knobs to expose. Do it when you need to distribute the application or need install-time logic, not for parity.
Verdict
For manifests you own and deploy yourself, default to Kustomize: it keeps the repository readable, keeps environment differences explicit, and has one less toolchain to version. Reach for Helm when you are consuming or distributing packaged software, or when configuration genuinely needs logic. Most platforms end up running both — Helm at the boundary where software enters the organisation, Kustomize everywhere the organisation writes its own YAML — and that is a sound end state, not a compromise.
Frequently asked questions
Can I use both together?
Yes, and it is common. Use Helm for third-party software where the chart is the distribution format, and Kustomize for your own manifests where you control the source. Kustomize can also post-process rendered Helm output, which is the usual way to patch a vendor chart that has no value for the field you need to change.
Which is better for multi-environment configuration?
Kustomize, in most cases. Overlays per environment are explicit and diffable — you can see exactly what production changes relative to the base. Helm expresses the same thing as values files feeding template conditionals, which centralises logic in the chart and makes the effective difference between two environments harder to see without rendering both.
Why do people dislike Go templates in Helm charts?
Because the template is not YAML, so indentation errors surface as parse failures far from their cause, and normal YAML tooling cannot validate the source. A chart with substantial conditional logic is effectively a program in a language with no debugger. Rendering locally before applying is the standard defence.
Does Kustomize handle CRDs and ordering?
It builds manifests but does not orchestrate the apply, so ordering is the client's problem. Helm has hooks and weights for sequencing. Under GitOps this matters less, since Argo CD sync waves or Flux dependencies handle ordering for either tool — the sequencing moves up a layer.
Which should I use for an internal application?
Kustomize, unless you need to distribute the application to teams who will install it themselves. For manifests owned and deployed by one team, plain YAML with overlays is easier to read, review, and debug than a chart, and you avoid maintaining templating for variation that only you consume.