Verified13 commandsAI-assisted

kustomize

.md

Verified against kustomize v5.8.1 (standalone), flags verified via `kustomize --help` / `kustomize · official docs

What it is and where it fits 🎯#

kustomize customizes plain Kubernetes YAML without a templating language — no {{ }} placeholders, no values file, no Go template syntax to learn. Instead you write a kustomization.yaml that points at a set of real, valid manifests (a "base") and layers declarative patches, generators, and field overrides ("overlays") on top of them. kubectl itself embeds an older kustomize under kubectl apply -k, but the standalone kustomize binary this page covers moves faster and is worth installing separately — this sandbox's kubectl (v1.34.0) embeds kustomize v5.7.1, while the standalone binary installed fresh today is already v5.8.1; kubectl's bundled version reliably lags the latest release. The natural point of comparison is Helm: Helm is a templating and packaging system with versioned releases and a templating language for genuinely variable logic (loops, conditionals); kustomize is patch-based composition for environment-to-environment variation on manifests that are otherwise the same YAML you'd hand-write anyway. Many GitOps setups (Argo CD, Flux) support both natively, and plenty of teams use Helm to package an app and a kustomize overlay on top of the rendered chart output for last-mile, environment-specific tweaks.

How a base and overlays combine#

Diagram

The base never changes between environments — every overlay references the same base manifests and only adds patches on top, which is what keeps staging and production from drifting apart on anything the overlays don't deliberately override.

Installation#

curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
brew install kustomize                                # macOS/Linux Homebrew
go install sigs.k8s.io/kustomize/kustomize/v5@latest    # if you have a Go toolchain

kustomize version
kubectl version --client -o yaml | grep -i kustomize    # check what version kubectl -k is actually running

Note

The install script always fetches the latest release — if you need to pin a specific version for reproducibility, download the matching tarball directly from the kustomize releases page instead.

Building a base#

mkdir -p base && cd base
# deployment.yaml — a normal, complete, valid Kubernetes manifest, nothing kustomize-specific in it
kustomize create --resources deployment.yaml,service.yaml

kustomize create writes the starter kustomization.yaml for you — a base is genuinely just "a directory with a kustomization.yaml listing which real manifests it owns," not a special file format.

Creating an overlay#

Real commands run in this sandbox against a base with one Deployment (catalog-api), producing a real overlays/production/kustomization.yaml:

mkdir -p overlays/production && cd overlays/production
kustomize create --resources ../../base
kustomize edit add label "env:production"
kustomize edit set image registry.example.com/catalog-api=registry.example.com/catalog-api:1.4.2
kustomize edit set replicas catalog-api=3

Resulting file (captured verbatim from that run):

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: registry.example.com/catalog-api
  newName: registry.example.com/catalog-api
  newTag: 1.4.2
replicas:
- count: 3
  name: catalog-api
labels:
- includeSelectors: true
  pairs:
    env: production

Sample output — a real kustomize build run#

Running kustomize build overlays/production against the files above (real captured output, not illustrative):

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    env: production
  name: catalog-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: catalog-api
      env: production
  template:
    metadata:
      labels:
        app: catalog-api
        env: production
    spec:
      containers:
      - image: registry.example.com/catalog-api:1.4.2
        name: catalog-api
        ports:
        - containerPort: 8080

Every override applied cleanly: the image tag, the replica count, and the env: production label — including on the Deployment's own selector.matchLabels, which includeSelectors: true deliberately propagates into so the label actually participates in Pod selection, not just metadata.

Patches: strategic merge vs. JSON 6902#

kustomize edit add patch --path memory-patch.yaml --kind Deployment --name catalog-api    # strategic-merge-style patch file
kustomize edit add patch --path add-toleration.json --kind Deployment --name catalog-api --patch-type json    # RFC 6902 JSON patch
# memory-patch.yaml — a strategic merge patch: only the fields you name are touched
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
spec:
  template:
    spec:
      containers:
        - name: catalog-api
          resources:
            limits:
              memory: 512Mi
[
  { "op": "add", "path": "/spec/template/spec/tolerations", "value": [{ "key": "spot", "operator": "Exists" }] }
]

Important

patchesStrategicMerge and patchesJson6902 are deprecated in favor of a single unified patches: field — confirmed live: running kustomize edit fix against a kustomization still using the old field names rewrote them automatically, printing 'commonLabels' is deprecated. Please use 'labels' instead and folding both old patch fields into one patches: list. A kustomization authored a couple of years ago that still uses the old field names still works today, but new kustomizations should use patches: (with target: selecting the resource, and either inline YAML or patch: | strategic-merge content, or path:) from the start.

Generators: ConfigMaps and Secrets from real files#

kustomize edit add configmap app-config --from-literal=LOG_LEVEL=info --from-file=app.properties
kustomize edit add secret db-creds --from-literal=DB_PASSWORD=changeme --disable-name-suffix-hash
configMapGenerator:
  - name: app-config
    literals:
      - LOG_LEVEL=info
    files:
      - app.properties
secretGenerator:
  - name: db-creds
    literals:
      - DB_PASSWORD=changeme
    options:
      disableNameSuffixHash: true

Warning

By default, generated ConfigMaps/Secrets get a content hash suffix appended to their name (app-config-8f7d9c2b), and kustomize automatically rewrites every reference to that name across the other manifests it builds. This is a deliberate feature — it forces a new Pod rollout whenever the ConfigMap's content changes, since Kubernetes itself doesn't restart Pods on a ConfigMap edit alone. Using --disable-name-suffix-hash (or a hardcoded name a Deployment references directly) trades that automatic rollout-on-change behavior away — only do it deliberately, for something like a Secret an external operator manages by fixed name.

Config file format — kustomization.yaml field reference#

FieldPurpose
resourcesManifest files, directories, or a base path to include
patchesStrategic-merge or JSON 6902 patches, targeted by target: selectors
imagesOverride an image's name/tag/digest across every resource that references it
replicasOverride a workload's replica count by resource name
labels / commonAnnotationsAdd labels/annotations to every generated resource (labels replaces the deprecated commonLabels)
namePrefix / nameSuffixPrepend/append a string to every resource's metadata.name
namespaceForce every resource into one namespace, overriding whatever the base set
configMapGenerator / secretGeneratorGenerate ConfigMaps/Secrets from literals or files, with automatic hash-suffix rollout behavior
componentsReusable, optionally-included chunks of kustomization logic (like a mixin)
vars (legacy) / replacementsCopy a field's value from one resource into another at build time — replacements is the current, more explicit mechanism

Real-world scenario: promoting a change from staging to production#

A team keeps overlays/staging/ and overlays/production/ as siblings sharing one base/ — promoting a change means editing the base once and reviewing what each overlay's build output actually changes, rather than copy-pasting YAML between environment folders:

kustomize build overlays/staging > /tmp/staging-before.yaml
# ... edit base/deployment.yaml ...
kustomize build overlays/staging > /tmp/staging-after.yaml
diff /tmp/staging-before.yaml /tmp/staging-after.yaml    # confirm exactly what changed, before it ships
kustomize build overlays/production | kubectl apply -f -

Real-world scenario: GitOps with Argo CD or Flux, no CI render step at all#

Both Argo CD and Flux understand kustomization.yaml natively — pointing an Argo CD Application or a Flux Kustomization resource at overlays/production/ means the GitOps controller runs the equivalent of kustomize build itself on every reconciliation loop, continuously, without a CI pipeline step rendering and pushing manifests anywhere. This is the single biggest reason teams standardize on kustomize's patch-based model over a bespoke templating pipeline — the exact same directory a human runs kustomize build against locally is what the GitOps controller reconciles against in the cluster.

Real-world scenario: pinning every image in a PR without hand-editing YAML#

A release pipeline needs to bump every service's image tag to a freshly-built SHA without touching the actual Deployment manifests, so a reviewer's diff shows exactly one line per service:

cd overlays/production
kustomize edit set image catalog-api=registry.example.com/catalog-api@sha256:abc123...
git diff kustomization.yaml    # a one-line diff, not a YAML rewrite

CI/CD integration: render-and-diff in a pull request#

# .github/workflows/kustomize-preview.yml
name: Kustomize preview
on: [pull_request]
jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install kustomize
        run: |
          curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
          sudo mv kustomize /usr/local/bin/
      - name: Render production overlay
        run: kustomize build overlays/production > rendered.yaml
      - name: Upload rendered manifest as an artifact
        uses: actions/upload-artifact@v4
        with:
          name: rendered-manifests
          path: rendered.yaml

Tip

Uploading the rendered output as a build artifact (rather than just running build and discarding the result) gives reviewers something concrete to diff against the previous merge's rendered output — catching an unintended change to a resource nobody meant to touch, which a diff of the source kustomization.yaml alone can't show.

Common pitfalls#

  • Trusting kubectl apply -k to match the standalone kustomize CLI's behavior exactly — see the version-lag note in "What it is and where it fits"; a feature or bug fix in a newer standalone release may not exist yet in kubectl's embedded copy.
  • Still using patchesStrategicMerge/patchesJson6902/commonLabels — all deprecated in favor of patches:/labels:; run kustomize edit fix to migrate a kustomization automatically.
  • Not realizing generated ConfigMap/Secret names get a hash suffix by default — see the WARNING above; this breaks anything hardcoding the un-suffixed name.
  • A replicas/images/patch target name that doesn't match any actual resourcekustomize build fails outright with an error naming the mismatched GVK/name rather than silently no-op'ing, which is easy to misread as a real bug when it's really just a typo in the overlay.
  • Forgetting resources: in an overlay must point at the base directory, not individual base files — omitting the base entirely produces a build with only the overlay's own generators/patches and nothing to apply them to.

Exit codes and when to reach for something else#

0 on a successful build/edit; non-zero (with a descriptive error naming the missing resource/GVK/field) on anything that fails to resolve.

Reach for Helm instead when the actual variation between deployments needs real logic — conditionals, loops over a list, computed values, or a chart meant for other teams/the public to consume with their own values.yaml. Reach for kustomize when every environment is fundamentally the same manifests with a bounded set of field-level differences, and when the team wants to review plain YAML in pull requests rather than a templating language. The two aren't mutually exclusive — kustomize build --enable-helm can even inflate a Helm chart as one more resource for kustomize to patch, for teams standardizing on kustomize as the outer layer regardless of what a given component ships as internally.