Kustomize: Kubernetes Configuration Without the Template Sprawl of Helm
Plain YAML, patched and overlaid, instead of a templating language bolted onto YAML

Contents
There’s a moment, somewhere around your third environment, when a Helm chart stops being a convenience and starts being a small programming language you didn’t ask to learn. You’re staring at {{- if .Values.ingress.enabled }} nested four levels deep, whitespace-sensitive, debugged by squinting at helm template output. The thing you wanted was “the same manifests as production, but with a different replica count and hostname.” The thing you got was Go templating wrapped around YAML, which is a syntax wrapped around a syntax, and your editor can’t help you with either.
Kustomize takes a different bet. Your manifests stay plain, valid, schema-checkable Kubernetes YAML. To vary them, you overlay — you patch the base. No template language, no {{ }}, no rendering step that turns text into more text and hopes it parses. I came to it grudgingly and stayed happily.
The base-and-overlays idea
You write your real manifests once, in a base directory. Then each environment is a thin overlay that references the base and describes only its differences. The mental model is inheritance, not substitution.
| |
The key insight is that the base is complete and deployable on its own. You can apply the base directly, with no overlay, and get a working application. The overlays don’t fill in blanks — there are no blanks — they amend something that already stands up. That’s the opposite of a Helm chart, where the bare templates are inert until you feed them values. It also means you can review the base in isolation and trust that what you read is what runs.
The base’s kustomization.yaml just lists the resources:
| |
And production overlays its changes on top:
| |
That replicas-patch.yaml is a strategic-merge patch — it looks exactly like the deployment, but with only the fields you want to change. Kustomize merges it into the base. No new syntax to learn; if you can write a Deployment, you can write the patch:
| |
That’s the whole file. Kustomize finds the myapp Deployment in the base by kind and name, deep-merges these fields over it, and leaves everything else untouched. If you need surgical, positional edits — remove the third container, add an env var to a specific list index — there’s also a JSON 6902 patch that speaks in explicit op: replace/add/remove paths. Reach for strategic-merge by default; drop to JSON patches only when merge semantics can’t express what you mean (typically list manipulation, where strategic-merge’s behaviour depends on whether the list has a merge key).
It’s built into kubectl
This is the part people miss. You don’t have to install anything. kubectl ships with Kustomize built in:
| |
The standalone kustomize binary is usually a version or two ahead and worth installing for serious use, but for kicking the tyres, the -k flag is already on your machine.
The transformers you’ll actually use
Beyond patches, Kustomize has a handful of built-in transformers that do the boring-but-error-prone rewrites for you. namePrefix/nameSuffix rename every resource so staging and prod can coexist. commonLabels stamps a label across everything, including the selectors, so you don’t desync a Deployment from its Service. images swaps tags without you hunting through YAML. And configMapGenerator builds a ConfigMap from a file or literals, appending a content hash to its name so that changing the config triggers a rolling restart — a genuinely clever touch that solves a problem Helm leaves you to handle yourself.
| |
That content hash is the quietly brilliant part and worth dwelling on. In vanilla Kubernetes, editing a ConfigMap does not restart the pods that mount it — they keep running with the old values in memory until something happens to recreate them, which is a classic “why didn’t my config change take effect?” trap. Because configMapGenerator appends a hash of the content to the name, changing the config produces a new ConfigMap name, which changes the Deployment’s pod template, which is a change the Deployment controller notices — so you get a rolling restart for free, triggered by the config change itself. Helm makes you wire this up by hand with a checksum/config annotation; Kustomize does it because the design falls out that way. There’s a matching secretGenerator for Secrets, though for anything genuinely sensitive you’ll want proper secret encryption on top rather than a plaintext secrets.env — generators solve the restart problem, not the “don’t commit passwords” problem.
A quick note on commonLabels, because it saves a real class of outage: it stamps a label onto every resource and into selectors. That matters because a Deployment and its Service are joined by a label selector, and if you hand-edit labels you can silently desync them — the Service stops matching the pods and traffic black-holes with no error anywhere. Letting Kustomize own the labels keeps the two ends in step by construction.
Components: sharing overlays without copy-paste
The base-and-overlays model has one obvious failure mode: what if two overlays need the same optional feature? Say both staging and production want a Prometheus sidecar, but dev doesn’t. Copy-pasting the patch into two overlays is exactly the duplication Kustomize is meant to kill. The answer is a component — a reusable bundle of resources and patches that an overlay opts into by listing it:
| |
Each component is itself a little kustomization with kind: Component, and it can carry its own resources, patches, and generators. This is how you keep a growing cluster’s config from turning into a fractal of near-identical files: factor the shared bits into components, and let each environment compose the set it needs. It’s the closest Kustomize gets to Helm’s optional-feature toggles, but it stays plain YAML the whole way down — you’re assembling files, not evaluating conditionals.
The mental discipline that makes all of this work is keeping the base genuinely minimal and genuinely deployable. Every field you push down into the base is a field every overlay inherits whether it wants to or not, so the base should hold only what’s truly universal, and everything environment-specific belongs in an overlay or a component. Get that split right and the diffs stay small, the review stays legible, and adding a fourth environment is a five-line file rather than a copy-paste marathon.
Where it’s weaker than Helm
Honesty time. Kustomize has no notion of conditionals or loops, and that’s deliberate — but it means there’s no neat way to say “create this Ingress only if enabled” or “generate N near-identical objects.” You end up with more overlay files instead of fewer template branches, which is a trade, not a free lunch. It has no packaging or distribution story: there’s no equivalent of a Helm registry to helm install someone-elses-thing. And many third-party projects ship only a Helm chart, so if you want their software you’re consuming Helm whether you like it or not. The pragmatic answer is to use Kustomize for your config and Helm for other people’s packaged apps — and Kustomize can even post-render Helm output, so the two aren’t strictly enemies.
Troubleshooting the overlay model
Kustomize fails in quieter ways than Helm — no stack traces, just output that isn’t what you expected — so learn to diagnose by rendering.
“My patch didn’t apply.” The commonest cause is a name mismatch. A strategic-merge patch targets by kind plus metadata.name, and if your overlay sets namePrefix: prod-, the base object is still named myapp at patch time — patches run before the prefix transformer, so target the un-prefixed name. When in doubt, render and grep:
| |
“replicas keeps snapping back.” If a HorizontalPodAutoscaler owns the replica count at runtime, hard-coding replicas in a patch fights it on every reconcile. Either let the HPA win (omit replicas from the patch) or you’ll watch the two ping-pong forever — a genuinely maddening bug that looks like Kustomize misbehaving when it’s doing exactly as told.
“configMapGenerator broke my mounts.” The name-hash suffix that triggers rolling restarts also means the ConfigMap isn’t called app-config any more — it’s app-config-9f8h2k. Kustomize rewrites references inside resources it manages, but anything referring to the ConfigMap by a hard-coded name from outside the kustomization won’t get rewritten and will mount nothing. Keep generated ConfigMaps referenced only through Kustomize-managed resources.
The golden rule: when something’s off, run kubectl kustomize <overlay> and read the actual rendered YAML before you touch anything. Nine debugging sessions in ten end the moment you look at what’s really being produced instead of what you assumed. That habit of inspecting the real artefact is the same discipline that keeps Podman-based container work sane without Docker’s daemon — see the actual thing, not the abstraction you imagine.
If your manifests are basically the same across environments with a handful of differences — replica counts, hostnames, image tags, resource limits — Kustomize is the calmer choice, and it’s already in your kubectl. You keep real YAML that your editor and CI can validate, you avoid the template-debugging tax, and the diffs in code review are legible. If you genuinely need conditionals, loops, or to distribute an application for strangers to install, Helm earns its complexity. Most internal platform teams want the first thing and reach reflexively for the second. Try the overlay model before you commit to learning a templating language — you may find you never needed it.
Who is this for, concretely? Anyone running a small-to-medium cluster where you own the manifests: a homelab, an internal platform, a handful of services across dev/staging/prod. It pairs beautifully with a learning cluster — the K3s-on-a-Raspberry-Pi setup is the perfect place to practise base-and-overlay without risking anything real. If you’re a vendor shipping software for other people to install, or you truly can’t express your config without conditionals, this isn’t your tool and that’s fine. For everyone in between, the plain-YAML overlay is the quieter, more auditable path, and it costs you nothing to try because it’s already installed.




