Kustomize vs Helm in GitOps: Overlays, Patches, Templating and When to Use Each with Argo CD and Flux (2026)

Kustomize vs Helm in GitOps: Overlays, Patches, Templating and When to Use Each with Argo CD and Flux (2026)

Kustomize vs Helm in GitOps: Overlays, Patches and When to Use Each

Most teams that argue about kustomize vs helm are really arguing about something else: who owns the variability in a Kubernetes manifest, and where in the pipeline the final YAML is allowed to appear. Helm answers by shipping a parameterised template and a values contract. Kustomize answers by shipping plain YAML and a set of declarative edits. Both end up as the same Deployments and Services, yet they fail in completely different ways at 2 a.m., and they interact with Argo CD and Flux through very different seams.

This matters more in 2026 than it did two years ago. Helm 4 shipped in November 2025 with server-side apply and kstatus-based waiting, Argo CD now renders charts with Helm 4, and Flux’s helm-controller exposes the new Helm 4 knobs directly. The choice is no longer “pick one tool”, it is “pick a rendering pipeline”.

You will leave with a clear mental model (template engine versus patch engine), a layout for overlays, components and replacements, a safe way to combine the two, and a decision matrix.

What this covers: the two mental models, reference repo layouts, how Argo CD and Flux actually render each tool, helm-in-kustomize and post-renderers, secrets, drift, testing and diffing, anti-patterns, and a recommendation checklist.

Context and Background

Kubernetes manifests are verbose, repetitive and environment specific. The same Deployment differs between dev and prod in replica count, resource limits, image tag, ingress host, and a handful of feature flags. Copy-pasting the file per environment works until the third environment, and then a security fix to the base needs to be applied in five places. Every configuration management tool in the ecosystem is an answer to that duplication problem.

Helm is the older answer. It is a package manager: a chart is a versioned archive of Go templates plus a values.yaml that documents the knobs. It was designed for distributing software to strangers, which is why it has a release concept, hooks, dependencies, and a registry story based on OCI. Kustomize is the later answer, built into kubectl and maintained under kubernetes-sigs/kustomize. It is not a package manager. It takes a base of valid YAML and applies transformations declared in a kustomization.yaml. The latest Kustomize release I could verify while writing this was v5.8.1, which added Helm v4 support alongside Helm v3 and allowed empty patch files.

GitOps changes the evaluation criteria. In a pull-based model, a controller (Argo CD or Flux) watches a Git repository, renders the manifests, and reconciles the cluster toward them. Rendering therefore happens inside the controller, not on a laptop, and the properties that matter are determinism, reviewability of the rendered diff, and how the tool behaves when the live cluster drifts. If you want the controller-side background, our Argo CD 3 upgrade guide covers how Argo CD’s repo-server and application controller split that work.

Helm’s own life cycle also matters here. Helm v4.0.0 was announced on 12 November 2025 at KubeCon and was the first new major version in six years. According to the Helm project, v3 receives bug fixes until 8 July 2026 and security fixes until 11 November 2026, with no feature backports beyond Kubernetes client library updates (Helm 4 release announcement). If you still run Helm 3 anywhere in a pipeline, that date is days away; our Helm 4 migration piece covers the upgrade path.

Finally, both tools coexist more often than blog titles imply. Kustomize can inflate a Helm chart through its helmCharts generator, and Flux’s HelmRelease can post-process rendered output with Kustomize patches. The real design question is which tool owns which layer.

The Core Argument: Templates Describe a Family, Patches Describe a Delta

The shortest honest comparison is this. Helm is a template engine: the chart author decides in advance which fields are variable and exposes them as values. Kustomize is a patch engine: the base author decides nothing in advance, and consumers can modify any field of any resource they can address. Helm is closed by default and open by contract. Kustomize is open by default and constrained by convention.

kustomize vs helm mental models: a template engine merges values into templates while a patch engine layers overlays onto a plain base

Figure 1: Two mental models. Helm renders a family of manifests from templates and values. Kustomize derives variants from a plain base by applying declared transformations.

Figure 1 shows why the two tools feel so different in code review. A Helm change is usually a values diff, small and declarative, but the consequence depends on template logic you have to read separately. A Kustomize change is a patch diff, which is explicit about the field it touches but can be fragile against changes in the base. Neither is strictly better; each moves the complexity to a different place.

Helm: the values contract

A chart exposes a typed-by-convention interface through values.yaml, and ideally a values.schema.json that validates it. This contract is Helm’s superpower for distribution. A consumer of an upstream chart such as an ingress controller does not need to know how the Deployment is assembled; they set replicaCount and resources. The chart author can refactor templates freely as long as the contract holds, and semantic versioning of the chart communicates breaking changes.

The cost is that anything the author did not parameterise is out of reach. Every Helm user eventually opens an issue asking for an extraEnv, podAnnotations or topologySpreadConstraints value, and mature charts accumulate dozens of escape hatches precisely because the contract is the only door. Template logic (if, range, include, tpl) also makes the chart a small program, and a program needs tests.

Helm also owns a release lifecycle: a release record per install, revision history, hooks and rollback. In GitOps this is a mixed blessing. It is valuable when Helm itself performs the install, as in Flux’s helm-controller, and largely irrelevant when Argo CD only uses Helm as a renderer, because Argo CD runs helm template and manages the lifecycle itself (Argo CD Helm docs).

Kustomize: the transformation pipeline

A kustomization.yaml lists resources and the transformations applied to them. The fields most teams use are resources, patches, images, replicas, namePrefix and namespace, labels, configMapGenerator, components, replacements and helmCharts, all documented in the kustomization reference.

The key property is that the base is valid, deployable YAML by itself. You can kubectl apply -k base/ and it works. There is no template syntax to lint away, no values schema to maintain, and any developer who reads Kubernetes YAML can read the base. The price is that variability is expressed as edits, so the quality of the design depends on how well you partition bases, overlays and components.

Why the distinction is really about trust boundaries

Here is the framing I find most useful, and it is an opinion rather than a documented rule. Ask who is the author and who is the consumer.

If the author and consumer are different organisations, choose Helm. A vendor cannot predict your overlays, and you cannot predict their internals; a versioned values contract is the right boundary. If author and consumer are the same team, or at least the same company and the same Git repository, the contract is overhead. Kustomize lets the platform team publish a base and lets application teams patch what they need without a negotiation per field.

The middle case, an internal platform team shipping a golden application chart to dozens of product teams, is where both survive. A chart gives the platform team a controlled interface; overlays or post-renderers give product teams environment specific tuning. Our platform engineering golden paths article discusses that interface design at the organisational level.

Overlays, Components, Patches and Replacements in Practice

The standard Kustomize layout has a base directory and one overlay per environment. The overlay references the base and declares only the differences.

apps/payments/
  base/
    kustomization.yaml
    deployment.yaml
    service.yaml
  components/
    hpa/
      kustomization.yaml
      hpa.yaml
    pdb/
      kustomization.yaml
      pdb.yaml
  overlays/
    dev/
      kustomization.yaml
    prod/
      kustomization.yaml
      replica-patch.yaml

A prod overlay then looks like this:

# apps/payments/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payments
resources:
  - ../../base
components:
  - ../../components/hpa
  - ../../components/pdb
images:
  - name: registry.example.com/payments/api
    newTag: "1.42.3"
replicas:
  - name: api
    count: 6
patches:
  - path: replica-patch.yaml
  - target:
      kind: Deployment
      name: api
    patch: |-
      - op: add
        path: /spec/template/spec/containers/0/resources
        value:
          requests: {cpu: "500m", memory: 512Mi}
          limits: {memory: 1Gi}

Strategic merge versus JSON 6902

Kustomize supports two patch dialects, both expressed under the single patches field with an optional target selector. A strategic merge patch is a partial resource: you write the fragment you want and Kubernetes’ merge semantics (including list merge keys such as container name) combine it with the original. A JSON 6902 patch is a list of operations (add, replace, remove) addressed by path. Strategic merge reads better for adding a container or env var; JSON 6902 is necessary when you need to remove a field or address a list element by index.

The practical advice is to prefer strategic merge, keep each patch to one concern, and name the file after the concern (add-topology-spread.yaml, not patch1.yaml). A patch that targets a list element by index, as the example above does with containers/0, breaks silently the day someone adds a sidecar ahead of the main container. Where possible, use a strategic merge patch keyed on container name instead.

Components for optional, cross-cutting features

A Kustomize component is a reusable bundle of resources and patches that you opt into per overlay. It solves a problem that overlay inheritance cannot: an HPA and a PodDisruptionBudget are wanted in prod and staging but not in dev, while a different feature is wanted in dev and prod but not staging. Overlays form a tree, so you cannot express an arbitrary subset. Components are composable and orthogonal, so they can.

The caveat is maturity. The Flux documentation still labels Kustomize components as an alpha feature in Kustomize and treats them as experimental (Flux Kustomization docs). They are widely used in practice, but pin your Kustomize version and test upgrades. Argo CD additionally errors on a missing component directory unless ignoreMissingComponents is set (Argo CD Kustomize docs).

Replacements: copying values inside the tree

Replacements substitute fields in one or more targets with a value taken from a source field. A typical use is making a single hostname defined in a ConfigMap appear in an Ingress and in a Deployment env var without hard-coding it twice. They replace the older vars mechanism, which is deprecated in the Kustomize project.

replacements:
  - source:
      kind: ConfigMap
      name: app-config
      fieldPath: data.PUBLIC_HOST
    targets:
      - select:
          kind: Ingress
          name: api
        fieldPaths:
          - spec.rules.0.host

Use replacements sparingly. They create implicit data flow across files, and that is exactly the debugging experience people hate in templates. If you find yourself writing five replacements per overlay, the variability probably belongs in a chart.

How Argo CD and Flux Actually Render Each Tool

The same repository behaves differently depending on the controller, and this is where most “Helm or Kustomize” debates are settled in practice. The two controllers have different architectures: Argo CD is a renderer plus a diff-and-sync engine, while Flux splits the work into a source-controller, a kustomize-controller and a helm-controller.

helm charts vs kustomize rendering pipelines in Argo CD and Flux, from Git source to applied manifests

Figure 2: Rendering pipelines. Argo CD renders Helm with helm template and Kustomize with kustomize build, then syncs. Flux applies Kustomizations through kustomize-controller and installs charts through helm-controller.

Argo CD: everything becomes rendered YAML

Argo CD inspects the application’s source path. If a kustomization.yaml exists, it runs Kustomize; if a Chart.yaml exists, it runs helm template. In both cases the output is plain manifests that Argo CD diffs against live state and applies itself. For Helm that has consequences the docs spell out: Helm does not manage the release lifecycle, Argo CD maps pre-install and pre-upgrade hooks to PreSync and post-install and post-upgrade hooks to PostSync, Helm test and rollback hooks are unsupported, and Argo CD cannot tell install from upgrade, so both kinds of hooks run on every sync (Argo CD Helm docs).

Helm value precedence in Argo CD, highest first, is parameters, valuesObject, values, valueFiles, then the chart’s own values.yaml. Value files accept globs that expand lexically, so later files win, and since Argo CD 2.6 they can come from a different repository. That last feature enables a clean pattern: the chart lives in one repo or an OCI registry, and each environment’s values live next to the Application manifests.

On the Helm version, the current stable docs state that from Argo CD v3.5 charts are rendered with Helm v4 only. If your chart relied on Helm 3 specific behaviour, test it before upgrading Argo CD. A subtle trap the docs also mention: a chart that uses randAlphaNum renders a different value on every render, so the application is perpetually OutOfSync unless you pin it.

For Kustomize, Argo CD exposes declarative overrides under spec.source.kustomize (namePrefix, images, replicas, patches, components, namespace and others). Global build flags go in kustomize.buildOptions in argocd-cm, and you can register several Kustomize binaries and select one per application with spec.source.kustomize.version. Two documented gotchas deserve attention. Remote bases inherit credentials only from the application’s own repository, so a base in a different private repository fails even if Argo CD knows that repository. And spec.destination.namespace can miss namespace fields on some custom resources, so the docs recommend setting spec.source.kustomize.namespace instead.

Flux: Kustomization for YAML, HelmRelease for charts

Flux has a clear division of labour. A Kustomization object (the Flux CRD, not to be confused with the Kustomize file of the same name) tells kustomize-controller to build a path from a Git or OCI source and apply it. It supports inline patches with target selectors, components, prune, dependsOn, wait, healthChecks, SOPS decryption, and postBuild.substituteFrom, which substitutes ${VAR} placeholders from ConfigMaps and Secrets after the build (Flux Kustomization docs).

A HelmRelease tells helm-controller to install a chart for real, keeping a Helm release record. Values merge from valuesFrom sources in listed order, with inline .spec.values overriding all of them. Install and upgrade remediation, with retries and rollback or uninstall strategies, are first-class, and so is drift detection: in warn or enabled mode the controller compares the Helm storage manifest with the cluster using a server-side dry-run apply, and enabled corrects what it finds (Flux HelmRelease docs).

The Helm 4 defaults are visible in that API. install.serverSideApply defaults to true, or false when the UseHelm3Defaults feature gate is on. waitStrategy defaults to poller, which is kstatus based, versus legacy for Helm 3 behaviour. postRenderStrategy defaults to combined, the Helm 4 default, with nohooks reproducing Helm 3 behaviour. These matter because server-side apply changes field ownership: after a Helm 4 server-side install, other controllers such as an HPA or a mutating webhook that write fields will be tracked as separate field managers instead of silently fighting the three-way merge.

The same app in both controllers

An Argo CD Application pointing at a Kustomize overlay is almost trivial:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments-prod
  namespace: argocd
spec:
  project: payments
  source:
    repoURL: https://git.example.com/platform/apps.git
    targetRevision: main
    path: apps/payments/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: payments
  syncPolicy:
    automated: {prune: true, selfHeal: true}

The Flux equivalent is a Kustomization:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: payments-prod
  namespace: flux-system
spec:
  interval: 10m
  path: ./apps/payments/overlays/prod
  prune: true
  wait: true
  sourceRef:
    kind: GitRepository
    name: apps

Notice how little differs. The tool choice (Kustomize) is the same; the controller wiring is a different dialect. For Helm the divergence is larger, because Argo CD treats the chart as a renderer input while Flux treats it as an installable release. If you intend to run progressive delivery on top, read our Argo Rollouts vs Flagger ADR, since both interact with how Deployments are rendered and mutated.

Combining the Two: Helm Inside Kustomize and Post-Renderers

The most productive pattern in 2026 is not either-or. Consume third-party software as a chart, because its authors maintain the templates, and apply your organisation’s modifications as patches, because that is what patches are good at. There are three ways to do it.

helm in kustomize and post-renderer patterns combining a chart with environment specific patches

Figure 3: Three combination patterns. Kustomize inflates a chart with helmCharts, Flux patches a release with postRenderers, or the chart is rendered to Git and Kustomize treats it as a plain base.

Pattern A: helmCharts in a kustomization

Kustomize has a chart inflation generator. You declare the chart, version, release name, namespace and values inline, and Kustomize runs Helm to produce resources that your patches can then edit.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
  - name: cert-manager
    repo: https://charts.jetstack.io
    version: v1.17.0
    releaseName: cert-manager
    namespace: cert-manager
    includeCRDs: true
    valuesInline:
      crds:
        enabled: true
patches:
  - target:
      kind: Deployment
      name: cert-manager
    patch: |-
      - op: add
        path: /spec/template/spec/priorityClassName
        value: system-cluster-critical

The chart version shown is illustrative; pin to whatever you have vetted. Rendering charts inside Kustomize requires the --enable-helm build flag, which is not enabled by default. In Argo CD that flag is not an application option: you add it to kustomize.buildOptions in argocd-cm, which applies to every Kustomize application, or you write a config management plugin for just the apps that need it (Argo CD Kustomize docs). The global flag widens what any Kustomize repo can execute on your repo-server, so treat it as a security decision, not a convenience toggle.

Be aware of what you give up. Argo CD’s Helm hook mapping does not apply, because the Helm step is hidden inside Kustomize and hooks arrive as ordinary annotated resources. Chart hooks, helm test and lookups behave differently when the chart is only inflated. Kustomize v5.8.1 added Helm v4 support next to Helm v3, so check which Helm binary your Kustomize shells out to.

Pattern B: Flux postRenderers

If you use Flux’s HelmRelease, the supported way to patch a chart’s output is spec.postRenderers, which applies Kustomize patches and images to the rendered manifests in the order listed. Helm persists the post-rendered result in the release manifest, so the release record matches the cluster. One documented limitation: post-renderers do not apply to chart hooks.

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: ingress
  namespace: infra
spec:
  interval: 15m
  chartRef:
    kind: OCIRepository
    name: ingress-chart
  values:
    controller:
      replicaCount: 3
  postRenderers:
    - kustomize:
        patches:
          - target: {kind: Deployment, name: ingress-controller}
            patch: |-
              - op: add
                path: /spec/template/metadata/annotations/team
                value: platform

The Flux docs recommend OCIRepository with a layer selector for production chart sources, and note that for OCI sources the artifact digest is appended to the chart version, so a re-pushed tag with a new digest triggers an upgrade even when the chart version string is unchanged. That is helpful for supply-chain integrity, and a surprise if you assumed tags are immutable.

Pattern C: render to Git, patch the output

The third option is to run helm template in CI, commit the rendered YAML, and let Kustomize treat it as a plain base. This is sometimes called the rendered manifests pattern. The benefit is that the exact bytes applied are in Git, reviewable in a pull request, and independent of controller-side rendering quirks. The costs are a bigger repository, a CI job you must keep honest, and the loss of Helm’s runtime features. I reach for it when auditability is the top requirement, for example in regulated environments, and when the chart changes only a few times a quarter.

Choosing among the three

Pattern A fits Argo CD shops that want a single mental model (Kustomize everywhere) and accept the global flag. Pattern B fits Flux shops, where the release lifecycle, rollback and drift detection of helm-controller are a feature. Pattern C fits anyone who values a Git-visible final state over convenience. Mixing all three in one repository is possible, but it multiplies the number of ways a manifest can be produced, and every extra rendering path is a place where “works on my machine” re-enters a GitOps workflow.

Secrets, Drift, Testing and Diffing

The previous sections covered how manifests are produced. This one covers what happens around them: how secrets get in, how you notice that the cluster no longer matches Git, and how you review a change before it ships.

Secrets: neither tool should hold plaintext

Neither Helm nor Kustomize encrypts anything. A secretGenerator in Kustomize produces a base64 Secret from literals or files, and a chart’s secret.yaml template does the same from values; both put plaintext or trivially decodable data in Git or in a rendered artifact. The workable options are the same for both tools, and the choice follows from your controller.

Flux has the most integrated answer. kustomize-controller supports SOPS decryption natively, with age, OpenPGP or cloud KMS keys (AWS KMS, Azure Key Vault, GCP KMS, or Vault-compatible backends), and it is the only decryption provider the Kustomization API supports. Encrypted Secret files sit in the overlay, and the controller decrypts them at apply time. Argo CD has no built-in equivalent; teams typically use an External Secrets operator, a Sealed Secrets controller, or a plugin. My preference in either controller is to keep secret values out of the repository altogether and commit only a reference object that an in-cluster operator resolves. It reduces the blast radius of a leaked repo and decouples rotation from deploys.

Two Flux-specific details are worth knowing. postBuild.substituteFrom can read variables from Secrets, which is convenient but means substituted values appear in applied manifests. And the annotation kustomize.toolkit.fluxcd.io/decrypt: Disabled skips decryption for a resource, which is the escape hatch when a file legitimately contains SOPS-looking content.

A related Kustomize behaviour trips people up: configMapGenerator and secretGenerator append a content hash suffix to names by default, so that a config change rolls the pods that reference it. Kustomize rewrites references in the same build, which is elegant, but the generated name changes on each edit and Argo CD may flag the old object as extraneous. The Argo CD docs recommend the IgnoreExtraneous compare option for generated resources so they do not appear as drift. Setting generatorOptions: {disableNameSuffixHash: true} removes the rolling behaviour; do it only if something else triggers restarts.

Drift: who owns a field?

Drift is any difference between the rendered desired state and the live object. Some drift is unwanted (an engineer ran kubectl edit), and some is by design (an HPA changes spec.replicas, a service mesh injects a sidecar, an admission policy adds defaults).

gitops config management drift loop comparing rendered manifests with live cluster state and field ownership

Figure 4: The drift loop. The controller renders manifests, compares them with live state, and either reports, corrects or ignores each difference depending on field ownership rules.

The two tools interact with drift handling differently. With Kustomize, desired state is the rendered YAML and the controller’s diff engine does the comparing. Argo CD offers ignoreDifferences per resource and field, and Flux offers per-resource annotations and a prune switch. A classic mistake is setting replicas in an overlay for a Deployment that also has an HPA: the controller and the autoscaler fight, and the app flaps between OutOfSync and Synced. The fix is to omit spec.replicas from the base for HPA-managed workloads, or to ignore the field in the diff.

With Helm under Flux, helm-controller can compare the Helm storage manifest to the cluster using a server-side dry-run apply. In warn mode it emits a Kubernetes Event, in enabled mode it corrects drift, and ignore rules use JSON Pointers with optional target selectors. Individual resources can opt out with the helm.toolkit.fluxcd.io/driftDetection: disabled label or annotation. Under Argo CD, Helm is only the renderer, so drift is Argo CD’s concern, with selfHeal controlling auto-correction.

Server-side apply is the real structural change here. Helm 4 supports it, and Flux’s serverSideApply options accept enabled, disabled or auto on upgrade, with auto following what the release used previously. Field ownership is then explicit, which makes “who changed this?” answerable. See our Kubernetes operators article for why reconcilers that share fields with other writers need careful ownership, and the Terraform drift article for the infrastructure analogue of the same idea.

Testing and diffing the rendered output

Whatever tool you choose, review the rendered result, not just the source diff. A one-line values change can alter dozens of resources, and a patch can silently no-op if its target selector stopped matching.

The minimal, tool-agnostic pipeline in CI looks like this:

# Render
kustomize build apps/payments/overlays/prod > /tmp/prod.yaml
helm template payments ./charts/payments -f values/prod.yaml > /tmp/prod-helm.yaml

# Validate against the Kubernetes API schemas
kubeconform -strict -summary /tmp/prod.yaml

# Diff against the base branch render
git stash -q; kustomize build apps/payments/overlays/prod > /tmp/main.yaml; git stash pop -q
diff -u /tmp/main.yaml /tmp/prod.yaml || true

Add three more layers as you mature. First, policy checks on the rendered manifests with a policy engine; our piece on ValidatingAdmissionPolicy with CEL shows how to enforce the same rules in-cluster so CI and admission agree. Second, for Helm, helm lint, a values.schema.json, and helm unittest-style tests for conditional template branches (the unit-test plugin is a community project and I have not benchmarked it here). Third, a server-side dry run (kubectl apply --dry-run=server) against a representative cluster, which catches admission failures that schema validation cannot.

Argo CD users get an extra preview: argocd app diff --local renders local files and diffs them against live state, and many teams run a pull-request preview that posts the diff. Flux users commonly use flux diff kustomization in CI for the same purpose. Either way, the habit that pays off is posting the rendered diff in the pull request, so reviewers see the Deployment that will exist rather than the patch that describes it.

Decision Matrix: When to Use Which

No single column wins. The matrix below summarises the trade-offs; ratings are my judgement, not measurements.

Criterion Helm chart Kustomize overlays Helm in Kustomize
Consuming third-party software Best: upstream ships charts Poor: you must vendor YAML Good: chart plus your patches
Internal app with few environments Heavier than needed Best: plain YAML plus patches Unnecessary
Internal platform shipping a golden path Strong: values contract Weaker: no interface boundary Strong
Reading the base without tooling Hard: template syntax Easy: valid YAML Mixed
Patching a field nobody parameterised Fork or post-renderer Trivial Trivial
Release history and rollback Native (Helm release) Via Git revert Via Git revert
Hooks and test hooks Native in Flux, mapped in Argo CD Not applicable Weaker
Review of the real diff Requires rendering Requires rendering Requires rendering
Secret handling External or SOPS External or SOPS External or SOPS
Argo CD fit Good, renders via template Excellent Needs enable-helm flag
Flux fit Excellent via HelmRelease Excellent via Kustomization Use postRenderers instead

A shorter heuristic follows from the matrix.

  • Use Helm when you consume upstream software, when you publish for other teams, or when variability is large and structured.
  • Use Kustomize when you own the manifests, when environments differ by a handful of edits, and when readable raw YAML is a team value.
  • Use both when you consume upstream charts and must apply organisation-wide modifications, using post-renderers in Flux or helmCharts in Argo CD with the flag consciously enabled.
  • Render to Git when auditability beats convenience.

Trade-offs, Gotchas, and What Goes Wrong

Patch fragility. Kustomize patches couple your overlay to the internal shape of the base. Index-addressed JSON patches break when a list is reordered, and a target selector that stops matching either errors or silently does nothing depending on the case. Treat the rendered diff as the test, and keep patches small and named.

Overlay sprawl. Teams add an overlay per environment, then per region, then per cluster, then per tenant. The result is a combinatorial tree where a base change needs verification in thirty places. Prefer components for cross-cutting features and generate the per-cluster leaf from a small data file (Argo CD ApplicationSets or Flux variable substitution) instead of hand-writing it.

Values sprawl and template logic. A chart with five hundred lines of values.yaml and nested conditionals is a program nobody tests. Helm’s contract is an asset only while it stays small and schema validated. Beware tpl and lookup: lookup queries the cluster during rendering, which behaves differently under helm template than under a real install, so renders become non-deterministic across contexts.

Non-deterministic renders. randAlphaNum and similar functions produce a new value each render, so the application never settles. Pin generated secrets, or move them out of the chart.

Hidden controller-side configuration. Enabling --enable-helm globally, selecting a Kustomize version per application, or relying on a feature gate such as UseHelm3Defaults moves behaviour into controller configuration that is not in the application repo. Document it where the repo lives.

Version skew. Argo CD from v3.5 renders charts with Helm 4 only, Flux exposes Helm 3 behaviour behind a feature gate, and Kustomize shells out to whichever Helm binary is on the path. Upgrading one without checking the others is how a quiet Tuesday becomes an incident. The Helm 3 security-fix window ends on 11 November 2026 according to the Helm project.

Server-side apply surprises. Moving to server-side apply changes field ownership and conflict behaviour. A write by another controller that used to be silently overwritten may now surface as a conflict. That is usually the correct outcome, but it needs a plan before a fleet-wide upgrade, and in Flux force is ignored under server-side apply.

The “just one more values file” anti-pattern. Stacking five values files, an inline valuesObject and parameter overrides makes the effective configuration unknowable without rendering. Fix the precedence order in documentation, cap the layers at two or three, and always render in CI.

Practical Recommendations

Start from ownership, not preference. For every workload in the repository, write down who authors the manifests and who consumes them, and let that decide the tool. Upstream and cross-team software gets a chart. Same-team applications get a plain base with Kustomize overlays and a few components.

Standardise one rendering path per controller. On Argo CD, that is usually Kustomize overlays for your own apps and Helm sources with values files for upstream charts, with --enable-helm left off unless you have a documented reason. On Flux, that is Kustomization for your YAML and HelmRelease for charts, with postRenderers for the small edits you need.

Make the rendered diff a merge gate. Render both the target branch and the pull request, validate with a schema checker, run policy checks, and post the diff. Decide your drift policy per workload: self-heal everything by default, and explicitly ignore the fields that other controllers legitimately own.

Keep secret values out of Git. Use SOPS under Flux or a reference-based operator under either controller.

A short checklist you can adopt this week:

  • [ ] Every workload has a named author and a chosen tool, recorded in the repo README.
  • [ ] Helm versions across CI, Argo CD, Flux and Kustomize are known and aligned with Helm 4.
  • [ ] Charts have a values.schema.json; overlays have no index-addressed patches.
  • [ ] CI renders, validates, policy-checks and diffs every change.
  • [ ] No plaintext Secrets in Git; generated names are excluded from drift noise.
  • [ ] HPA-managed workloads do not set replicas in the base.
  • [ ] Global flags such as --enable-helm are a recorded decision.

Frequently Asked Questions

Is Kustomize better than Helm for GitOps?

Neither is universally better. Kustomize suits manifests your team owns, because the base is plain YAML and overlays express small, reviewable edits. Helm suits distributing or consuming software across teams, because a versioned chart and values contract defines the interface. Both work with Argo CD and Flux, and many production repositories combine them, using charts for upstream components and Kustomize patches or post-renderers for local modifications.

Can you use Helm and Kustomize together?

Yes, in three ways. Kustomize’s helmCharts generator inflates a chart so patches can edit the output, which in Argo CD requires --enable-helm in the Kustomize build options. Flux’s HelmRelease supports postRenderers that apply Kustomize patches after rendering. Or render the chart in CI, commit the result, and treat it as a plain Kustomize base.

Does Argo CD run helm install?

No. Argo CD runs helm template to render the chart and then applies the resulting manifests itself, so Helm does not keep a release record or manage the life cycle. Helm hooks are mapped to Argo CD hooks where possible: pre-install and pre-upgrade become PreSync, post-install and post-upgrade become PostSync. Helm test and rollback hooks are unsupported, and install and upgrade cannot be distinguished.

How does Flux handle Kustomize and Helm differently?

Flux uses separate controllers. A Kustomization object makes kustomize-controller build and apply a path, with patches, components, health checks, pruning and SOPS decryption. A HelmRelease makes helm-controller perform a real Helm install or upgrade, with remediation, rollback, drift detection and values merged from multiple sources. You choose per workload which object fits.

Is Helm 4 supported by Kustomize, Argo CD and Flux?

Per the sources I checked: Kustomize v5.8.1 added Helm v4 support alongside v3; the current Argo CD docs say that from v3.5 charts are rendered with Helm v4 only; and Flux’s HelmRelease documents Helm 4 defaults such as server-side apply and kstatus waiting, with a UseHelm3Defaults gate to restore older behaviour. Verify your exact versions against release notes before upgrading.

How should I handle secrets with Kustomize and Helm?

Do not commit plaintext. Under Flux, kustomize-controller can decrypt SOPS-encrypted files using age, OpenPGP or cloud KMS keys. Under Argo CD, use an external secrets operator, Sealed Secrets or a plugin. Prefer committing only references that an in-cluster operator resolves, so rotation does not require a redeploy and a leaked repository exposes no values.

Further Reading

Related posts on this site:

Primary sources:

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *