تخطَّ إلى المحتوى
العودة إلى أدلة الدارسين
KustomizeDevOpsContainers & Kubernetes3 مستويات121 قسمًايغطّي Kustomize 5.8دليل بالإنجليزية

The Complete Kustomize Guide

Customise Kubernetes manifests per environment without templates using Kustomize. Taught at three levels — Beginner, Mid-level and Senior — each with an in-depth guide, interview prep, and practical tips.

التوثيق الرسمي مسودّة بالذكاء الاصطناعي · مراجعة المجتمع جاريةساعدنا في مراجعته
17sections
79examples

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.

PLAIN YAMLdeployment.yaml, service.yaml
→
KUSTOMIZATIONa list of edits
→
KUSTOMIZE BUILDapplies the edits
→
FINAL YAMLto stdout

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.

Try it
  1. Think of one application you have deployed, or could deploy, to two environments.
  2. Write down three things that must differ between the environments (for example replica count, image tag, hostname).
  3. Write down three things that must stay identical.
a short list of differences and a longer list of sameness. Kustomize exists to store the sameness once and the differences next to it.

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.

Try it
  1. Find a repository (yours or open source) that deploys to Kubernetes.
  2. Decide which of the four approaches it uses: copies, scripts, Helm or Kustomize. The clues are folders named after environments, a Chart.yaml file, or a kustomization.yaml file.
  3. Note one thing that would be hard to change safely in that approach.
an answer that connects the repository's pain, such as duplicated files or templated YAML, to one of the trade-offs above.

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:

OVERLAYS
overlays/dev1 replica, web-dev
overlays/prod3 replicas, web-prod
each overlay lists the base under resources:
BASE
basedeployment.yaml, service.yaml
The base knows nothing about the overlays. You can apply the base alone, and adding a new overlay never touches it.

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.

Try it
  1. Draw a box for a base and two boxes for overlays on paper.
  2. Draw the arrows. Which way do they point?
  3. Write one sentence explaining why the base never needs to change when you add a third overlay.
arrows pointing from overlays to the base, and a sentence saying the base does not refer to its overlays at all.

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:

BASH
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:

BASH
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):

BASH
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:

BASH
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:

BASH
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:

BASH
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:

BASH
docker run --rm -v "$PWD":/work -w /work registry.k8s.io/kustomize/kustomize:v5.8.1 build overlays/prod
Piping a script into bash Running 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:

BASH
kustomize version
TEXT
v5.8.2

For more detail, ask for structured output:

BASH
kustomize version -o yaml
TEXT
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 is optional, and old tools meet new Helm badly You only need the 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.
Try it
  1. Install Kustomize by any route above and run kustomize version.
  2. Run kubectl version --client if you have kubectl, and compare the two version numbers.
  3. Run kustomize --help and read the list of commands, noting build, create and edit.
a version that starts with v5, and a help screen listing the three commands you will use most. The kubectl number may be lower than the standalone number. That gap is normal.

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:

BASH
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.

base/deployment.yaml
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
base/service.yaml
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:

base/kustomization.yaml
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:

BASH
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:

YAML
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:

base/kustomization.yaml
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:

BASH
kustomize build base
YAML
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:

BASH
kustomize build base
TEXT
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.

Why the error says "json" The message mentions JSON even though your file is YAML. Internally, Kustomize converts the YAML to JSON before reading it into a Go structure. The word refers to that internal step. Read it as "the file has a key I do not know".
Try it
  1. Create the base folder with the three files above and run kustomize build base.
  2. Add the labels section and build again.
  3. Run kustomize build base > rendered.yaml and open rendered.yaml, then run diff between it and base/deployment.yaml to see the normalisation.
  4. Introduce a typo in a field name and read the error.
labels appearing in the output but not in your source files; keys in alphabetical order; and an 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:

BASH
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:

  1. 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.
  2. 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 as kubeconform to check the output.
  3. 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.

A habit worth starting today Before you apply anything, run 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.
Try it
  1. Run mkdir out, then kustomize 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.)
  2. Run kustomize build nonexistent and read the error.
  3. Rename base/kustomization.yaml to base/kustomize.yaml, build again and read the error, then rename it back.
one file per resource in 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:

BASH
mkdir -p overlays/prod
overlays/prod/kustomization.yaml
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

overlays/prod/kustomization.yaml
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

overlays/prod/kustomization.yaml
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:

overlays/prod/kustomization.yaml
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

overlays/prod/kustomization.yaml
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

overlays/prod/kustomization.yaml
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.

The label trap: selectors are immutable In the base you saw that 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:

overlays/prod/kustomization.yaml
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
BASH
kustomize build overlays/prod
YAML
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.

Try it
  1. Create overlays/prod/kustomization.yaml with the complete version above.
  2. Build it, and confirm the name, namespace, image tag, replica count and both labels.
  3. Create overlays/dev/kustomization.yaml that refers to ../../base and sets only namespace: web-dev and namePrefix: dev-.
  4. Build both overlays and run diff on the two outputs.
a diff whose every line maps to a difference you wrote in the two overlays, with no other noise. Then run 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:

overlays/prod/resources-patch.yaml
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:

overlays/prod/kustomization.yaml
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 containers list is matched by each entry's name. The patch entry with name: web merges into the existing container called web. 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 args or command: 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:

overlays/prod/remove-limits.yaml
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.

overlays/dev/drop-service.yaml
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.

overlays/prod/kustomization.yaml
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:

overlays/prod/kustomization.yaml
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.

A patch that matches nothing is an error, and that is good If the header of a strategic merge patch names a resource that does not exist, the build stops with 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.
Try it
  1. In overlays/prod, add the resources-patch.yaml above and list it under patches.
  2. Build and confirm the memory limit appears, and that the image and ports remain.
  3. Add a JSON6902 patch that adds an env variable, and confirm it in the output.
  4. Change name: web in your strategic patch to name: nope and read the error, then change it back.
resource limits and an env var on the container with everything else intact, then 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

overlays/prod/kustomization.yaml
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:

YAML
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:

overlays/prod/kustomization.yaml
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:

YAML
      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:

base/kustomization.yaml
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:

base/kustomization.yaml
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:

overlays/prod/kustomization.yaml
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.

Base64 is encoding, not encryption Anyone who can read the output can decode the secret with one command. Never commit real passwords or tokens into a 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:

overlays/prod/kustomization.yaml
configMapGenerator:
- name: web-config
  literals:
  - LOG_LEVEL=info
  options:
    disableNameSuffixHash: true

Or for every generator in the kustomization at once with generatorOptions:

overlays/prod/kustomization.yaml
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):

overlays/prod/kustomization.yaml
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.

Try it
  1. Add a configMapGenerator with one literal to overlays/prod/kustomization.yaml and build. Note the hashed name.
  2. Add the envFrom patch shown above, build, and find the rewritten reference.
  3. Change the literal's value, build again, and compare the two hash suffixes.
  4. Add options: {disableNameSuffixHash: true} and build once more.
a hashed name that changes with the data, a reference that follows it, and a plain name when the hash is disabled. This is the rollout-on-config-change behaviour described above.

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:

TEXT
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:

BASH
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 ../../base line 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:

overlays/prod/kustomization.yaml
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.

Keep the base deployable Test the base with 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.
Try it
  1. Add a readinessProbe to base/deployment.yaml (an HTTP GET on path /, port 80).
  2. Build both overlays without touching them.
  3. Confirm the probe appears in both outputs.
the probe in dev and prod, with no overlay edited. That is one change reaching every environment.

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:

BASH
kustomize create --autodetect

With --autodetect, it scans the directory for Kubernetes resource files and lists them. The generated file looks like this:

kustomization.yaml
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.

BASH
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:

overlays/dev/kustomization.yaml
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:

BASH
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.

`edit add label` writes the deprecated field 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

BASH
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.

Try it
  1. In an empty folder, copy your two manifests and run kustomize create --autodetect. Read the file it writes.
  2. Run kustomize edit set image nginx=nginx:1.27.3 and then kustomize edit set replicas web=2. Read the file again.
  3. Build, and confirm both changes are in the output.
a kustomization with 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.

BASH
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.

BASH
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.

BASH
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.

BASH
kubectl delete -k overlays/prod

This deletes every object in the build output, which is a tidy way to tear down an environment.

Two versions of Kustomize may be in play 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.

Try it
  1. Start a local cluster (for example kind create cluster) and create the web-dev namespace.
  2. Run kubectl diff -k overlays/dev and read the additions.
  3. Run kubectl apply -k overlays/dev and then kubectl get all -n web-dev.
  4. Run kubectl delete -k overlays/dev to clean up.
a Deployment named 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:

TEXT
# 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.

Try it
  1. Add commonLabels: {team: ml} to a scratch kustomization and build it. Read the warning on the error stream.
  2. Run kustomize edit fix and look at what replaced it.
  3. Build again and confirm the warning is gone.
the deprecation warning first, then a 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.

TEXT
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.

TEXT
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.

TEXT
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.

TEXT
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.

TEXT
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.

TEXT
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.

TEXT
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.

TEXT
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.

Try it
  1. 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.
  2. For each, read the message from the end backwards and say the cause in one sentence before fixing it.
you can now name the cause from the message alone, and each fix takes seconds. Keep this list open for your first real project.

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:

TEXT
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:

base/kustomization.yaml
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:

base/deployment.yaml
      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.

overlays/dev/kustomization.yaml
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:

overlays/prod/kustomization.yaml
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
overlays/prod/resources-patch.yaml
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.

BASH
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:

BASH
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.

Try it
  1. Build the project exactly as shown, from empty folders.
  2. Add a third overlay called staging with two replicas and the same tag as prod.
  3. Change something in the base, such as a container port name, and confirm all three outputs change without any overlay being edited.
a third environment made from about ten lines and a base change that reaches every overlay.

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 kubectl may 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