This is part one of three. It covers everything you need to do real work with Kustomize, not a teaser. By the end you can take a plain set of Kubernetes manifests, turn them into a reusable base, and produce a development version and a production version of the same application without copying a single file. You will know how to change images, replica counts, names, namespaces and configuration from one small file, how to patch anything the built-in fields do not cover, how to read the errors the tool prints, and how to preview and apply the result to a cluster. Mid-level and Senior take the same topics further; nothing here is thrown away.
Each section ends with a Try it task. Do them as you go. They take a few minutes each, and this tool only makes sense once you have watched your own YAML go in one end and come out different at the other. You do not need a cluster for most of this guide. Kustomize only reads and writes text, so a laptop and a terminal are enough until the section on applying.
The guide assumes you have seen a Kubernetes manifest before: a YAML file with apiVersion, kind, metadata and spec. If not, read the Kubernetes guide first, or at least its sections on Deployments and Services. You do not need to know Helm. Where the two tools meet, the text explains both sides.
What Kustomize is, and the problem it solves
Kustomize takes Kubernetes YAML files you already have, applies a list of edits you describe in a small file, and prints the resulting YAML. That is the whole tool. It never contacts a cluster, it never stores anything, and it never changes the files you wrote. Files go in, edited text comes out.
The problem it solves appears the first time you deploy the same application to two places. Imagine a web service. In development you want one replica, the namespace web-dev, and whatever image tag the last build produced. In production you want three replicas, the namespace web-prod, a pinned and tested image tag, and a slightly different configuration. The two deployments are 95 percent identical. The question is what to do about the other 5 percent.
The obvious answer is to copy the folder and edit the copy. It works for a week. Then someone adds a health probe to the development copy and forgets production. Someone else fixes a typo in a label in production only. Within a month the two folders have drifted, and nobody can say with confidence what differs between them or why. Kubernetes manifests have no notion of inheritance, so every copy is a fork, and forks diverge.
Kustomize's answer is to keep one copy of the shared part, called the base, and describe each environment as a small set of differences on top of it, called an overlay. The base is ordinary Kubernetes YAML that you could kubectl apply directly. The overlay is a short file that says "take the base, then set the namespace, then change the replica count, then pin the image tag". Because the differences are explicit and short, you can read them in one screen, review them in a pull request, and be sure that a fix to the base reaches every environment at once.
The official documentation describes the idea as template-free customisation, and the phrase matters. Many tools solve the same problem by turning your YAML into a template with placeholders such as {{ .Values.replicas }}. Once you do that, your manifest is no longer valid Kubernetes YAML; it is a program that has to be run before it becomes YAML. Kustomize keeps your files as real, valid, readable manifests. The edits live next to them, not inside them. You can open deployment.yaml in any editor, validate it with any tool, and apply it with plain kubectl at any time.
A useful way to hold both ideas is to compare it with two older tools. Like make, a kustomization is a declaration: you write down what you want and the tool works out the steps. Like sed, the output is your input text with edits applied, and the input is never modified. But unlike sed, Kustomize understands Kubernetes. It knows that a Deployment's name appears in a Service selector, that a ConfigMap name is referenced from a Pod, and that labels belong in particular fields. You describe the intent, and it edits the right places.
Four things that Kustomize does, in the order you will use them in this guide:
Bases and overlays
Share one set of manifests across environments and describe each environment as a short list of differences.
Common changes
Set the namespace, prefix every name, add labels, pin image tags and change replica counts with one line each.
Patches
Change any field of any resource when the built-in fields are not enough, without editing the original file.
Generated configuration
Build ConfigMaps and Secrets from literals and files, with a content hash in the name so changing the data restarts the pods that use it.
It is also built into kubectl. Since Kubernetes 1.14, kubectl apply -k and kubectl kustomize contain a copy of Kustomize, which is why so many teams meet it without ever installing anything. A later section explains the one catch with that copy: it is often older than the standalone tool.
- Think of one application you have deployed, or could deploy, to two environments.
- Write down three things that must differ between the environments (for example replica count, image tag, hostname).
- Write down three things that must stay identical.
What came before: copies, sed and templates
Understanding the alternatives makes the design choices obvious, and it is also a common interview opener. There are four approaches you will meet in real repositories.
Copy and edit. One folder per environment, each containing full manifests. It needs no tooling and is easy to understand, which is why everybody starts here. Its weakness is drift, described above. The cost grows with every file and every environment, and a review of "what is different in production" turns into a manual diff of two directory trees.
Shell scripting with sed or envsubst. Keep one set of manifests with placeholders like $IMAGE_TAG, then run a script that substitutes values before kubectl apply. This removes the copies, but the manifests are no longer valid YAML, the script is one more thing to maintain, and mistakes surface only at apply time. A typo in a sed pattern can silently change the wrong line.
Helm. The most popular Kubernetes packaging tool. A Helm chart contains templates written in Go templating language and a values.yaml file of defaults; you override values per environment. Helm is powerful and is the standard way to distribute software to other people, including most third-party projects such as ingress controllers and databases. Its cost is that templates are a programming language embedded in YAML, which gets hard to read as charts grow. Helm also installs things as numbered "releases" with their own stored state in the cluster. The Helm guide covers it in depth.
Kustomize. No templates, no parameters, no state. The base is real YAML; the edits are declared in a separate file; the output is text.
The two main alternatives make different trade-offs, and it helps to see them side by side:
Kustomize
- Your manifests stay valid YAML
- Edits are declared, not programmed
- No cluster state; output is just text
- Best for your own applications across environments
- No variables, loops or conditionals
Helm
- Templates are not valid YAML until rendered
- Logic through Go templates and values
- Tracks releases and supports rollback
- Best for distributing software to others
- Full programming features, and the complexity that comes with them
Neither side is "better". Teams often use both: Helm for third-party software they install, Kustomize for their own services, and sometimes Kustomize on top of Helm output to patch a chart without forking it. Kustomize can even run Helm for you through a field called helmCharts, which the mid-level guide covers. For this guide, remember only that Kustomize is the tool for the manifests you own.
There is one more honest point in Kustomize's design that surprises newcomers. It deliberately does not have variables. If you want to inject a value such as an image tag from your build system, Kustomize's answer is to run a command that edits the kustomization.yaml file first, then build. You will see that command later in this guide. The authors made this choice on purpose: a build that depends on hidden environment variables is hard to reproduce, whereas a build that depends only on files in git gives the same output every time.
- Find a repository (yours or open source) that deploys to Kubernetes.
- Decide which of the four approaches it uses: copies, scripts, Helm or Kustomize. The clues are folders named after environments, a
Chart.yamlfile, or akustomization.yamlfile. - Note one thing that would be hard to change safely in that approach.
The mental model: five words
Kustomize has a small vocabulary. Learn these five words and every error message and documentation page becomes readable.
| Word | Meaning | Example |
|---|---|---|
| Resource | One Kubernetes object written in YAML | deployment.yaml containing a Deployment |
| Kustomization | A directory with a file named kustomization.yaml that lists resources and edits |
base/kustomization.yaml |
| Base | A kustomization that other kustomizations build on | base/ |
| Overlay | A kustomization that refers to a base and adds its own edits | overlays/prod/ |
| Patch | A partial YAML document (or a list of operations) that changes one resource | replicas-patch.yaml |
Two other terms you will see are transformer and generator. A transformer changes resources that already exist; setting the namespace, adding a label and applying a patch are all transformers. A generator creates new resources; the ConfigMap and Secret generators are the ones you will use. Every convenient field in kustomization.yaml is shorthand for a built-in transformer or generator, so the two words describe the entire set of things Kustomize does.
A kustomization deserves a moment, because the word is used in two ways. The file is called kustomization.yaml (you may also see kustomization.yml or Kustomization). The directory containing it is also called a kustomization, and in the documentation it is also called the root of that kustomization. When a later guide says "the kustomization root", it means the folder that holds the file.
The file itself is a Kubernetes-style object, with apiVersion: kustomize.config.k8s.io/v1beta1 and kind: Kustomization. The version is v1beta1 and, as of Kustomize v5.8.2, there is no v1 yet. It does not describe something that runs in a cluster; it only describes how to build YAML. You will almost always include those two lines at the top for clarity and editor support, but the tool assumes them if you leave them out.
The layering works like this:
Notice that the arrow points one way. An overlay names its base, but the base has no idea that overlays exist. That one-way dependency is what lets you add a third environment without touching anything that already works. It also means you can build the base on its own at any time, which is how you will test it.
Finally, the most important sentence to remember about how the tool runs: kustomize build reads a directory and prints YAML to standard output. It does not apply anything. Applying is a separate step, performed by kubectl or by a GitOps tool. This separation is why you can run builds freely, in any environment, with no fear of changing a cluster.
- Draw a box for a base and two boxes for overlays on paper.
- Draw the arrows. Which way do they point?
- Write one sentence explaining why the base never needs to change when you add a third overlay.
Installing Kustomize and checking the setup
You have two routes: use the copy inside kubectl, or install the standalone kustomize binary. The standalone binary is better for learning, because it is newer and its error messages and flags match this guide. The current release at the time of writing is Kustomize v5.8.2, published on 30 September 2026.
Check whether you already have it through kubectl. If you have kubectl installed, run:
kubectl version --client
The output includes a line such as Kustomize Version: v5.7.1. That is the embedded copy. It is fine for the exercises here, but it lags behind the standalone release: kubectl 1.34 and 1.35 contain v5.7.1, and kubectl 1.36 and 1.37 contain v5.8.1. None contains v5.8.2 yet. Check the version on your machine rather than assuming.
Install the standalone binary. Pick the route for your system.
On macOS with Homebrew:
brew install kustomize
On Windows with Chocolatey (Scoop also has a kustomize package, and you can download the .zip from the GitHub releases page and put kustomize.exe on your PATH):
choco install kustomize
On Linux or macOS with the official install script, which detects your operating system and processor, downloads the binary into the current directory, and refuses to overwrite an existing file:
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
The script accepts a version and a target directory, so you can pin what you install:
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 5.8.2 /usr/local/bin
If you have Go installed, you can build from source. The /v5 at the end of the path is mandatory:
go install sigs.k8s.io/kustomize/kustomize/v5@v5.8.2
If you would rather not install anything, the project publishes a container image, and the entrypoint is the kustomize command itself. Image tags lag the release by a little, so check the registry for the newest one before quoting it:
docker run --rm -v "$PWD":/work -w /work registry.k8s.io/kustomize/kustomize:v5.8.1 build overlays/prod
curl ... | bash executes whatever the URL returns. The script above is published by the Kustomize project and is widely used, but if your employer restricts this pattern, use your package manager or download the binary from the GitHub release page and verify it against the published checksums.txt file.
Verify the installation:
kustomize version
v5.8.2
For more detail, ask for structured output:
kustomize version -o yaml
version: v5.8.2
gitCommit: unknown
buildDate: "2026-09-30T12:53:01Z"
goOs: darwin
goArch: arm64
goVersion: go1.26.5
Your gitCommit, goOs and goArch will differ. The line to check is version. Older releases printed a longer string such as kustomize/v5.0.0; current ones print v5.8.2. If you see a --short flag in an older tutorial, ignore it: that flag is deprecated and prints a warning.
Shell completion saves typing. For zsh, for example, kustomize completion zsh prints a script you can load from your shell profile; bash, fish and powershell work the same way.
If you ever want to know which version is running when kubectl apply -k is used, kubectl version --client answers it. That matters more than it sounds, because if your laptop and your pipeline use different versions, the same directory can build to slightly different YAML. Section "Applying to a cluster" returns to this.
helm program if you use the helmCharts field, which this guide does not. If you do use it later with Helm 4, you need Kustomize v5.8.1 or newer: older versions call helm version -c --short, which Helm 4 no longer accepts.
- Install Kustomize by any route above and run
kustomize version. - Run
kubectl version --clientif you have kubectl, and compare the two version numbers. - Run
kustomize --helpand read the list of commands, notingbuild,createandedit.
Your first project, step by step
Build a tiny application from nothing. You need only a terminal. Make a working folder and a base directory inside it:
mkdir -p kustomize-demo/base
cd kustomize-demo
Step 1: write two plain manifests. These are ordinary Kubernetes objects, exactly what you would write without Kustomize. The Deployment runs the web server nginx, and the Service gives it a stable address inside the cluster.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
Step 2: write the kustomization. It lives in the same directory and must be named kustomization.yaml exactly. At its simplest it lists the files to include:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
The resources field is the list of things to put in the output. Each entry is a path, relative to the directory holding the kustomization file. An entry can be a file, another directory that has its own kustomization (which is how overlays will refer to bases), or a remote git address. Kustomize handles the entries in order.
Step 3: build it. From the kustomize-demo folder:
kustomize build base
The argument is a directory, not a file. If you leave it out, Kustomize builds the current directory (.). The output, which goes to your terminal, is:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
ports:
- port: 80
targetPort: 80
selector:
app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: nginx:1.25
name: web
ports:
- containerPort: 80
Read this carefully, because it teaches four things before you have used a single feature.
First, the output is one stream of YAML, with the documents separated by ---. You could pipe it into kubectl apply -f - and get the same result as applying the two original files.
Second, the order changed. You listed the Deployment first, but the Service appears first. Kustomize sorts the output into a fixed, conventional order meant to suit kubectl apply (namespaces, then configuration, then services, then workloads, and so on). You do not control that order by default, and you rarely need to.
Third, the keys inside each object were re-sorted alphabetically. Your file had name before image; the output has image before name. Kustomize parses the YAML and writes it back out in a normalised form. The meaning is identical, but the text will not match your input character for character, and any comments are dropped. This is why people compare builds with diff rather than by eye, and why you should never expect the build output to look like your source files.
Fourth, nothing was changed in your source files. Run git status or ls and confirm it: the base directory holds the same three files. The tool is read-only with respect to your manifests.
With no edits listed, the build is an identity operation: the same objects in, the same objects out. The value appears as soon as you add edits, which is what the next sections do.
Step 4: give the output a label. Add one section to the base kustomization so you can see a transformer at work. The labels field adds labels to every resource:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/name: web
Build again and look at the metadata of each object:
kustomize build base
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: web
name: web
spec:
ports:
- port: 80
targetPort: 80
selector:
app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: web
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: nginx:1.25
name: web
ports:
- containerPort: 80
Both the Service and the Deployment now carry app.kubernetes.io/name: web, and you touched neither file. Look closely at what did not get the label: the Service's selector and the Deployment's selector and pod template are unchanged. That is deliberate, and it is the subject of an important warning in the section on common changes. For now, notice that the field name is pairs, a map of label names to values, and that labels is a list, so the dash in front of pairs: matters.
Step 5: start a second, misspelled build on purpose. Errors are part of learning. Rename the field labels to lables, then build:
kustomize build base
Error: invalid Kustomization: json: unknown field "lables"
Kustomize checks the field names strictly. A misspelling produces an immediate error that names the exact word it did not recognise, rather than silently ignoring it. Fix the spelling and continue.
- Create the
basefolder with the three files above and runkustomize build base. - Add the
labelssection and build again. - Run
kustomize build base > rendered.yamland openrendered.yaml, then rundiffbetween it andbase/deployment.yamlto see the normalisation. - Introduce a typo in a field name and read the error.
unknown field error naming your typo.
Reading what the build does, and what it does not
Because every later section asks you to run kustomize build and read the output, it helps to fix a few facts about the command now.
kustomize build DIR does four things in this order: it finds kustomization.yaml inside DIR; it loads every entry under resources; it runs the generators and transformers you listed; and it prints the result to standard output. If anything fails, it prints an error to standard error and exits with a non-zero status, which is what makes it usable in scripts and pipelines.
The destination of the output is the part beginners mix up:
kustomize build base # print to the terminal
kustomize build base > rendered.yaml # save to one file
mkdir -p out && kustomize build base -o out/ # one file per resource, into an existing directory
kustomize build base | kubectl apply -f - # send straight to a cluster
The -o (or --output) flag writes to a file, or to a directory if you give it one, in which case each resource gets its own file. Redirecting with > does the same as -o with a file path. Piping into kubectl apply -f - is the classic two-step deployment: build, then apply. The - means "read from standard input".
There are three things the build does not do, and they are the source of half the confusion in the first week:
- It does not talk to a cluster. It cannot tell you whether the resources already exist, whether a namespace is present, or whether an image can be pulled. A build that succeeds only proves that the YAML could be assembled, not that Kubernetes will accept it.
- It does not validate against Kubernetes. Kustomize knows the shape of common Kubernetes objects well enough to edit them, but it will happily print a Deployment with
replicas: "three"or an unknown field. Kubernetes rejects that at apply time. If you want validation in a pipeline, add a separate tool such askubeconformto check the output. - It does not remember anything. There is no release history, no rollback, no record of what you built last Tuesday. Git is your history. That is a feature when you want reproducibility, and a gap when you expect Helm-style rollbacks. Rolling back a Kustomize deployment means checking out the old commit and applying again.
There is one more piece of output you may see: lines beginning with # Warning: printed before the YAML. They are deprecation notices, and they are sent to the error stream so they do not corrupt the YAML on standard output. A later section lists the fields they refer to.
kustomize build DIR | less or redirect it to a file and read it. You are looking at exactly what Kubernetes will receive. Most "it deployed the wrong thing" surprises are visible in that text, and reading it costs ten seconds.
- Run
mkdir out, thenkustomize build base -o out, and look at the files it creates. (If the path does not exist as a directory, Kustomize writes a single file with that name instead.) - Run
kustomize build nonexistentand read the error. - Rename
base/kustomization.yamltobase/kustomize.yaml, build again and read the error, then rename it back.
out; then an error saying the directory is not a valid directory; and then unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization'. These two are the most common first errors, and now you have met both.
Common changes with built-in fields
The fields in this section change every resource in a kustomization. They are the first things you reach for in an overlay, and most overlays need nothing else. Each one is a one-line, declarative version of an edit you would otherwise make by hand in several files.
Start by creating the production overlay. Make the directory and add a kustomization that refers to the base:
mkdir -p overlays/prod
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
The entry ../../base is a path from overlays/prod back up two levels and into base. It points at a directory, so Kustomize looks for a kustomization.yaml there and uses everything it produces. Building overlays/prod at this point gives output identical to the base. Now add edits one at a time.
Namespace
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: web-prod
Every namespaced resource in the output gets metadata.namespace: web-prod. You did not have to put a namespace in the base manifests, which is a good habit: a base without hardcoded namespaces can be reused anywhere. Two cautions: Kustomize does not create the namespace (add a Namespace manifest as a resource, or create it separately), and cluster-scoped resources such as ClusterRoles are not namespaced, so this field leaves them alone.
Name prefix and suffix
namePrefix: prod-
Every resource name is prefixed, so web becomes prod-web for both the Deployment and the Service. Use nameSuffix for the end of the name. This is how you run two copies of the same application in the same namespace, and it is handy for telling objects apart in logs. You do not need to fix references by hand. If a Pod refers to a ConfigMap by name, or an Ingress refers to a Service, Kustomize follows the rename and updates the reference. The documentation calls this name reference tracking, and it is one of the reasons the tool is more than sed.
Look at one thing it does not rewrite: a label selector. The Service's selector: app: web refers to labels, not to a resource name, so it stays app: web. Only real name references are rewritten.
Images
The images field changes container images without touching the Deployment file:
images:
- name: nginx
newTag: "1.27.3"
The name is the image name as written in the base, without the tag. Here the base says nginx:1.25, so the name is nginx. The new tag replaces the old one wherever that image appears, in any kind of workload resource. You can also change the repository with newName (handy for switching to a private registry mirror) or pin to a content digest with digest. Quote tags that look like numbers ("1.27") so YAML does not read them as a floating-point value and drop a trailing zero.
This is the field that continuous-delivery pipelines edit most often, since it is the one thing that changes on every release. There is a command for it, described in the everyday commands section.
Replicas
replicas:
- name: web
count: 3
Sets the replica count of the named Deployment, StatefulSet or other scalable resource. You could do the same with a patch, but this is shorter and says what it means. The name here is the resource's name in the base (web), not the prefixed name.
Labels and annotations
labels:
- pairs:
environment: production
commonAnnotations:
owner: platform-team
labels adds labels to the metadata of every resource. commonAnnotations adds annotations. Labels are used by Kubernetes to select objects, and annotations are free-form notes for people and tools, so the distinction is worth remembering: labels can change behaviour, annotations never do.
labels did not touch the Service's selector or the Deployment's selector. That is the safe default. The field has an option, includeSelectors: true, which also adds the label to selectors and pod templates. Do not turn it on for a live Deployment. A Deployment's spec.selector cannot be changed after creation, so applying a new selector fails with field is immutable, and the only cure is deleting and recreating the Deployment. The old top-level field commonLabels does exactly this unsafe thing by default, which is why it is deprecated. If you see it in a tutorial, use labels instead.
All together
Here is the overlay with every change combined, followed by its build output:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: web-prod
namePrefix: prod-
images:
- name: nginx
newTag: "1.27.3"
replicas:
- name: web
count: 3
labels:
- pairs:
environment: production
commonAnnotations:
owner: platform-team
kustomize build overlays/prod
apiVersion: v1
kind: Service
metadata:
annotations:
owner: platform-team
labels:
app.kubernetes.io/name: web
environment: production
name: prod-web
namespace: web-prod
spec:
ports:
- port: 80
targetPort: 80
selector:
app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
owner: platform-team
labels:
app.kubernetes.io/name: web
environment: production
name: prod-web
namespace: web-prod
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: nginx:1.27.3
name: web
ports:
- containerPort: 80
Check each line against the overlay. The names gained the prefix, both objects moved into web-prod, the image tag changed, the replicas became 3, and the base's label plus the overlay's label both appear. Labels from the base and from the overlay accumulate; they do not replace each other.
One subtlety is worth saying aloud: the overlay never mentions the Service or the Deployment by file. It describes what should be true of the whole output, and the tool finds every place to change. That is the declarative style the official documentation calls its central idea.
- Create
overlays/prod/kustomization.yamlwith the complete version above. - Build it, and confirm the name, namespace, image tag, replica count and both labels.
- Create
overlays/dev/kustomization.yamlthat refers to../../baseand sets onlynamespace: web-devandnamePrefix: dev-. - Build both overlays and run
diffon the two outputs.
kustomize build base once more and confirm it is still unchanged.
Patches: changing anything else
The built-in fields cover the common cases. Sooner or later you need to change something they do not reach: a memory limit, an environment variable, a health probe, a node selector. For that there are patches. A patch is a small piece of YAML that describes a change to one resource, and Kustomize merges it into the matching resource during the build. Your original file stays untouched.
There are two kinds of patch. Both go under the same field, patches, and as a beginner you will use the first one nearly all the time.
Strategic merge patches
A strategic merge patch looks like a partial copy of the resource, containing only the fields you want to change, plus enough identity to say which resource you mean. Identity means apiVersion, kind and metadata.name. Suppose production needs three replicas and a memory limit. Create a file next to the overlay's kustomization:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
template:
spec:
containers:
- name: web
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 256Mi
Reference it from the overlay:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: resources-patch.yaml
Kustomize reads the patch header, finds the Deployment named web from the base, and merges the patch into it. Three rules describe the merge:
- Maps are merged. Fields in the patch are added to or replace the matching fields in the original. Fields you did not mention stay as they were. The image, the ports and the labels survive, even though the patch never mentions them.
- Lists of named items are merged by name. The
containerslist is matched by each entry'sname. The patch entry withname: webmerges into the existing container calledweb. You do not have to repeat its image or ports. - Most other lists are replaced completely. If a list has no natural key, your version of the list wins and the original is discarded. Keep that in mind for something like
argsorcommand: patching it means writing the whole list.
The name in the patch header is what matters. The name must be the name as it is in the base, here web, even if the overlay also sets namePrefix: prod-. Kustomize remembers the original names while it builds, so you patch what the base calls the thing. This is the most common source of a puzzling error, which the errors section shows.
Removing a field with a patch
Patches normally add or change. To delete a field, use the special value null:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: web
resources: null
There is also a whole-resource directive, $patch: delete, which removes an entire resource from the output. It is the one place Kustomize lets you take something away, because the project's design otherwise avoids removal: the official guidance is that if you want less than the base provides, put less in the base.
apiVersion: v1
kind: Service
metadata:
name: web
$patch: delete
JSON6902 patches
The second kind is a precise list of operations, written as a standard called JSON Patch (RFC 6902). Each operation has an op (add, replace or remove), a path into the document, and usually a value. Use it when you want to change one exact field, especially inside a list with no merge key, where a strategic merge patch would replace the whole list.
patches:
- target:
group: apps
version: v1
kind: Deployment
name: web
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: nginx:1.27.3
- op: add
path: /spec/template/spec/containers/0/env
value:
- name: LOG_LEVEL
value: info
Three details distinguish this form. First, target is required, because the patch body carries no identity of its own; it tells Kustomize which resource to edit. Second, the body is written inline with patch: |-, a YAML block scalar, though you can also point to a file with path: and add a target. Third, paths use / between steps and numeric positions for lists, so containers/0 means the first container. If a key contains a /, as Kubernetes annotation keys often do, write it as ~1.
An add operation on a list can also append with - as the last index, as in /spec/template/spec/containers/-.
Choosing between them, and picking targets
The choice is simple. If the change reads naturally as "this resource, with these fields different", use a strategic merge patch. If it is one surgical edit deep in a list, use JSON6902. Both are listed in the same patches field and applied in the order written.
For strategic merge patches the target is inferred from the header, but you can supply a target anyway, and it can match by kind, name, namespace, labelSelector or annotationSelector. The name and namespace values are regular expressions anchored at both ends, so name: web means exactly web and name: web-.* matches everything starting with web-. One patch body can thus be applied to several resources, which is useful for something like a node selector on every Deployment:
patches:
- target:
kind: Deployment
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: ignored
spec:
template:
spec:
nodeSelector:
pool: general
When a target is given for a strategic merge patch, the name inside the body is ignored, which is why ignored is a fine placeholder. It just has to be present.
Older spellings you will meet
Two older fields, patchesStrategicMerge and patchesJson6902, were the way to write patches in tutorials from 2019 to 2022. Both are deprecated in favour of the single patches field, and Kustomize prints a warning if you use them. kustomize edit fix converts them automatically. When you read old examples, translate in your head: same idea, different field.
no resource matches strategic merge patch. Do not read that as a bug. It saved you from a silent no-op. Look for a typo in apiVersion, kind or name, or a mismatch with what the base actually calls the resource.
- In
overlays/prod, add theresources-patch.yamlabove and list it underpatches. - Build and confirm the memory limit appears, and that the image and ports remain.
- Add a JSON6902 patch that adds an
envvariable, and confirm it in the output. - Change
name: webin your strategic patch toname: nopeand read the error, then change it back.
Error: no resource matches strategic merge patch "Deployment.v1.apps/nope.[noNs]".
Generating ConfigMaps and Secrets
Applications need configuration, and in Kubernetes that means ConfigMaps (for ordinary settings) and Secrets (for sensitive values). Writing these as YAML by hand is tedious, and worse, changing one does not restart the pods that use it. Kustomize solves both problems with generators: fields that build a ConfigMap or Secret from literals or files.
The configMapGenerator
configMapGenerator:
- name: web-config
literals:
- LOG_LEVEL=info
- FEATURE_X=on
A generator creates a new resource. The build prints a ConfigMap you never wrote. Combined with the namespace and namePrefix fields from the earlier sections, here is what it emits:
apiVersion: v1
data:
LOG_LEVEL: info
kind: ConfigMap
metadata:
name: prod-web-config-hf678c7m2b
namespace: web-prod
The name is not web-config. It is prod-web-config-hf678c7m2b: the prefix from namePrefix, and a hash suffix computed from the content. The hash is the feature that makes generators worth using.
Why the hash matters
Kubernetes has an irritating property: if you change a ConfigMap, pods that already use it do not notice. They keep the old values until something restarts them. Teams work around it with manual kubectl rollout restart commands and comments that say "remember to restart after editing config".
Kustomize handles it differently. When the content changes, the hash changes, so the ConfigMap gets a new name. Kustomize also rewrites every reference to the ConfigMap, in envFrom, env, volumes and so on, to the new name. A changed reference in a Deployment's pod template is a changed pod template, and Kubernetes responds to that by doing a normal rolling update. Editing the config triggers the rollout on its own.
You can watch it happen. Give the Deployment a reference to the ConfigMap by its original name, using a patch:
patches:
- target:
kind: Deployment
name: web
patch: |-
- op: add
path: /spec/template/spec/containers/0/envFrom
value:
- configMapRef:
name: web-config
Build, and the Deployment's reference is rewritten to the hashed, prefixed name:
containers:
- envFrom:
- configMapRef:
name: prod-web-config-hf678c7m2b
image: nginx:1.25
name: web
You wrote web-config; the output says prod-web-config-hf678c7m2b. Kustomize found the reference and updated it. Change LOG_LEVEL=info to LOG_LEVEL=debug, build again, and the suffix changes, and so does the reference. The hash you see will differ from the one above if your content differs; it depends only on the data.
There is one important consequence. Always refer to a generated ConfigMap by its original name in your manifests and patches. Never type the hashed name; it changes whenever the data does.
Files and environment files
Literals are fine for a few values. For a whole file, such as an nginx.conf, use files, which stores the file's content under a key that defaults to the file name:
configMapGenerator:
- name: nginx-conf
files:
- nginx.conf
To choose the key, write key=path, such as default.conf=nginx.conf. For many simple settings, envs reads a file of KEY=value lines, the same format as a .env file:
configMapGenerator:
- name: web-env
envs:
- settings.env
Generator files have to be inside or below the kustomization directory, because of a safety rule explained in the errors section. Use envs (plural). An older singular env key exists but is legacy.
The secretGenerator
Secrets work the same way, with a different field name:
secretGenerator:
- name: web-credentials
literals:
- API_TOKEN=change-me
type: Opaque
The output is a Secret whose values are base64-encoded, with a hash suffix just like a ConfigMap. The type defaults to Opaque; use kubernetes.io/tls for certificates, with tls.crt and tls.key as files.
kustomization.yaml literal, and never commit an unencrypted file referenced by files or envs. The generator gives you the convenience and the hash, not secrecy. Real projects keep the values in a secrets manager or use an encryption tool, which the Mid-level and Senior guides cover. For a learning exercise on your laptop, placeholder values are fine.
Options: hash names and labels
Sometimes you do not want a hash, for example when something outside Kubernetes refers to the ConfigMap by a fixed name. Switch it off for one generator:
configMapGenerator:
- name: web-config
literals:
- LOG_LEVEL=info
options:
disableNameSuffixHash: true
Or for every generator in the kustomization at once with generatorOptions:
generatorOptions:
disableNameSuffixHash: true
Turning the hash off gives up the automatic rollout on change. If you switch it off, you have to restart the pods yourself when the data changes. options can also add labels and annotations to the generated resource, and mark it immutable.
Merging with a generator in the base
An overlay can extend a ConfigMap that the base generated. Give the overlay's generator the same name and set behavior: merge (or replace to discard the old data completely):
configMapGenerator:
- name: web-config
behavior: merge
literals:
- LOG_LEVEL=warn
The default behaviour is create, which fails if the name already exists. With merge, keys in the overlay override keys in the base and new keys are added. The name, and any namespace, must match the base's ConfigMap as the base had it, before the overlay's own namespace or namePrefix changes are applied. Getting that wrong produces does not exist; cannot merge or replace, covered in the errors section.
- Add a
configMapGeneratorwith one literal tooverlays/prod/kustomization.yamland build. Note the hashed name. - Add the
envFrompatch shown above, build, and find the rewritten reference. - Change the literal's value, build again, and compare the two hash suffixes.
- Add
options: {disableNameSuffixHash: true}and build once more.
Bases and overlays in practice
You have already used a base and an overlay. This section makes the structure explicit and covers how to grow it without making a mess.
The standard layout is one base and one overlay per environment:
kustomize-demo/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
├── kustomization.yaml
└── resources-patch.yaml
Nothing forces the names base and overlays. They are a convention every Kustomize user recognises, so following it makes your repository instantly readable to new colleagues. The directory names dev and prod are likewise only names.
What belongs in the base? Whatever is true in every environment: the Deployment, the Service, the container ports, the probes, the labels that identify the app. What belongs in an overlay? Whatever differs: replica counts, image tags, resource sizes, hostnames, namespaces, environment-specific configuration. A reliable test for any setting: "If I changed this in the base, would every environment want the change?" If yes, it belongs in the base.
Build each environment with one command, so the path is the only thing that changes:
kustomize build overlays/dev
kustomize build overlays/prod
Because all the environments share the base, a fix such as adding a readiness probe goes in one file and appears in every environment at the next build. That is the practical payoff of the whole design.
What an overlay may refer to
An overlay lists its base under resources, pointing at a directory. There are rules about what else it may load, and breaking them produces the most instructive errors in this guide.
- Referencing a directory (a base) can point anywhere, including
../../base. That is what the../../baseline does. - Loading individual files is restricted to the overlay's own directory and below. A patch file, a generator file or a resource file living outside the overlay is rejected with a
security; file ... is not in or below ...error. This is the load restriction, and it exists so a kustomization cannot quietly read arbitrary files from your disk, such as~/.ssh/id_rsa, into a ConfigMap. - Do not reference a single file from another kustomization's directory. If you want its contents, turn that directory into a base and reference the directory.
An overlay can also reference more than one base, or other overlays, and Kustomize will merge them. Multi-base setups go beyond a first project, but you should know the rule that applies: two resources in one build may not have the same kind, name and namespace. Include the same base twice, and you will see may not add resource with an already registered id.
Remote bases
A base does not have to be on your disk. A resources entry can be a git URL, which is how many open-source projects publish a deployable configuration:
resources:
- https://github.com/example-org/example-repo//deploy/base?ref=v1.2.3
The double slash separates the repository from the subdirectory inside it, and ref picks a tag, branch or full commit hash. Kustomize clones the repository on every build, and the default time limit is 27 seconds. For anything important, pin ref to a tag or full commit hash so the build is repeatable, and remember that a remote base is code you did not write, so read it before you trust it. Short commit hashes are not supported; use the full 40 characters.
kustomize build base whenever you change it. A base that only works when an overlay patches it is fragile, and its errors are harder to read. The base should always build on its own.
- Add a
readinessProbetobase/deployment.yaml(an HTTP GET on path/, port 80). - Build both overlays without touching them.
- Confirm the probe appears in both outputs.
The everyday commands, grouped by what you want to do
You have used build. Here is the whole beginner toolbox, organised by intent rather than alphabet.
See the result
| I want to | Command |
|---|---|
| Print the final YAML | kustomize build DIR |
| Save it to a file | kustomize build DIR > rendered.yaml |
| Write one file per resource | kustomize build DIR -o out/ |
| Use the copy in kubectl | kubectl kustomize DIR |
| Build the current directory | kustomize build |
kubectl kustomize DIR does the same thing as kustomize build DIR using the version embedded in kubectl. The two accept the same flags.
Start a kustomization
If you already have YAML files in a folder, kustomize create writes a starting kustomization.yaml:
kustomize create --autodetect
With --autodetect, it scans the directory for Kubernetes resource files and lists them. The generated file looks like this:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
Without --autodetect, pass the files yourself: kustomize create --resources deployment.yaml,service.yaml. The command also accepts --namespace, --nameprefix, --namesuffix, --labels and --annotations so you can set those fields at creation time.
Change the file with commands instead of an editor
kustomize edit edits kustomization.yaml for you. It is ideal in scripts, because editing YAML with sed is fragile, while edit understands the structure. It operates on the kustomization in the current directory, so cd into the overlay first.
cd overlays/prod
kustomize edit set image nginx=nginx:1.27.3
kustomize edit set replicas web=3
kustomize edit set namespace web-prod
kustomize edit set nameprefix prod-
kustomize edit add resource ../../base
kustomize edit add configmap web-config --from-literal=LOG_LEVEL=info
Each command changes exactly one thing. After running the first two in a dev overlay, the file contains:
images:
- name: nginx
newName: nginx
newTag: 1.27.3
replicas:
- count: 2
name: web
The command normalised the entry: it filled in newName: nginx (the same as the name) and wrote the tag without quotes. That is harmless, though it is why a hand-written file and an edited file sometimes differ in style.
The set image command accepts several forms:
| Form | Effect |
|---|---|
nginx=nginx:1.27.3 |
Change name and tag |
nginx:1.27.3 |
Change the tag for image nginx |
nginx=registry.example.com/mirror/nginx |
Change the repository only |
nginx@sha256:<digest> |
Pin to a digest |
Tags may only contain letters, digits, ., _ and -. If you pass * as a new name, tag or digest, the existing value is kept.
This command solves the "no variables" problem mentioned earlier. In a CI pipeline, you inject the version of the image you just built like this:
cd overlays/prod
kustomize edit set image nginx=nginx:"$IMAGE_TAG"
kustomize build . | kubectl apply -f -
The shell substitutes $IMAGE_TAG before Kustomize runs, and Kustomize itself never reads your environment. The edit is recorded in the file, so you can commit it and the change is visible in git. That matches the project's own advice for passing a value into a build.
kustomize edit add label key:value writes the old commonLabels field by default, including the selector-changing behaviour. Add --without-selector to write the safe labels field instead. If you use labels, this flag matters.
Fix deprecated fields
kustomize edit fix
It rewrites deprecated fields in the current kustomization to their modern replacements, such as patchesStrategicMerge to patches and bases to resources. Commit your work first so you can see what it did. There is a section on deprecated fields below.
Preview and apply
| I want to | Command |
|---|---|
| See what would change in the cluster | kubectl diff -k DIR |
| Apply to a cluster | kubectl apply -k DIR |
| Delete what the kustomization created | kubectl delete -k DIR |
| Build, then apply with the standalone tool | kustomize build DIR | kubectl apply -f - |
The next section covers these in detail.
- In an empty folder, copy your two manifests and run
kustomize create --autodetect. Read the file it writes. - Run
kustomize edit set image nginx=nginx:1.27.3and thenkustomize edit set replicas web=2. Read the file again. - Build, and confirm both changes are in the output.
images and replicas sections you never typed, and a build that reflects them.
Applying to a cluster
So far nothing has left your laptop. To deploy, you hand the built YAML to Kubernetes. You need a cluster (a local one such as kind or minikube is perfect for practice) and a working kubectl pointed at it.
One step with kubectl apply -k.
kubectl apply -k overlays/prod
The -k flag (short for --kustomize) tells kubectl that the argument is a kustomization directory, not a file. It builds with its embedded Kustomize and applies the result. You cannot combine -k with -f or -R in one command. The same flag works on kubectl diff -k, kubectl delete -k and kubectl get -k.
Two steps with a pipe.
kustomize build overlays/prod | kubectl apply -f -
The pipe form uses your standalone Kustomize instead of the one inside kubectl. Choose it when you want the newer version's features, or when you need to pass build flags, because kubectl apply -k cannot take them.
Preview first.
kubectl diff -k overlays/prod
It prints what would change between the live cluster and what you are about to apply, as a diff, without changing anything. On the first deployment everything is an addition; on later ones, the lines that differ are the ones that matter. Make it a habit before any production apply.
Create the namespace first. Setting namespace: web-prod changes objects' namespace field, but does not create the namespace. If it does not exist, the apply fails with namespaces "web-prod" not found. Create it once with kubectl create namespace web-prod, or add a Namespace manifest to the base or to the overlay's resources.
Remove what you created.
kubectl delete -k overlays/prod
This deletes every object in the build output, which is a tidy way to tear down an environment.
kubectl apply -k uses the copy built into kubectl, and kustomize build uses the standalone binary. They can be different releases, and so can the YAML they produce, especially around newer fields and bug fixes. Decide which one your team uses and stick to it. A safe beginner rule: use the pipe form (kustomize build ... | kubectl apply -f -) so the version is the one you installed and can name.
Where GitOps tools fit. You will meet Argo CD and Flux, which watch a git repository and apply it for you. Both understand Kustomize natively: point them at an overlay directory, and they build and apply it on every change. All the knowledge in this guide carries over unchanged, because the overlay directory is the interface. The Mid-level guide covers the details, and the catalogue has a guide for Argo CD if you want to go there next.
- Start a local cluster (for example
kind create cluster) and create theweb-devnamespace. - Run
kubectl diff -k overlays/devand read the additions. - Run
kubectl apply -k overlays/devand thenkubectl get all -n web-dev. - Run
kubectl delete -k overlays/devto clean up.
dev-web and a Service named dev-web, both in web-dev, and a clean namespace after the delete. If you have no cluster, read the output of kustomize build overlays/dev and confirm that every object carries the dev prefix and namespace.
Configuration details and deprecated fields
A few facts about the kustomization file itself help you read other people's files and avoid outdated advice.
File name and header. The file must be called kustomization.yaml (kustomization.yml and Kustomization also work). The first two lines should be apiVersion: kustomize.config.k8s.io/v1beta1 and kind: Kustomization. They may be omitted, but if you write one you must write both. The version is v1beta1, and there is no v1 at the time of writing.
Ordering of effects. Inside one kustomization, Kustomize loads resources first, then runs generators, then transformers. You do not control the order of the convenient fields (namespace, namePrefix, images and so on) by moving lines around; they run in a fixed internal order. Patches run in the order written in the patches list. The only place order is yours to choose is the patches list.
No globs. resources: ["*.yaml"] does not work. Kustomize does not expand wildcards in the file, so list files explicitly. (kustomize edit add resource *.yaml works because your shell expands the glob before Kustomize sees it.)
Output order is fixed. Resources are sorted into a conventional order on output. A sortOptions field can change it at the top level, which a beginner rarely needs.
Deprecated fields. A large share of the tutorials online use fields that have since been deprecated. They still work, but Kustomize prints a warning and they are excluded from any future v1 version. Learn the modern names:
| If you see | Use instead |
|---|---|
commonLabels |
labels (add includeSelectors: true only for new resources) |
patchesStrategicMerge |
patches |
patchesJson6902 |
patches with a target |
bases |
resources |
vars |
replacements (a Mid-level topic) |
imageTags |
images |
The warning looks like this:
# Warning: 'commonLabels' is deprecated. Please use 'labels' instead. Run 'kustomize edit fix' to update your Kustomization automatically.
It names the field, the replacement and the command that converts it. Treat it as a to-do item, not as an error: the build still works.
Removed features. A few behaviours from the past are gone entirely as of the v5 series and will break old examples. Generators no longer read your environment variables. The gh: shorthand for GitHub URLs was removed. Support for Starlark functions was removed in v5.5.0. If a tutorial depends on one of these, it is too old to follow.
The standalone versus embedded difference again. The fields above exist in both, but newer features, such as sourceValue in replacements (v5.7.0), need a recent version. If something from the docs fails with unknown field, check kustomize version and kubectl version --client before assuming your YAML is wrong.
- Add
commonLabels: {team: ml}to a scratch kustomization and build it. Read the warning on the error stream. - Run
kustomize edit fixand look at what replaced it. - Build again and confirm the warning is gone.
labels entry with includeSelectors: true after the fix, then a clean build.
Common errors and how to read them
Kustomize error messages look long and alarming, but each has a pattern: a prefix telling you what stage failed, then a detail. Read the last part first, because it usually holds the actual reason. Here are the errors you will meet in your first month.
The kustomization file is not found.
Error: unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization' in directory '/home/me/demo/overlays/qa'
You pointed build at a directory that has no kustomization file. Check the path you gave, and check the file name; kustomize.yaml and kustomisation.yaml do not count. If the directory itself does not exist you get a similar message saying must build at directory: not a valid directory.
A resource file is missing.
Error: accumulating resources: accumulation err='accumulating resources from 'service.yaml': evalsymlink failure on '/home/me/demo/base/service.yaml' : lstat /home/me/demo/base/service.yaml: no such file or directory': must build at directory: not a valid directory: ...
The phrase accumulating resources means it was loading your resources list. no such file or directory names the missing path. Kustomize tried the entry as a file first, then as a directory, and the trailing must build at directory is the second failure. The real cause is the first: the file does not exist at that path. Paths are relative to the kustomization file, not to where you ran the command.
A field is misspelled or invalid.
Error: invalid Kustomization: json: unknown field "namespce"
A key is unknown. Kustomize checks field names strictly, so a typo or a field from a newer version shows up here. A different message, yaml: line 2: did not find expected key, means the file is not valid YAML at all: check the indentation near the line it names.
A patch matches nothing.
Error: no resource matches strategic merge patch "Deployment.v1.apps/nope.[noNs]": no matches for Id Deployment.v1.apps/nope.[noNs]; failed to find unique target for patch Deployment.v1.apps/nope.[noNs]
The quoted identifier has the shape Kind.version.group/name.namespace. Here it asks for a Deployment named nope with no namespace. Compare it to the resource you meant: a typo in the name, a wrong kind, or a wrong apiVersion all cause this. Use the name the base gives the resource.
A file is outside the allowed directory.
Error: loading KV pairs: file sources: [../outside/settings.env]: security; file '/home/me/demo/outside/settings.env' is not in or below '/home/me/demo/overlays/dev'
This is the load restriction. A generator (or patch, or resource) points to a file outside the overlay's own directory tree. Move the file into the overlay, or turn the other directory into a base and refer to it as a directory. You can disable the check with --load-restrictor LoadRestrictionsNone, but as a beginner you should not. The error exists to protect you.
The same resource twice.
Error: accumulating resources: accumulation err='merging resources from 'b.yaml': may not add resource with an already registered id: Deployment.v1.apps/web.[noNs]' ...
Two resources share kind, name and namespace. The usual cause is that a base appears twice, or two files define the same object. Remove the duplicate. A namePrefix in one of the two places can also separate them.
Merging into a generated ConfigMap fails.
Error: merging from generator ... does not exist; cannot merge or replace
You used behavior: merge or replace, but no ConfigMap with that name and namespace exists in the base. The usual reasons are a typo in the name, or a namespace mismatch: generators run before the namespace field takes effect, so use the base's own namespace, not the overlay's new one.
A build that works but kubectl apply fails.
The Deployment "web" is invalid: spec.selector: Invalid value: ...: field is immutable
This is a Kubernetes error, not a Kustomize one. The build added a label to a selector (through commonLabels or includeSelectors: true) on a Deployment that already exists. Selectors cannot change. Remove that selector change, or delete the Deployment and let the apply recreate it.
- Cause each error above on purpose in a scratch copy: delete a resource file, misspell a key, rename a patch target, and list the same file twice.
- For each, read the message from the end backwards and say the cause in one sentence before fixing it.
Putting it all together
Here is a small end-to-end project that uses everything so far. You will build a web application with a base and two overlays, a generated ConfigMap, a patch, a pinned image and a deployment preview. It takes about fifteen minutes. The finished layout is:
shop/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
├── kustomization.yaml
└── resources-patch.yaml
The base holds what is true everywhere. The Deployment and Service are the ones from the first project. The base kustomization adds a label and a default ConfigMap:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/name: shop
configMapGenerator:
- name: shop-config
literals:
- LOG_LEVEL=debug
Wire the ConfigMap into the Deployment by adding an envFrom block in base/deployment.yaml, using the original name:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: shop-config
The dev overlay is deliberately tiny: its own namespace, a name prefix, and nothing else, so it keeps the base's debug logging and single replica.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: shop-dev
namePrefix: dev-
The prod overlay pins a tested image, scales up, sets resource limits by patch, and overrides the log level by merging into the base's ConfigMap:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: shop-prod
namePrefix: prod-
images:
- name: nginx
newTag: "1.27.3"
replicas:
- name: web
count: 3
patches:
- path: resources-patch.yaml
configMapGenerator:
- name: shop-config
behavior: merge
literals:
- LOG_LEVEL=warn
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: web
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 256Mi
Build both and compare.
kustomize build overlays/dev > dev.yaml
kustomize build overlays/prod > prod.yaml
diff dev.yaml prod.yaml
The diff should show exactly the differences you declared: the names, the namespace, the tag, the replica count, the resource limits and the log level. The ConfigMap's hash suffix differs too, because its content (the log level) differs. It should show nothing else. If something unexpected appears, you found it before Kubernetes did, which is the point.
Check that the ConfigMap wiring followed the hash. In each output, find the configMapRef under the Deployment. It should name the generated, prefixed, hashed ConfigMap, and that name should match the ConfigMap in the same file. Change a literal, rebuild, and watch both change together.
Preview and apply to a local cluster, one environment at a time:
kubectl create namespace shop-dev
kustomize build overlays/dev | kubectl diff -f -
kustomize build overlays/dev | kubectl apply -f -
kubectl get all,configmap -n shop-dev
Notice that this sequence uses the standalone build and the pipe form, as recommended earlier. For production the commands are the same with overlays/prod and shop-prod. When you are done, kustomize build overlays/dev | kubectl delete -f - removes everything.
What have you just done? You wrote the application once, described two environments in a few lines each, had the tool rewrite every reference that depended on a generated name, and verified the result before it touched a cluster. You can repeat that for any application.
- Build the project exactly as shown, from empty folders.
- Add a third overlay called
stagingwith two replicas and the same tag as prod. - Change something in the base, such as a container port name, and confirm all three outputs change without any overlay being edited.
What you can now do, and what comes next
You started without knowing what a kustomization was. You can now do the following, unaided:
- Explain the problem Kustomize solves and why it keeps YAML valid instead of templating it.
- Name the five core words (resource, kustomization, base, overlay, patch) and say which way the dependency arrows point.
- Install the tool, check its version, and know why the copy in
kubectlmay differ. - Write a
kustomization.yaml, build it, and read the output, including its normalised ordering. - Change namespace, name prefix, images, replicas, labels and annotations with one-line fields.
- Write strategic merge and JSON6902 patches, and choose between them.
- Generate ConfigMaps and Secrets, explain what the hash suffix does, and avoid committing real secrets.
- Build a base with dev and prod overlays, and preview and apply them with
kubectl. - Recognise deprecated fields and read the most common error messages.
Practical next steps, in order of usefulness: take one of your real applications and convert it, using kustomize create --autodetect to get started; add kustomize build for each overlay to your continuous-integration checks, so a broken overlay fails a pull request before it merges; and read the output of your builds until it feels ordinary.
The Mid-level guide picks up where this ends. It explains how the build pipeline works internally, so you can predict behaviour rather than discover it, and then covers components, replacements, remote bases, Helm integration, multi-cluster layouts, secrets handling and testing. The Senior guide treats Kustomize as a platform other teams depend on. For the tools around it, go to the Kubernetes guide if the cluster side still feels shaky, the Helm guide to see the other major approach, and the Terraform guide for creating the clusters themselves.
A last piece of advice. Kustomize rewards small steps. Change one thing, build, read the output, and only then change the next. The tool never touches your files or your cluster when you build, so there is no cost to experimenting, and every confusing result is one command away from an explanation.
Sources
- Kustomize reference documentation
- Kustomization file reference
- Glossary
- Built-in plugins
- kustomize build
- kustomize create
- kustomize edit
- kustomize version
- patches
- labels
- commonLabels
- images
- replicas
- namespace
- configMapGenerator
- secretGenerator
- generatorOptions
- Eschewed features
- Versioning policy
- Installing Kustomize
- Kustomize releases on GitHub
- Declarative management of Kubernetes objects using Kustomize