Tool comparison

Helm vs Kustomize for Kubernetes applications

Deploy the same representative API with Universal Helm Chart values and with Kustomize overlays—then decide where each tool owns the contract, without crowning a false winner.

Chart fidelity

Helm examples use Universal Helm Chart application 0.4.3 keys from values.yaml and configuration.md. Reproducible sources: examples/helm-vs-kustomize/ in this site repo. Replace ghcr.io/example-org/* images and hostnames.

Chart 0.4.3 — args / multi-port / service.enabled

From application 0.4.3: main container args (default []); Deployment revisionHistoryLimit (default 10); multi-port via containerPorts and service.ports; optional chart-managed Service via service.enabled (default true). When service.ports is set, Ingress / simple HTTPRoute / NOTES use the first entry (templates/_helpers.tpl application.servicePort). Legacy single-port service.name / service.port / service.protocol / service.appProtocol remain supported. Set service.enabled: false when the workload does not need a chart-managed Service. Do not invent other port keys.

Chart 0.4.2 — externalSecretHooks

Behavior change in application 0.4.2: ExternalSecret objects in extraManifests are ordinary resources by default. The chart does not add Helm hooks automatically. Recommend explicit lifecycle/ordering annotations on the manifest (for example argocd.argoproj.io/sync-wave: "-5"). Set externalSecretHooks.enabled: true (default false) only when legacy automatic helm.sh/hook: pre-install,pre-upgrade / hook-weight: "-5" / hook-delete-policy: before-hook-creation behavior is required (templates/1_extra-manifests.yaml). Do not invent other externalSecret* keys.

Tested versions (measured)

Fact vs recommendation

Measured claims cite golden renders under examples/helm-vs-kustomize/. Recommendations are labeled as such. No unverified performance or “hours saved” numbers.

Same app, two configuration models

Target workload: one HTTP API (demo-api) with probes, resources, env, Service on port 8080, and an HTTPRoute attached to a platform Gateway. Dev and prod differ by replicas, resources, APP_ENV, and hostname.

Helm path: one chart release + values files. Kustomize path: base manifests + overlays that patch the same fields. Neither path creates the Gateway—that stays platform-owned.

Helm values (dev excerpt)

image: ghcr.io/example-org/demo-api
imageTag: "1.2.0"
imagePullPolicy: IfNotPresent
replicaCount: 1
serviceAccount:
  create: true
  automount: false
service:
  port: 8080
env:
  - name: APP_ENV
    value: dev
route:
  enabled: true
  gateway: external
  gatewayNamespace: kgateway-system
  sectionName: https
  hostname: demo-api-dev.example.com
  path: /
  pathMatchType: PathPrefix

route.gatewayNamespace must match the platform Gateway namespace. This series uses lab/platform kgateway-system (aligned with the kgateway articles). Replace if your platform uses another NS—do not invent a chart default.

Hardening: set chart key serviceAccount.automount: false when the workload does not call the Kubernetes API (default chart value is true). Kustomize base mirrors automountServiceAccountToken: false on the ServiceAccount.

Full files: examples/helm-vs-kustomize/helm/values-dev.yaml and values-prod.yaml (replicaCount: 3, larger resources, demo-api.example.com).

Kustomize overlay (dev)

Base ships Deployment, Service, ServiceAccount, and HTTPRoute. The dev overlay patches env and hostname; prod also patches resources and sets replicas: 3.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
replicas:
  - name: demo-api
    count: 1
patches:
  - target:
      kind: HTTPRoute
      name: demo-api
    patch: |-
      - op: replace
        path: /spec/hostnames/0
        value: demo-api-dev.example.com

Render and compare

helm repo add universal https://chaser100.github.io/u-helm-chart
helm repo update
helm template demo-api-dev universal/application --version 0.4.3 \
  -f examples/helm-vs-kustomize/helm/values-dev.yaml | rg '^kind:'
kustomize build examples/helm-vs-kustomize/kustomize/overlays/dev | rg '^kind:'

Measured: both paths emit the same kind set—Deployment, Service, ServiceAccount, HTTPRoute. Hostnames and replica counts match the encoded env intent. Line counts differ (Helm templates carry chart labels/helpers; Kustomize base is minimal YAML)—that is structure, not a speed claim.

Packaging and distribution

Recommendation: when many services share Deployment/Service/HTTPRoute shape, package once (Helm chart) and vary values. When you already own final YAML and need env deltas, overlays stay lighter.

Values vs overlays

Complement: offline or CI helm template → commit into a Kustomize base when platform policy must patch labels/sidecars after packaging. Argo CD composition (accurate): a single Application source is either Helm (chart) or Kustomize (path)—Argo CD does not natively chain Helm render then Kustomize as a built-in post-render step on one source. Combine via (a) multi-source Applications (chart + Git values), (b) a Config Management Plugin that you own, or (c) render Helm offline and sync the resulting Kustomize directory. Validate your Argo CD version docs before relying on multi-source or CMP.

Schema validation

Measured: application 0.4.3 ships values.schema.json; helm lint -f values-*.yaml passes on the evidence pack. Schema uses JSON Schema with additionalProperties: true—unknown keys are not rejected by default; still prefer documented keys only.

Kustomize validates kustomization structure at build time; it does not provide an application-level values schema equivalent to Helm’s chart schema. Policy engines (Kyverno, OPA Gatekeeper) apply after render for both paths.

Official: Helm schema files.

Dependencies

Diff and debug ergonomics

# Helm: render + diff values layers
helm template demo-api universal/application --version 0.4.3 -f values-dev.yaml > /tmp/dev.yaml
helm template demo-api universal/application --version 0.4.3 -f values-prod.yaml > /tmp/prod.yaml
diff -u /tmp/dev.yaml /tmp/prod.yaml | head

# Kustomize: overlay diff
diff -u \
  <(kustomize build examples/helm-vs-kustomize/kustomize/overlays/dev) \
  <(kustomize build examples/helm-vs-kustomize/kustomize/overlays/prod) | head

# Cluster-facing (either path)
kubectl diff -f <(helm template ...)   # or kustomize build
helm get values <release>              # Helm release state only

Helm keeps release history / values on the cluster when installed via Helm. Pure Kustomize/GitOps often has no release secret—source of truth is Git. Neither fact implies fewer outages; it changes where you look when debugging.

Argo CD behavior

Argo CD syncs one source type per source entry: Helm charts (source.chart + helm.valueFiles / valuesObject) or Kustomize directories (source.path). That is composition of Application sources—not a native Helm→Kustomize post-render pipeline inside one source. Illustrative Applications (marked DO NOT APPLY AS-IS): examples/helm-vs-kustomize/argocd/applications.illustrative.yaml.

Official: Argo CD Helm, Argo CD Kustomize.

Not measured here: live Argo CD sync timings on a cluster. Render parity is verified offline; treat Application YAML as pattern documentation until your GitOps repo adopts it.

Shared Gateway, per-app HTTPRoute, kgateway policies

Scoped comparison for templating vs overlays (not a full Gateway migration guide):

Measured limitation: Universal Helm Chart application 0.4.3 has no first-class route.cors / TrafficPolicy keys—escape hatch only. See also kgateway migration patterns and examples/gateway-api-helm-contract/.

Ownership boundaries

With Universal Helm Chart, app teams stay on values; escape hatches (extraManifests, extraDeployments) exist when the model is insufficient—see Why one Helm chart is enough.

Upgrades, drift, and long-term maintenance

Recommendation: prefer a shared chart package for fleet-wide defaults, but treat every pinned consumer bump as an explicit change; keep Kustomize for environment-specific last-mile patches or non-chart workloads.

Failure modes

Decision table

ContextPrefer HelmPrefer KustomizeCombine
Many services, one Deployment/Service/HTTPRoute shapeYes — shared application chart + valuesOnly if you refuse chartsChart values in GitOps bridge
Need values JSON Schema / helm lintYes (values.schema.json)No native app schemaGenerate base from Helm, overlay patch
Last-mile label/sidecar policy on any YAMLChart hooks limitedYes — overlays/componentsOffline/CI Helm→Kustomize tree, multi-source, or owned CMP—not native single-source post-render
Publish reusable package to Artifact HubYesNo chart package
Single app, unique objects, no reuseOptionalYes — plain YAML + overlays
Subchart / dependency resolverHelm dependencies (not used by this leaf chart)Compose resources manuallySeparate releases

Where they complement each other

Universal Helm Chart is the reusable packaging contract. Kustomize is the YAML patch toolkit. Teams that already standardize on the chart should not rewrite templates as overlays just to “use Kustomize.” Teams with mature overlay libraries should not invent a private chart if a shared chart already encodes the same objects—unless they need escape hatches the values model cannot express.

Recommendation: Helm for the application contract; Kustomize for platform-wide mutations and exceptions; Argo CD to reconcile Helm or Kustomize sources (compose deliberately—do not assume native Helm→Kustomize chaining).

Editorial checklist

Related: Why one Helm chart is enough → · Gateway API vs Ingress → · Getting started →

Related in this series

Why one Helm chart is enough → · One chart vs many → · GitOps repository structure → · Gateway API vs Ingress →