kustomize
.mdVerified 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#
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 runningNote
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.yamlkustomize 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=3Resulting 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: productionSample 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: 8080Every 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-hashconfigMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
files:
- app.properties
secretGenerator:
- name: db-creds
literals:
- DB_PASSWORD=changeme
options:
disableNameSuffixHash: trueWarning
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#
| Field | Purpose |
|---|---|
resources | Manifest files, directories, or a base path to include |
patches | Strategic-merge or JSON 6902 patches, targeted by target: selectors |
images | Override an image's name/tag/digest across every resource that references it |
replicas | Override a workload's replica count by resource name |
labels / commonAnnotations | Add labels/annotations to every generated resource (labels replaces the deprecated commonLabels) |
namePrefix / nameSuffix | Prepend/append a string to every resource's metadata.name |
namespace | Force every resource into one namespace, overriding whatever the base set |
configMapGenerator / secretGenerator | Generate ConfigMaps/Secrets from literals or files, with automatic hash-suffix rollout behavior |
components | Reusable, optionally-included chunks of kustomization logic (like a mixin) |
vars (legacy) / replacements | Copy 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 rewriteCI/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.yamlTip
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 -kto match the standalonekustomizeCLI'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 ofpatches:/labels:; runkustomize edit fixto 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/patchtargetname that doesn't match any actual resource —kustomize buildfails 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.