Loading...

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: template + values-prod.yaml vs Kustomize: 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/production

Decision matrix: which one fits your situation

Your situationUseWhy
Internal application, one team owns the manifestsKustomizePlain 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 installHelmA versioned chart with a values contract is the format the ecosystem expects, and release history gives consumers rollback.
Installing third-party softwareHelmAlmost 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 waysKustomizeOverlays 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 RBACHelmKustomize deliberately has no conditionals; expressing real variation as patches gets unreadable past a point.
GitOps with Argo CD or FluxEitherBoth 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.

kustomization.yaml — patching a vendor Helm 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.