Certified Kubernetes Application Developer (CKAD) #10 Kustomize: The Overlay Pattern and Per-Environment Manifests
In #9 Helm you learned how to deploy the same application per environment with charts and values. Where Helm stamps out manifests from a template engine, Kustomize takes the opposite approach. It leaves pure YAML untouched and layers only transformation rules on top of it to produce per-environment manifests. There’s no template syntax and no substitution markers like {{ }}, so a base manifest is, by itself, valid YAML that kubectl apply accepts.
The reason Kustomize matters so much on the CKAD is that it’s built into kubectl. With no separate binary to install, you can use kubectl apply -k and kubectl kustomize right away, so the exam environment needs no extra setup. In this post we’ll organize the core fields of kustomization.yaml, the base/overlays structure, patches and generators, and the build/apply flow from a hands-on exam perspective.
The problem Kustomize solves #
When you deploy the same application to dev, staging, and prod, what differs per environment is usually just a handful of fields. The replica count, image tag, namespace, and a few ConfigMap values change while the rest of the structure stays identical. There are two ways to handle this difference.
- Copy then edit. If you clone the entire manifest per environment, every time you fix the shared part you have to chase down the change across all copies. A missed copy turns straight into an incident.
- Separate the transformation rules. You keep a single shared manifest (the base) and describe each environment’s difference as a small patch (the overlay). The shared part lives in one place — the base — only.
Kustomize is the latter. It leaves the base intact and the overlay layers transformations on top, so duplication between environments disappears.
The core fields of kustomization.yaml #
Every Kustomize behavior is declared in a single kustomization.yaml file. The directory that holds this file is the build unit. Let’s start with the fields you’ll use most often.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# 1) Input resources (YAML files or other kustomize directories)
resources:
- deployment.yaml
- service.yaml
# 2) Apply a namespace to all resources at once
namespace: my-app
# 3) Attach a prefix/suffix before and after names
namePrefix: dev-
nameSuffix: -v1
# 4) Attach common labels/annotations to all resources
commonLabels:
app: web
env: dev
commonAnnotations:
team: platform
# 5) Swap image tag/name (without touching the manifest)
images:
- name: nginx
newName: nginx
newTag: "1.27"
# 6) Swap the replica count of a Deployment, etc.
replicas:
- name: web
count: 4The role of each field is as follows.
| Field | Role |
|---|---|
resources | Manifests or sub-kustomize directories to include in the build |
namespace | Apply a namespace across all target resources |
namePrefix / nameSuffix | Attach a prefix/suffix to resource names (references are updated too) |
commonLabels | Add labels to metadata.labels and the selector of every resource |
commonAnnotations | Add annotations to every resource |
images | Swap container image name/tag at build time |
replicas | Swap the replica count of the specified workload |
commonLabels doesn’t just attach labels — it also aligns the Deployment’s selector.matchLabels and the Pod template’s labels. That prevents the mismatch incidents you get when editing the selector by hand.
The base and overlays structure #
The most common shape in practice and on the exam is to split things into a single base directory and per-environment overlay directories.
myapp/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── replica-patch.yaml
└── prod/
├── kustomization.yaml
└── replica-patch.yamlbase/kustomization.yaml collects the shared resources.
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yamlEach overlay’s kustomization.yaml lists, in resources, the path to the base directory instead of files. That pulls in all the base’s resources and then layers on top of them the transformation rules and patches written in the same file.
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base # reference the entire base
namespace: prod
namePrefix: prod-
replicas:
- name: web
count: 6With this setup the base is maintained as a single copy, and dev and prod each declare only the differences they need. Fixing the base’s deployment.yaml propagates to both environments at once.
Patches: overwriting only some fields #
Changes that can’t be expressed with a dedicated field like namePrefix or replicas are handled with patches. A patch is a small fragment that picks out and overwrites only specific fields of the base. There are two approaches.
patchesStrategicMerge (strategic merge) #
When you write a partial YAML in the same shape as the original, Kustomize finds matching keys and merges them. It’s easy for humans to read, so it’s the most commonly used.
# overlays/prod/resource-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web # identifies which resource to patch
spec:
template:
spec:
containers:
- name: web
resources:
limits:
memory: 512Mi# part of overlays/prod/kustomization.yaml
patchesStrategicMerge:
- resource-patch.yamlmetadata.name and kind identify the target resource, and only the fields you specify (here, resources.limits.memory) are overlaid onto the base. The remaining fields keep their base values.
patches (JSON6902) #
When you need to pinpoint an array element to add or remove it, or precisely change the value at a specific path, a JSON6902 patch is the right fit. You specify the action with op (add, replace, remove) and path.
# part of overlays/prod/kustomization.yaml
patches:
- target:
kind: Deployment
name: web
patch: |-
- op: replace
path: /spec/replicas
value: 6
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: LOG_LEVEL
value: infoYou pick the target with target and write the list of operations inside patch. The /- in the path is the notation for “append to the end of the array.” Use it for precise manipulations that strategic merge struggles to express.
Generators: auto-generating ConfigMap and Secret #
Kustomize can create ConfigMaps and Secrets via generators instead of writing them directly as manifests. The core value of a generator is the content hash suffix.
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- TIMEOUT=30
files:
- app.properties # load the file's contents into the ConfigMap as is
secretGenerator:
- name: app-secret
literals:
- PASSWORD=s3cr3tA ConfigMap created by a generator gets a hash computed from its content appended to the name, like app-config-7h8d9f2k4m. When the content changes, the hash changes, the name changes, and the Deployment’s Pod template that references it is automatically updated to point at the new name. As a result, changing a ConfigMap value triggers an automatic rolling update of the Pods.
This solves a chronic problem with hand-written ConfigMaps. If you change only the ConfigMap, the existing Pods keep running on the old values without ever seeing the change — but a generator forces a rolling update via the name change. If you want to turn off the hash suffix, add the following.
generatorOptions:
disableNameSuffixHash: trueBuild and apply #
You can preview Kustomize’s output with kubectl kustomize. It applies nothing to the cluster and emits only the final merged YAML to standard output.
# only print the build result (does not apply)
k kustomize overlays/prod
# save to a file for review
k kustomize overlays/prod > /tmp/prod-rendered.yamlOnce the review is done, apply it directly with the -k flag. You pass the directory path (where kustomization.yaml lives) as the argument.
# build the overlay and apply it to the cluster
k apply -k overlays/prod
# compare changes before applying
k diff -k overlays/prod
# deletion works the same way with -k
k delete -k overlays/prodOn the exam, the recommended order is always to preview with k kustomize first, then apply with k apply -k. That way you can visually verify before applying whether the patch merged as intended and whether namePrefix updated the selector references too.
The difference from Helm #
#9 Helm and Kustomize solve the same problem in different ways. Helm substitutes variables into a template to generate manifests, while Kustomize layers transformations on top of finished YAML with overlays. Helm is a package manager that also covers packaging, distribution, rollback, and dependencies, whereas Kustomize is a lightweight tool focused on manifest transformation. The two aren’t mutually exclusive — you can even combine them by feeding a Helm chart’s output through Kustomize for one more round of transformation.
Full example: base + dev overlay #
Let’s tie together the elements so far and complete a single base plus one dev overlay.
First, the base.
# 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.27
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: app-config# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yamlThe dev overlay references the base while declaring, all at once, a namePrefix attachment, a replica-count patch, and ConfigMap generation.
# overlays/dev/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: dev
namePrefix: dev-
commonLabels:
env: dev
patchesStrategicMerge:
- replica-patch.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=debug
- TIMEOUT=10Now check the build result and apply it.
# check the merged result
k kustomize overlays/dev
# apply
k apply -k overlays/devIn the build result, the Deployment name becomes dev-web, the ConfigMap name becomes dev-app-config-<hash>, and the envFrom reference is automatically updated to point at the same hashed name. The replicas become 2 via the patch, and every resource gets the env: dev label. Not a single line of the base manifest was edited directly.
Exam points #
- Built into kubectl. Kustomize requires no separate install — use
k apply -k <directory>andk kustomize <directory>right away. The argument to-kis not a file but the directory where kustomization.yaml lives. - Build preview.
k kustomize <directory>prints the final merged YAML without touching the cluster. Build the habit of verifying before applying. - resources points at the base. Putting the base directory path in an overlay’s
resourcespulls in the entire base and then layers transformations on top. - patchesStrategicMerge vs patches. Overwriting some fields with a same-shape fragment is strategic merge; pinpointing an array element to add, remove, or replace is JSON6902.
- Generator hash rolling. configMapGenerator and secretGenerator append a content hash to the name, so when a value changes the Pods roll automatically. To prevent it, use
disableNameSuffixHash: true. - commonLabels updates the selector too. Unlike attaching labels by hand, it produces no selector mismatch.
Wrap-up #
What this post locked in:
- Kustomize is the template-free overlay approach. It leaves the base’s pure YAML intact and layers only transformation rules on top to build per-environment manifests.
- The core fields of kustomization.yaml. resources, namespace, namePrefix/nameSuffix, commonLabels/commonAnnotations, images, replicas.
- The base/overlays structure. Keep a single base and have overlays reference it, declaring only the per-environment differences.
- Two kinds of patch. patchesStrategicMerge (partial YAML merge) and patches (JSON6902 precise manipulation).
- Generators. The hash suffix of configMapGenerator and secretGenerator triggers an automatic roll when a value changes.
- Build and apply. Preview with
k kustomizeand apply withk apply -k.
Next: Probes #
We’ve wrapped up the deployment-tooling bundle (Helm and Kustomize). Next we move to the signals that tell Kubernetes whether a deployed application is alive and ready to take traffic.
In #11 Probes: liveness, readiness, startup (exec/HTTP/TCP) we’ll build, hands-on, the role differences among the three kinds of probe, the three check methods (exec, HTTP, TCP), timing parameters like initialDelaySeconds, periodSeconds, and failureThreshold, and the exam-favorite pattern where “a misconfigured probe keeps restarting the Pod.”