Kustomize
Kustomize is a template-free, overlay-based tool for customizing Kubernetes manifests, built directly into
kubectlas the-kflag (kubectl apply -k ./overlay) and also shippable as a standalonekustomizebinary (kustomize.io). Its founding design choice is the inverse of Helm’s: where Helm parameterizes manifests with a Go-templating language, Kustomize never introduces a templating language at all. Instead it treats plain, valid Kubernetes YAML as the input and applies a series of structured transformations — declared in akustomization.yamlfile — that merge into and patch those resources. Because the inputs and outputs are always real Kubernetes objects (never half-rendered text), every intermediate state is parseable, schema-checkable, andkubectl diff-able. The headline use case is the base + overlay pattern: abase/directory holds the canonical manifests, and per-environmentoverlays/dev|staging|prod/directories each patch that base into an environment-specific variant — eliminating the copy-paste duplication that YAML Sprawl Anti-Pattern describes. This note covers thekustomization.yamlfield set, the transformation catalog, the generators, and the bases/overlays architecture. The recurring “which one should I use” decision lives in Helm vs Kustomize; the philosophy Kustomize embodies is elaborated in Declarative vs Imperative Configuration.
Mental Model
flowchart TB subgraph BASE["base/"] BK["kustomization.yaml<br/>resources: deployment.yaml, service.yaml"] BD["deployment.yaml (plain, valid K8s YAML)"] BS["service.yaml (plain, valid K8s YAML)"] BK --> BD BK --> BS end subgraph PROD["overlays/prod/"] PK["kustomization.yaml<br/>resources: ../../base<br/>namePrefix: prod-<br/>replicas: [{name: web, count: 10}]<br/>images: [{name: web, newTag: v2.7}]<br/>patches: [prod-resources.yaml]<br/>configMapGenerator: [...]"] end subgraph DEV["overlays/dev/"] DK["kustomization.yaml<br/>resources: ../../base<br/>namePrefix: dev-<br/>replicas: [{name: web, count: 1}]"] end BASE --> PK BASE --> DK ENGINE{{"kustomize build<br/>(merge + transform, NO templating)"}} PK --> ENGINE OUT["fully-resolved manifests<br/>→ kubectl apply"] ENGINE --> OUT
What this diagram shows. A base/ directory is a complete, deployable-on-its-own set of plain Kubernetes manifests plus a kustomization.yaml listing them under resources. Each environment is an overlay — a directory whose kustomization.yaml lists the base in its resources field and then layers transformations on top: a namePrefix, an images tag override, a replicas count, a strategic-merge patch, a generated ConfigMap. kustomize build ./overlays/prod (or kubectl apply -k) recursively flattens the base into a list of objects and applies the overlay’s transformations to produce fully-resolved manifests. The insight to extract: there is no substitution step and no template language anywhere in this picture. The base YAML is real YAML; the overlay output is real YAML; the transformation is a structured merge of one valid object tree into another. This is what Kustomize means by “template-free” — and it is the property that makes the output impossible to malform at the syntax level.
Mechanical Walk-through
kustomization.yaml — the control file
A directory becomes a kustomization by containing a kustomization.yaml. Its fields fall into four families (kubectl.docs.kubernetes.io — Kustomization):
Resources — resources lists the inputs: paths to plain manifest files or paths to other directories that are themselves kustomizations (this recursion is what makes the base/overlay nesting work). A kustomization is “recursively built into a flat list of KRM objects.”
Generators — fields that create new resources:
configMapGenerator— builds ConfigMaps from literal key-values,.envfiles, or whole files.secretGenerator— the same for Secrets.helmCharts— inflates a Helm chart and folds its output into the resource list (the “Helm inflator”).
Transformers — fields that modify existing resources. The convenience transformers:
namePrefix/nameSuffix— prepend/append a string to every resource’smetadata.name.namespace— setmetadata.namespaceon every resource.commonLabels/labels— add labels to every resource.commonLabelsalso adds them to selectors and template labels (which can be dangerous — see Failure Modes);labelsis the newer field that lets you opt out of selector modification.commonAnnotations— add annotations to every resource.images— override a container image’snewName,newTag, ordigestwithout touching the Deployment YAML.replicas— override the replica count of a named workload.patches— apply strategic-merge or JSON6902 patches (see below).
Validators — transformers that check resources without mutating them.
The two patch dialects
patches is the general-purpose modification mechanism, and it accepts two patch dialects:
- Strategic-merge patch — a partial YAML object that is merged into the target by the same K8s strategic-merge rules
kubectl applyuses (lists of containers merged byname, etc., per Declarative vs Imperative Configuration). You write the fragment of the object you want to change. Historically declared under the now-legacypatchesStrategicMergefield; modern Kustomize folds it intopatches. - JSON6902 patch — an RFC 6902 JSON Patch: an explicit list of
add/remove/replaceoperations on JSON paths. Precise, surgical, the right tool for list-element edits a strategic merge can’t express. HistoricallypatchesJson6902; also folded intopatches.
A patches entry has a path (or inline patch) plus a target selector (by kind, name, labelSelector, etc.), so one patch can hit many resources.
Generators and the content-hash suffix
configMapGenerator and secretGenerator do more than build a ConfigMap/Secret — they append a content-based hash suffix to the generated object’s name (e.g. app-config-7t9k2m4f8d), and Kustomize then rewrites every reference to that ConfigMap/Secret (in Deployment env-from, volume mounts) to point at the hashed name (kubectl.docs.kubernetes.io — configMapGenerator). The payoff is automatic rollout-on-config-change: when the ConfigMap content changes, its hash changes, its name changes, the Deployment’s reference changes, the Deployment’s Pod template changes — and the Deployment controller triggers a rolling update to pick up the new config. With a hand-written ConfigMap, editing it does not restart the Pods consuming it; with a generator, it does. generatorOptions can disable the suffix (disableNameSuffixHash: true) or attach common labels to all generated objects.
Bases, overlays, and components
The architectural pattern:
- A base is a kustomization directory that is self-sufficient — it could be
kubectl apply -k-ed directly. It holds the canonical, environment-neutral manifests. - An overlay is a kustomization whose
resourcespoints at the base and which adds environment-specific transformations. Overlays do not duplicate the base; they reference and patch it. - A component (kubectl.docs.kubernetes.io — Components) is a reusable optional slice of configuration (
apiVersion: kustomize.config.k8s.io/v1alpha1,kind: Component) that multiple overlays cancomponents:-include — e.g. a “with-monitoring” or “with-mTLS” feature module. Components solve the combinatorial-explosion problem that pure overlays have when features are orthogonal.
Configuration / API Surface
A base and a prod overlay, with line-by-line commentary:
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: # the plain manifests this base is composed of
- deployment.yaml
- service.yaml
commonLabels:
app.kubernetes.io/name: web # added to every resource AND its selector/template labels# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base # the base is just another "resource" — recursion in action
namespace: web-prod # every resource gets metadata.namespace: web-prod
namePrefix: prod- # every metadata.name gains a "prod-" prefix
nameSuffix: -v2 # ...and a "-v2" suffix
commonAnnotations:
team: payments # annotation added to every resource
labels: # newer label field — does NOT touch selectors by default
- pairs:
environment: production
includeSelectors: false
images:
- name: registry.example.com/web # match containers using this image...
newTag: "2.7.1" # ...and rewrite the tag (no edit to deployment.yaml needed)
replicas:
- name: web # override the "web" workload's replica count...
count: 10 # ...to 10 (the base may say 1)
patches:
- target: # select what to patch...
kind: Deployment
name: web
patch: |- # ...inline strategic-merge patch: only the changed fragment
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 2Gi
configMapGenerator:
- name: web-config # generates ConfigMap "web-config-<contenthash>"
literals:
- LOG_LEVEL=info
- FEATURE_X=on
# references to web-config in the Deployment are auto-rewritten
# to the hashed name → config change forces a rollout
secretGenerator:
- name: web-tls
files:
- tls.crt=secrets/prod-tls.crt
- tls.key=secrets/prod-tls.key
generatorOptions:
disableNameSuffixHash: false # keep the content-hash suffix (the default; the useful behavior)Rendering and applying:
kustomize build ./overlays/prod # render to stdout — output is fully-resolved plain YAML
kubectl apply -k ./overlays/prod # render + apply in one step (kustomize is built into kubectl)
kubectl diff -k ./overlays/prod # preview the drift before applyingThe decisive property: every line of the output of kustomize build is plain Kubernetes YAML. There is no {{ }}, no rendering step that can produce malformed text — the merge engine operates on parsed object trees, so a structurally invalid output is not expressible.
Failure Modes
-
commonLabelsmutating an immutable selector.commonLabelsadds the label to resources and to their selectors and Pod templates. A Deployment’sspec.selectoris immutable after creation — so adding acommonLabelto an already-deployed Deployment produces afield is immutablerejection on the next apply. Mitigation: use the newerlabelsfield withincludeSelectors: falsefor labels that should not touch selectors; reservecommonLabelsfor green-field resources. -
Generator hash churn surprising operators. Because
configMapGeneratorrenames the ConfigMap on every content change,kubectl get configmapshows a new object each deploy and orphaned old ones accumulate.kubectl apply -kprunes them only if--pruneis configured. Mitigation: GitOps tools (ArgoCD, Flux) handle the pruning; rawkubectl apply -kneeds explicit prune setup. -
Patch target matches nothing. A
patchesentry whosetargetselector matches no resource is silently a no-op in older Kustomize versions — the intended change just doesn’t happen. Mitigation: render withkustomize buildand diff the output; newer versions error on unmatched targets. -
JSON6902 path into a non-existent list index. A JSON-patch
replaceat/spec/template/spec/containers/2/...when only two containers exist fails the build. JSON patches are positional and brittle against base changes; strategic-merge patches (matched byname) survive base reordering. -
No parameter, only patch. Kustomize has no notion of a free-form input variable. Configuration that genuinely varies value-by-value across many dimensions (a hostname per tenant across 50 tenants) forces 50 overlays — awkward where Helm’s
--setis one flag. This is the central trade-off explored in Helm vs Kustomize. -
Base not self-sufficient. A “base” that itself depends on an overlay’s patch to be valid (e.g. references a ConfigMap only the overlay generates) is not a real base. Bases must be independently buildable.
Alternatives and When to Choose Them
- Helm — choose when you need packaging, distribution, versioning, a release lifecycle with rollback, or the public chart ecosystem. Helm’s templating handles high-cardinality value variation that Kustomize’s patching handles awkwardly. See Helm vs Kustomize.
- Raw
kubectl apply -f— fine for a tiny static manifest set; Kustomize earns its keep the moment you have more than one environment. - Jsonnet / Tanka, CDK8s, Pulumi — programmatic generation; more expressive than either Helm templates or Kustomize patches, at the cost of a real toolchain and build step.
- Helm + Kustomize together —
kustomize buildcan inflate a Helm chart viahelmCharts, and converselyhelm template | kubectl apply -kpost-processes Helm output with Kustomize. Common in production; detailed in Helm vs Kustomize.
Production Notes
- Kustomize was merged into
kubectlin v1.14 (the-kflag), which is why “no install needed” is a real advantage — every cluster operator already has it. The standalonekustomizebinary tracks newer features than thekubectl-embedded copy, so teams pinning behavior often install the standalone binary. - ArgoCD and Flux both treat Kustomize as a first-class source type. ArgoCD auto-detects a
kustomization.yamland runskustomize build; Flux has a dedicatedKustomizationCRD and kustomize-controller. The base/overlay layout maps cleanly onto a Git repo with one overlay directory per cluster — the canonical GitOps repository shape. - The “kubectl-native, no extra tool” argument is Kustomize’s strongest adoption driver in security-conscious shops: nothing new to vet, nothing new to keep patched, no templating DSL for reviewers to learn. The output of a Kustomize PR is reviewable as plain YAML diff.
- CNCF: Kustomize is maintained by Kubernetes SIG-CLI (
kubernetes-sigs/kustomize), so it is part of the Kubernetes project proper rather than a separate ecosystem project.
See Also
- Helm — the templating-and-packaging alternative
- Helm vs Kustomize — the recurring comparison and the “use both” reality
- Helm Chart Anatomy — what a Helm chart’s internals look like by contrast
- Declarative vs Imperative Configuration — the strategic-merge semantics Kustomize patches reuse
- GitOps — the base/overlay layout is the canonical GitOps repo shape
- ArgoCD / Flux — GitOps engines that build Kustomize overlays natively
- Deployment — generator hash suffixes drive Deployment rollouts on config change
- YAML Sprawl Anti-Pattern — the duplication problem overlays exist to fix
- Kubernetes MOC — umbrella index (§15 Application Lifecycle and Delivery)