Kustomize, three years later

Revisiting the friction points of Kustomize without the rant energy, and what I actually use it for now.

I wrote a post in 2023 about hating Kustomize. Reading it back, the points still hold, but the tone is louder than I want my older posts to be. So here is the same idea, calmer, with three more years of using the tool in production.

Short version: Kustomize is fine. It is the marketing around it that I struggle with. The tool does a specific thing well enough, but the docs keep selling it as the answer to problems it does not actually solve.

The “template-free” claim

The landing page still opens with this:

Kustomize lets you customize raw, template-free YAML files for multiple purposes, leaving the original YAML untouched and usable as is.

Kustomize does leave the original file alone on disk. It then renders a different file with your patches applied. That is templating with extra steps and a different vocabulary. The output is not the input. Calling it template-free is a positioning choice, not a technical one.

The friction shows up when you actually patch something. A simple ingress edit looks like this with patchesJson6902:

[
  { "op": "replace", "path": "/spec/rules/0/host", "value": "foo.bar.io" },
  {
    "op": "replace",
    "path": "/spec/rules/0/http/paths/0/backend/servicePort",
    "value": 80
  },
  {
    "op": "add",
    "path": "/spec/rules/0/http/paths/1",
    "value": { "path": "/healthz", "backend": { "servicePort": 7700 } }
  }
]

For multi-object patches you trade JSON pointer syntax for a target selector:

patches:
  - path: <relative path to file containing patch>
    target:
      group: <optional group>
      version: <optional version>
      kind: <optional kind>
      name: <optional name or regex pattern>
      namespace: <optional namespace>
      labelSelector: <optional label selector>
      annotationSelector: <optional annotation selector>

This is a fine API. It is not less mental overhead than a Helm template. You still need to track which field is patched where, and the patch lives in a separate file that only makes sense in context. The “no templates” pitch promises a simpler model than what you get.

Transformers and generators

The glossary calls Kustomize’s surface area transformers and generators, deliberately not templates. The distinction matters internally because it shapes how the engine composes resources, but from a user’s seat the result is the same. You write YAML that produces different YAML.

What I do appreciate here is that generators have a narrow contract. A ConfigMap generator does one job. A patch transformer does one job. The composition story is cleaner than a Helm chart that uses tpl to template inside a template.

Base and overlay versus templates and values

The docs are firm about this:

sub-target / sub-application / sub-package A sub-whatever is not a thing. There are only bases and overlays.

In practice, a Kustomize base plays the role of a Helm chart’s templates, and an overlay plays the role of a values file. The pattern is not identical, but the analogy is close enough that the docs going out of their way to reject it feels more like positioning than pedagogy.

Where the model genuinely earns its keep is when you have three or four environments that mostly share the same shape and need small, declarative diffs between them. Overlays make those diffs reviewable. That is the use case I keep coming back to.

”Package has no meaning here”

The glossary insists the word package has no meaning in Kustomize because Kustomize is not a package manager. In real repos, the most common shape I see is:

|_ kustomization.yaml
|_ helmrelease.yaml

Where kustomization.yaml is:

kind: Kustomization
namespace: monitoring
resources:
  - ./helmrelease.yaml

And the actual workload comes from a Flux HelmRelease:

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: grafana
  namespace: monitoring
spec:
  interval: 30m
  chart:
    spec:
      chart: grafana
      sourceRef:
        kind: HelmRepository
        name: grafana
  values: {}

So the directory is, functionally, a package. Kustomize is the wrapper, Helm is the engine, and the unit you ship and reason about is the folder. The docs can decline the word, but the shape of the thing in the field is package-like.

This is not a bug. It is just that the abstractions Kustomize gives you compose well with package-shaped things, and that is how teams use them.

Globbing, the long way around

The history of globbing in resources: is the part that still bothers me, less because of the decision and more because of the loop it creates.

The original removal cited a Java blog post on why globs are dangerous. The followup, years later, conceded that Kustomize lives in a Git context, where the worry about what files happen to be on disk does not really apply. The conclusion was that globs would be reasonable now, but maintainer bandwidth made adding them back unlikely. Meanwhile the issue collected thumbs-down emoji and follow-up requests for the better part of a decade.

I do not need globbing in every tool. I do think the rationale should track the tool’s actual deployment model, and “we run inside Git, so the safety argument is weaker than we said” deserves to land somewhere in the docs and the issue tracker, not just in a side comment three years later.

The pattern underneath all of these is the same. Kustomize’s defaults are chosen to avoid problems that, in real GitOps repos, are not the problems I actually have. Refusing globs to keep builds reproducible costs more in everyday hassle than it saves, because the build is already reproducible from a commit SHA. Renaming templates to transformers does not reduce the mental load of patching nested fields. In practice, more of my time goes to working around the defaults than to the failure modes the defaults exist to prevent.

Where I actually use it

I reach for Kustomize sometimes, where it fits. Not as a default, not as the answer to every parametrization question. The cases where I do pull it in:

  • A small set of manifests that need dev, staging, and prod overlays with diffs small enough to review at a glance.
  • Wrapping a Helm release in a folder so Flux or Argo can sync it without me writing a chart. Yes, this is using Kustomize as a package wrapper around a package, after its own docs went out of their way to reject the word package. The shape still fits, even if the glossary disagrees.
  • Setting common labels, namespaces, or image tags across a directory when sed would feel hacky.

Outside those cases I usually pick something else: a Helm chart when the surface area is large, plain YAML when there is only one environment, or a Jsonnet or CUE setup when the logic actually warrants a real language.

Three years ago I called it bad. Reading that post back, I think what I actually meant was that the gap between the marketing and the daily experience was wider than I could shrug off. The gap is still there. The difference now is that I treat Kustomize like any other tool with a specific shape: useful when the shape matches, ignored when it does not, and not worth arguing with the glossary about.