Kustomize

Kustomize is a template-free, overlay-based tool for customizing Kubernetes manifests, built directly into kubectl as the -k flag (kubectl apply -k ./overlay) and also shippable as a standalone kustomize binary (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 a kustomization.yaml file — 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, and kubectl diff-able. The headline use case is the base + overlay pattern: a base/ directory holds the canonical manifests, and per-environment overlays/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 the kustomization.yaml field 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):

Resourcesresources 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, .env files, 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’s metadata.name.
  • namespace — set metadata.namespace on every resource.
  • commonLabels / labels — add labels to every resource. commonLabels also adds them to selectors and template labels (which can be dangerous — see Failure Modes); labels is the newer field that lets you opt out of selector modification.
  • commonAnnotations — add annotations to every resource.
  • images — override a container image’s newName, newTag, or digest without 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 apply uses (lists of containers merged by name, etc., per Declarative vs Imperative Configuration). You write the fragment of the object you want to change. Historically declared under the now-legacy patchesStrategicMerge field; modern Kustomize folds it into patches.
  • JSON6902 patch — an RFC 6902 JSON Patch: an explicit list of add/remove/replace operations on JSON paths. Precise, surgical, the right tool for list-element edits a strategic merge can’t express. Historically patchesJson6902; also folded into patches.

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 resources points 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 can components:-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 applying

The 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

  1. commonLabels mutating an immutable selector. commonLabels adds the label to resources and to their selectors and Pod templates. A Deployment’s spec.selector is immutable after creation — so adding a commonLabel to an already-deployed Deployment produces a field is immutable rejection on the next apply. Mitigation: use the newer labels field with includeSelectors: false for labels that should not touch selectors; reserve commonLabels for green-field resources.

  2. Generator hash churn surprising operators. Because configMapGenerator renames the ConfigMap on every content change, kubectl get configmap shows a new object each deploy and orphaned old ones accumulate. kubectl apply -k prunes them only if --prune is configured. Mitigation: GitOps tools (ArgoCD, Flux) handle the pruning; raw kubectl apply -k needs explicit prune setup.

  3. Patch target matches nothing. A patches entry whose target selector matches no resource is silently a no-op in older Kustomize versions — the intended change just doesn’t happen. Mitigation: render with kustomize build and diff the output; newer versions error on unmatched targets.

  4. JSON6902 path into a non-existent list index. A JSON-patch replace at /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 by name) survive base reordering.

  5. 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 --set is one flag. This is the central trade-off explored in Helm vs Kustomize.

  6. 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 togetherkustomize build can inflate a Helm chart via helmCharts, and conversely helm template | kubectl apply -k post-processes Helm output with Kustomize. Common in production; detailed in Helm vs Kustomize.

Production Notes

  • Kustomize was merged into kubectl in v1.14 (the -k flag), which is why “no install needed” is a real advantage — every cluster operator already has it. The standalone kustomize binary tracks newer features than the kubectl-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.yaml and runs kustomize build; Flux has a dedicated Kustomization CRD 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