Skip to content
Stribog

Delivery

All writing

Kustomize vs Helm: Which Manifest Can a Reviewer Read

Kustomize vs Helm decides what a pull request shows: an overlay, or a template plus values. How each hides intent, detects drift, and traces to approval.

Stribog13 min read

Most comparisons of the two tools list features: templating against patching, packaging, learning curve. That framing misses what actually goes wrong. The failure that hurts is a change that was approved in a pull request and did something the approver never saw — because the reviewer read a values file, or an overlay, and not the object that reached the cluster.

Three current facts make this sharper than it used to be. Argo CD inflates Helm charts with helm template, so a chart it deploys has no Helm release at all — helm ls and helm history show nothing. Helm 4 now sends chart hooks through post-renderers, which changes what a Kustomize last-mile patch touches. And Flux's helm-controller defaults to that new behaviour. This article is about which manifest a reviewer can read, what each delivery path compares against when it looks for drift, and which link in the approval chain an auditor will find missing.

Same YAML, different pull request

Take one change: a payments API goes to six replicas, a new image tag and a higher memory limit. In a Kustomize repository, the pull request touches the production overlay:

yaml
# deploy/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payments
resources:
  - ../../base
images:
  - name: ghcr.io/example/api
    newTag: "1.42.0"
patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: api
      spec:
        replicas: 6
        template:
          spec:
            containers:
              - name: api
                resources:
                  limits:
                    memory: 1Gi
A Kustomize production overlay. The patch is written in the shape of the object it changes, so the diff names the Deployment field.

In a Helm repository, the same change is a values file:

yaml
# charts/api/values-production.yaml
replicaCount: 6
image:
  repository: ghcr.io/example/api
  tag: "1.42.0"
resources:
  limits:
    memory: 1Gi
The equivalent Helm values. Whether `replicaCount` reaches `spec.replicas` depends on a template the pull request does not show.

Both can render an identical Deployment. The difference is where the meaning lives. The overlay's diff is in Kubernetes vocabulary: a reviewer reads spec.replicas: 6 and knows what it does. The values diff is in the chart author's vocabulary: replicaCount means whatever the template makes of it — it might be ignored when an autoscaler is enabled, it might be wrapped in a conditional, a helper might rename the container. To approve it honestly, the reviewer has to read the template too, or trust that nobody changed it.

That is the decision rule in one line, stated up front rather than hedged at the end: use both, and review neither source format on its own. Helm is the better way to package software you hand to others. Kustomize is the better way to own the last mile in your own repository. The control that matters is a rendered-manifest diff on every pull request.

What each tool actually is

Helm is three things in one binary: a Go-template engine, a chart packaging and distribution format, and a release manager. The third part is the one people forget. When Helm installs a chart it writes a release record, and by default "release information is stored in Secrets in the namespace of the release"; HELM_DRIVER can switch the backend to configmap, secret or sql. That record is what helm history, helm rollback and helm get manifest read — the last returns "a YAML-encoded representation of the Kubernetes resources that were generated from this release's chart(s)", according to its reference page, dependency charts included.

Helm also validates inputs, which matters for review. If a chart ships a values.schema.json, the chart documentation says it is applied on helm install, helm upgrade, helm lint and helm template, and that "the schema is applied to the final .Values object, and not just to the values.yaml file". A typo in a production values file can fail CI rather than silently fall back to a default — if the chart author wrote a schema.

Kustomize is a patch engine and nothing else. It reads plain manifests, applies overlays, generators and transformers, and prints YAML. It keeps no state and records no releases. It is also built into the client: per the Kubernetes documentation, "since 1.14, kubectl also supports the management of Kubernetes objects using a kustomization file", through kubectl kustomize and kubectl apply -k. The standalone binary moves faster than the copy embedded in kubectl; the latest release at the time of writing is kustomize/v5.8.3, published 8 October 2026, which made "no functional changes in kustomize".

The architectural difference follows: Helm knows what it installed last time; Kustomize does not know it ever ran. Neither property is good or bad by itself. A release record is evidence for an auditor and a second source of truth for an operator. Its absence is simplicity — and a gap someone else has to fill.

Each lane shows the reviewer a different source format. Only the hexagon is the same in both — which is why review belongs there.

What each one hides from a reviewer

Helm: logic, and a cluster that is not there

A chart's templates are a program. Conditionals, range loops, _helpers.tpl functions and defaults decide which objects exist and what their fields are. A values diff shows the program's input, never its output, and a chart version bump in Chart.yaml can change many rendered lines with a one-line diff.

The subtler blind spot is the cluster. Helm's lookup function "can be used to _look up_ resources in a running cluster", per the template guide, so a chart's output can depend on live state. But the tool CI uses to render is helm template, and its reference is blunt: "any values that would normally be looked up or retrieved in-cluster will be faked locally. Additionally, none of the server-side testing of chart validity (e.g. whether an API is supported) is done." A chart that branches on cluster capabilities or existing objects can render one thing in the CI job the reviewer reads and another under helm install. Pin what you can with --kube-version and --api-versions, and treat any chart that uses lookup as one whose rendered review is an approximation.

Kustomize: names, distance and indirection

Kustomize hides less logic but more context. Generated ConfigMaps and Secrets get a suffix: the Kubernetes documentation explains that "the generated ConfigMaps and Secrets have a content hash suffix appended" so that a content change produces a new object, and generatorOptions can disable it. That is good for rollouts and bad for diffs — every content change renames the object and updates the name references Kustomize is configured to track, so the rendered diff shows a delete-and-create rather than an edit.

Distance is the other cost. An overlay that points at a remote base, or a chain of overlays three levels deep, means the patch in the pull request is only meaningful relative to files the reviewer does not see. And vars — still common in older repositories — performs string substitution far from where the value lands; the Kustomize reference states that "the vars field was deprecated in v5.0.0" and will not be in the v1 Kustomization API. The page's migration section still carries older "plans to deprecate" wording; the status is deprecated, and the advice is replacements.

Both: Helm charts inside Kustomize

Kustomize can inflate a chart itself through the helmCharts field, which requires kustomize build --enable-helm. The maintainers restrict that builtin to bug fixes, critical security issues and fields analogous to helm template flags, and state they "will not add support for: private repository or registry authentication". The builtins reference adds that the inflation renders "all the contents of the templates directory, which can result in output different than what you get when using helm install" unless skipHooks is set. In Argo CD, the Kustomize guide notes the flag "is not part of the Kustomize options within Argo CD" — you need a plugin or a global kustomize.buildOptions entry in argocd-cm. Convenient, but a maintenance-only path with known output differences is not where a long-lived platform should rest its weight.

Review the render, not the source

The fix for both sets of blind spots is the same: render the base branch and the pull request head, diff the YAML, and put the diff in front of the reviewer. A source diff tells you what someone typed; a rendered diff tells you what the cluster will be asked to do.

bash
#!/usr/bin/env bash
# Render the base branch and the PR head, then post the rendered diff to the PR.
set -euo pipefail

: "${PR_NUMBER:?set PR_NUMBER to the pull request number}"
BASE_REF="${BASE_REF:-origin/main}"
WORK="$(mktemp -d)"
trap 'git worktree remove --force "$WORK/base" 2>/dev/null || true; rm -rf "$WORK"' EXIT

render() {
  local src="$1" out="$2"
  mkdir -p "$out"
  kustomize build "$src/deploy/overlays/production" > "$out/kustomize.yaml"
  helm template api "$src/charts/api" \
    --namespace payments \
    --values "$src/charts/api/values-production.yaml" \
    --kube-version 1.34.0 \
    --api-versions monitoring.coreos.com/v1 \
    > "$out/helm.yaml"
}

git worktree add --detach "$WORK/base" "$BASE_REF"
render "$WORK/base" "$WORK/render-base"
render "." "$WORK/render-head"

# diff exits 1 when the renders differ: that is the expected case, not a failure.
diff -ru "$WORK/render-base" "$WORK/render-head" > "$WORK/rendered.diff" || [ "$?" -eq 1 ]

if [ -s "$WORK/rendered.diff" ]; then
  {
    echo "Rendered manifest diff (base: $BASE_REF)"
    echo "~~~diff"
    head -c 60000 "$WORK/rendered.diff"
    echo "~~~"
  } > "$WORK/comment.md"
  gh pr comment "$PR_NUMBER" --body-file "$WORK/comment.md"
fi

# Optional: what a staging cluster would change, including server-side defaults.
if [ "${STAGING_CONTEXT:-}" != "" ]; then
  kubectl --context "$STAGING_CONTEXT" diff -k deploy/overlays/staging || [ "$?" -eq 1 ]
fi
A CI job that posts the rendered diff on every pull request. The `--kube-version` and `--api-versions` values are examples: set them to match the target cluster, because helm template cannot ask it. The diff is truncated so that a very large render still posts.

Two design choices matter. First, render both sides in the same job with the same tool versions, so a tool upgrade does not masquerade as a manifest change. Second, keep kubectl diff as a separate, optional step: the Kubernetes documentation describes kubectl diff -k as comparing objects "against the state that the cluster would be in if the manifest was applied", which needs credentials and a reachable cluster. The offline render is the review; the live diff is a check against reality.

For Helm releases that already exist, the helm-diff plugin does the live version of this: it "fetches the currently deployed version of a release and compares it to a local chart plus values". It needs a release record to compare against — which, as the next section shows, Argo CD never creates.

Argo CD has a GitOps-native form of the same idea. Its Source Hydrator — a beta feature since v3.5.0 — "pushes hydrated manifests to git before syncing them to the cluster", because templating tools "can obscure the actual Kubernetes manifests that are applied to the cluster". The rendered output becomes a commit, and a commit can be reviewed. Treat it as beta: useful, but not yet the foundation of an audit control.

Drift: who notices, and against what

Drift detection is a comparison, and the useful question is what each path compares the cluster against. Three common paths give three different answers.

Argo CD with a Helm chart. Argo CD's Helm guide states that "Helm is only used to inflate charts with helm template. The lifecycle of the application is handled by Argo CD instead of Helm." The FAQ spells out the consequence: "you cannot use any Helm command to view/verify the application." No release Secret, no helm history, no helm get manifest. Drift is measured against Argo CD's own rendering. Hooks are mapped by annotation, but the guide calls this "annotation compatibility, not a guarantee that hook lifecycle semantics are identical to Helm's", and pre-rollback, post-rollback, test-success and test-failure are not supported. One more trap for reviewers: values precedence is parameters > valuesObject > values > valueFiles > helm repository values.yaml, and the guide documents a UI bug that shows only the parameters, not the full merged set.

Flux HelmRelease. Flux keeps Helm's release model. With `.spec.driftDetection.mode` set to warn or enabled, helm-controller "will compare the manifest from the Helm storage with the current state of the cluster using a server-side dry-run apply"; enabled also corrects the drift. The reference is the release manifest — and because Flux post-renderers are "persisted by Helm to the manifest for the release in the storage", that manifest already includes any Kustomize patches applied on top.

Flux Kustomization. The reference is Git. Flux's recommended settings annotate the reconcile interval as the way to "detect drift and undo kubectl edits" and prune: true as the way to "remove stale resources from cluster". There is no separate record to consult: the overlay at a Git revision is the desired state.

Same cluster, three references. An auditor asking "what was supposed to be running?" gets a different answer from each path.

The controller trade-offs between those two GitOps engines at fleet scale are covered in the multi-cluster GitOps comparison; the same reconcile-loop logic is why Terraform should not own in-cluster workloads.

Helm 4 changed the hybrid

Helm v4.0.0 was released on 12 November 2025, at KubeCon + CloudNativeCon. For teams running Helm with a Kustomize post-render step, three changes in the Helm 4 overview need an audit.

  1. Post-renderers are plugins. Helm's end-of-life announcement puts it plainly: "helm install --post-renderer now takes a plugin name, not an executable path" — likewise on helm upgrade and helm template. (The Helm 4 overview still names a helm render command, which does not exist.) A CI job that pointed --post-renderer at a kustomize wrapper script needs repackaging.
  2. Hooks now reach the post-renderer. Per the advanced topics page, hooks and templates are sent together by default — the combined strategy — whereas Helm 3 "never sent hooks to post-renderers". The separate strategy invokes the post-renderer twice, and "Kustomize patches targeting template-only resources will fail when run against the hook stream"; Helm's guidance for those users is nohooks. The strategy is an SDK option; the plain CLI uses the default.
  3. Server-side apply for new releases only. Helm 4 "will default to server-side apply when installing a new Chart release", but upgrades and rollbacks follow the release's previous method, so "all releases created by Helm 3 will default to using client-side apply after upgrading to Helm 4". Two releases of the same chart can now be applied differently depending on when they were first installed.

Flux exposes the strategy as .spec.postRenderStrategy, which defaults to combined, "or nohooks when the UseHelm3Defaults feature gate is enabled" — and that gate defaults to false. Helm's own advanced page still describes a different Flux default; Flux's specification is the authority for Flux. A HelmRelease that packages with Helm, patches with Kustomize and detects drift looks like this:

yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: api
  namespace: payments
spec:
  interval: 10m
  chart:
    spec:
      chart: api
      version: "3.4.x"
      sourceRef:
        kind: HelmRepository
        name: internal-charts
        namespace: flux-system
  values:
    replicaCount: 6
  driftDetection:
    mode: warn
  postRenderers:
    - kustomize:
        patches:
          - target:
              kind: Deployment
              name: api
            patch: |
              - op: add
                path: /spec/template/spec/securityContext
                value:
                  runAsNonRoot: true
Helm packages, Kustomize owns the last mile, Flux reports drift without correcting it. Under the default combined strategy the patch sees hook resources too; its target selects only the Deployment.

The calendar matters as well. Helm 3's support window was extended on 2 June 2026: bug fixes ran up to the final Helm 3 feature release on 9 September 2026, limited to Kubernetes client library updates, and security fixes end on 10 February 2027, extended from November 2026. As of 1 November 2026 the feature window has closed and the security window is still open. Plan the post-renderer and apply-method audit inside that window, not after it.

What an auditor can trace

An auditor wants an unbroken chain: approved change, to rendered manifest, to applied state. Each setup breaks it in a different place.

  • Helm CLI from CI: commit SHA → (values + chart version) → release record. The release record holds the manifest, but the link from approval to rendered output exists only if CI saved the render it showed the reviewer.
  • Argo CD with Helm: commit SHA → Argo CD's rendering → cluster. There is no Helm release record at all; the rendered manifest is reproducible from the commit, but nobody reviewed it unless CI posted it — or the Source Hydrator committed it.
  • Flux HelmRelease: commit SHA → HelmRelease → post-rendered manifest in Helm storage. The strongest record of what was applied, held in Secrets inside the cluster, not in Git.
  • Flux Kustomization: commit SHA → overlay → cluster. The render can be reproduced from the revision with the same tool version — provided remote bases are pinned.

In every case the missing link is the same artefact: the rendered manifest, attached to the approval. Posting it to the pull request closes the gap for all four. Admission policy is the next layer — Kyverno or Gatekeeper evaluate the rendered object, not the template — and secrets need their own chain, which Sealed Secrets keeps in Git as ciphertext. For rollout evidence after merge, Argo Rollouts gates promotion on metrics rather than on the diff.

Exit ramps and the long game

Both tools are open source, and both have cheap exits. Kustomize output is plain YAML already. Helm output becomes plain YAML on request — and the result can be a Kustomize base:

bash
#!/usr/bin/env bash
# Freeze a running Helm release into a plain-YAML Kustomize base.
set -euo pipefail

RELEASE="${RELEASE:?set RELEASE}"
NAMESPACE="${NAMESPACE:?set NAMESPACE}"

mkdir -p base
helm get manifest "$RELEASE" -n "$NAMESPACE" > base/rendered.yaml
# Hooks are not in the manifest. Review them and decide what replaces each one.
helm get hooks "$RELEASE" -n "$NAMESPACE" > hooks-to-review.yaml

cat > base/kustomization.yaml <<'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - rendered.yaml
EOF

kustomize build base > /dev/null
For a Flux HelmRelease the manifest already includes post-renderer patches, because Flux persists them to release storage. Hook resources — migration Jobs and similar — are fetched separately and need a deliberate replacement, not a silent drop.

For an Argo CD-managed chart there is no release to read, so helm template with the same values is the equivalent starting point. Either way, the frozen base keeps its rendered history in Git from that day on — the same pattern that suits disconnected sites, where Fleet bundles per-site overlays rather than reaching back to a chart repository.

The long-game rule is short. Package with Helm when you distribute software to people who will configure it. Own the last mile with Kustomize, in your own repository, in Kubernetes vocabulary. Review the render, always — because templates change, tools change major versions, and the rendered manifest is the only form of the change that every tool, every reviewer and every auditor can read the same way.

§FAQ/Common questions

Frequently asked

Should I use Kustomize or Helm?

Usually both. Helm is a templating engine, a packaging format and a release manager, which suits software distributed to others. Kustomize is a template-free patch engine built into kubectl, which suits owning the last-mile configuration in your own repository. Whichever you use, post the rendered manifest diff on each pull request, because neither source format shows a reviewer the final object.

Why does helm ls show nothing for applications deployed by Argo CD?

Argo CD uses Helm only to inflate charts with helm template and then applies the manifests itself instead of running helm install. Its FAQ states you cannot use any Helm command to view or verify the application. There is no release Secret, so helm ls, helm history, helm get manifest and helm-diff have nothing to read.

Can I review helm template output instead of running helm install?

Yes, and you should, with one caveat. helm template fakes any values that would normally be looked up in-cluster and skips server-side checks such as whether an API is supported. Pin --kube-version and --api-versions to match the target cluster, and treat charts that use the lookup function as ones whose rendered review is an approximation.

What changed for Helm and Kustomize post-renderers in Helm 4?

Post-renderers are now plugins, so an executable cannot be passed directly to --post-renderer. Hooks are also sent to post-renderers, together with templates by default (the combined strategy). Under the separate strategy, Kustomize patches targeting template-only resources fail on the hook stream; Helm's guidance for those users is nohooks. Flux defaults to combined unless the UseHelm3Defaults gate is enabled.

How does Flux detect drift for a HelmRelease?

With .spec.driftDetection.mode set to warn or enabled, helm-controller compares the manifest stored in Helm storage with the cluster using a server-side dry-run apply. warn reports the drift; enabled also corrects it. Because post-renderer output is persisted to that manifest, any Kustomize patches are part of the reference.

kustomize vs helmhelm vs kustomizehelm template rendered manifest reviewkustomize overlays productionhelm post-renderer kustomizeargo cd source hydrator rendered manifests

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.