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 --set away.
  • Packaging and distribution — a chart is a single versioned .tgz publishable 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 — is helm install.
  • Release lifecycle — numbered revisions, helm history, one-command helm 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, a range whitespace 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.yaml and 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, no kustomize 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 --set shines.

The honest decision table

ConcernHelmKustomize
Customization mechanismGo templates + values substitutionStructured strategic-merge / JSON6902 patches
Templating language to learnYes (Go templates + Sprig)No
Output validityOnly valid after renderingAlways structurally valid YAML
Packaging artifactVersioned chart .tgzNone
DistributionChart repos + OCI registriesNone (it’s just files in your repo)
VersioningChart version + appVersion (semver)Whatever your Git history gives you
Release lifecycle / rollbackYes — numbered revisions, helm rollbackNone — Kustomize doesn’t track deployments
Install third-party softwareThe standard way (Artifact Hub)Not its job
Free-form parametersYes (--set, -f)No — patches only
Tooling footprintSeparate helm binaryBuilt into kubectl
Project governanceCNCF graduated project (since 2020)Kubernetes SIG-CLI project (kubernetes-sigs/kustomize); not a CNCF project
High-cardinality value variationNatural (--set)Awkward (overlay explosion)
Reviewability of a changeRender to diffDirect 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.yaml

Kustomize — 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/prod

Note 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:

  1. helm template | kustomize (post-render). Render a third-party chart to plain YAML with helm 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.

  2. Kustomize’s Helm chart inflator. A kustomization.yaml can declare a helmCharts: list (kubectl.docs.kubernetes.io — helmCharts); kustomize build invokes the helm binary 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: true

    The documented fields are name, repo, version, releaseName, namespace, valuesFile, valuesInline, additionalValuesFiles, includeCRDs, apiVersions, nameTemplate, and skipTests (kubectl.docs.kubernetes.io — kustomize builtins). The build must be run as kustomize build --enable-helm — the flag exists, per the docs, “to have the user acknowledge that kustomize is running an external program as part of the build step,” and a helm binary must be on PATH. 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 in kubectl (kubectl apply -k / kubectl kustomize) does not accept --enable-helm and will reject it as an unknown flag, so helmCharts cannot be inflated through kubectl directly (kubernetes/kubectl #1144, #1162). The standard workaround is the standalone binary piped to apply: kustomize build --enable-helm . | kubectl apply -f -.

  3. Helm for third-party, Kustomize for your own. Use helm install for 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

  1. 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.

  2. 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.

  3. 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 Helm range) is the right tool.

  4. 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.

  5. GitOps drift from rendering differences. A GitOps controller configured to render with Helm and a developer rendering locally with helm template can diverge on .Capabilities-dependent output. Pin --kube-version/--api-versions so 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 into kubectl itself. 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.yaml vs kustomization.yaml per Application; Flux ships both a HelmRelease/helm-controller and a Kustomization/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 — frequently kustomize — formalizing composition pattern (1) above as a single helm upgrade invocation.
  • 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