| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Base and overlay layout, kustomization.yaml, kubectl kustomize preview, namePrefix, namespace, labels, replicas, unified patches, images, configMapGenerator, secretGenerator, kubectl apply -k, kubectl diff -k, kubectl delete -k, comparison tables, and common path and patch mistakes. Does not cover Helm chart inflation, GitOps controllers, remote Git bases in depth, Kustomize plugins, custom transformers, live kubectl patch workflows, or CI/CD pipeline design. |
| Related guides | Kubernetes ConfigMaps |
Kustomize lets you keep one set of shared manifests and layer environment-specific changes on top. In this walkthrough you build a small nginx Deployment and Service as a base, customize development and production overlays, preview the rendered YAML, and apply each overlay with kubectl apply -k.
What Is Kustomize?
Kustomize customizes Kubernetes manifests without editing the original resource files in your base directory. Each directory that participates in a build contains a kustomization.yaml file listing resources, transformations, patches, and generators.
Kustomize ships inside kubectl, so you do not need a separate install for the commands in this article.
Bases vs overlays
| Base | Overlay |
|---|---|
| Contains reusable manifests | Customizes a base |
| Shared across environments | Environment-specific |
| Avoids duplicated YAML | Adds patches and transformations |
| Can be rendered directly | Usually represents a deployable environment |
The usual layout splits concerns like this:
- A base holds manifests shared by every environment.
- An overlay references the base and adds environment-specific settings.
kubectl kustomize(orkustomize build) renders the final YAML before you apply it.
Build the Shared Base
Use a single application with a Deployment and a ClusterIP Service. Every overlay in this demo reuses the same base files.
Directory layout:
kustomize-demo/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── development/
│ ├── app.env
│ ├── app-tier.txt
│ └── kustomization.yaml
└── production/
├── credentials.txt
├── patch-resources.yaml
└── kustomization.yamlCreate the tree on your workstation:
mkdir -p kustomize-demo/base kustomize-demo/overlays/development kustomize-demo/overlays/productioncd kustomize-demoAll later kubectl kustomize and kubectl apply -k commands assume you are inside kustomize-demo.
Deployment and Service manifests
Save base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80Save base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80Keep version-specific image tags and replica counts out of the base when overlays will set them. Here the base uses nginx:1.25.4 and two replicas as sensible defaults that production will override.
Base kustomization
Save base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app: web
includeSelectors: true
includeTemplates: trueThe base kustomization.yaml lists those files and applies the app: web label to selectors and Pod templates.
Preview the base
Rendering before apply shows exactly what Kustomize will send to the API server.
kubectl kustomize base/Sample output (trimmed):
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web
spec:
ports:
- port: 80
targetPort: 80
selector:
app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
spec:
replicas: 2The output is plain Kubernetes YAML with no kustomization.yaml resource. Two documents appear because the base lists a Service and a Deployment.
If you install the standalone Kustomize binary, kustomize build base/ prints the same manifest set. This article uses kubectl kustomize as the primary command because it is already on the cluster admin workstation.
Create Development and Production Overlays
Create the target namespaces separately. namePrefix transforms the names of all rendered resources, so including a Namespace manifest inside a prefixed overlay would not leave the namespace named kustom-dev or kustom-prod.
kubectl create namespace kustom-devkubectl create namespace kustom-prodSample output:
namespace/kustom-dev created
namespace/kustom-prod createdDevelopment overlay
Add environment inputs for configMapGenerator:
printf 'APP_MODE=local\n' > overlays/development/app.envprintf 'frontend' > overlays/development/app-tier.txtSave overlays/development/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kustom-dev
namePrefix: dev-
labels:
- pairs:
environment: development
includeSelectors: false
includeTemplates: true
resources:
- ../../base
replicas:
- name: web
count: 1
patches:
- target:
kind: Service
name: web
patch: |-
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
- target:
kind: Deployment
name: web
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: nginx
envFrom:
- configMapRef:
name: app-config
configMapGenerator:
- name: app-config
literals:
- APP_ENV=development
envs:
- app.env
files:
- APP_TIER=app-tier.txtThis overlay:
- Sets
namespace: kustom-devon rendered resources - Adds the
dev-prefix so dev objects do not collide with production names - Sets the
environment: developmentlabel on templates - Scales the Deployment to one replica
- Patches the Service to
NodePort - Generates a ConfigMap and wires it into the Deployment through
envFrom
Render the development overlay:
kubectl kustomize overlays/development/Sample output (trimmed):
apiVersion: v1
data:
APP_ENV: development
APP_MODE: local
APP_TIER: frontend
kind: ConfigMap
metadata:
labels:
environment: development
name: dev-app-config-d454g2957h
namespace: kustom-dev
---
apiVersion: v1
kind: Service
metadata:
labels:
app: web
environment: development
name: dev-web
namespace: kustom-dev
spec:
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
environment: development
name: dev-web
namespace: kustom-dev
spec:
replicas: 1
template:
spec:
containers:
- envFrom:
- configMapRef:
name: dev-app-config-d454g2957h
image: nginx:1.25.4
name: nginxThe hash suffix on dev-app-config-d454g2957h comes from configMapGenerator. Kustomize rewrites configMapRef.name from the logical generator name app-config to the final prefixed, hashed name.
Production overlay
printf 'prod-file-token' > overlays/production/credentials.txtSave overlays/production/patch-resources.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: 100m
memory: 64Mi
envFrom:
- secretRef:
name: app-secretSave overlays/production/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kustom-prod
namePrefix: prod-
labels:
- pairs:
environment: production
includeSelectors: false
includeTemplates: true
resources:
- ../../base
replicas:
- name: web
count: 4
images:
- name: nginx
newTag: "1.27.0"
patches:
- path: patch-resources.yaml
secretGenerator:
- name: app-secret
literals:
- API_KEY=prod-demo-key
files:
- FILE_TOKEN=credentials.txtProduction sets four replicas, bumps the nginx tag to 1.27.0, applies container resource requests, generates a Secret, and wires it into the Deployment through envFrom.
Render production output:
kubectl kustomize overlays/production/ | grep -E 'replicas:|image:|cpu:|memory:|secretRef|envFrom'Sample output:
replicas: 4
- envFrom:
- secretRef:
name: prod-app-secret-m6kccbfbk7
image: nginx:1.27.0
cpu: 100m
memory: 64MiThe production overlay keeps the example small. Real teams often add more overlays per stage and wire image tags from CI build IDs rather than hand-editing kustomization.yaml for every release.
Customize Resources with Kustomize
Patches change selected fields on resources that Kustomize already loaded from the base or from resources. The unified patches field replaces older patchesStrategicMerge and patchesJson6902 entries.
Labels, namespaces, and name prefixes
namespaceinjects the target namespace into rendered resourcesnamePrefixandnameSuffixtransform resource names after the base manifests are loadedlabelswithincludeTemplates: trueadd labels to Pod templates without changing selectors whenincludeSelectorsisfalse
Create namespaces separately when you use namePrefix. Prefixing a Namespace manifest would change its name and break the namespace you intended to target.
Replica overrides
The replicas field is the simplest way to scale a Deployment by name:
replicas:
- name: web
count: 1Kustomize matches the base Deployment name web before namePrefix transforms the live object name to dev-web or prod-web.
Inline and file-based patches
The development overlay sets the Service type inline:
patches:
- target:
kind: Service
name: web
patch: |-
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePortInline patches work well for one or two field changes that you want visible beside other overlay settings.
Store larger or reusable patches in their own YAML files and list them under patches with path, as production does with patch-resources.yaml. Separate files are easier to review in pull requests and reuse across overlays.
Live edits with kubectl patch change objects already on the cluster. Kustomize patches change what gets rendered from Git. See kubectl command examples for imperative patch types when you need a one-off cluster edit without touching manifests.
Container image changes
The images field rewrites container image references without a patch on spec.template.spec.containers[].image.
Production overlay excerpt:
images:
- name: nginx
newTag: "1.27.0"Kustomize replaces nginx:1.25.4 from the base with nginx:1.27.0 in the rendered Deployment. You can also set newName when the registry host or repository path changes.
That pattern is cleaner than maintaining a patch whose only job is to swap an image string. Build, tag, and push the image first, then point overlays at the tag your registry published. See build and push container images for the workstation side of that workflow.
Generate ConfigMaps and Secrets
Generator input types
configMapGenerator creates ConfigMap objects from literals, env files, or arbitrary file content at build time.
Development overlay generator:
configMapGenerator:
- name: app-config
literals:
- APP_ENV=development
envs:
- app.env
files:
- APP_TIER=app-tier.txtliteralscreate individual keysenvsparse eachKEY=VALUEline into a separate ConfigMap keyfileswith an explicit key such asAPP_TIER=app-tier.txtstore the file contents under that key
Using files: - settings.properties without a key remap stores the entire file under a key named settings.properties, not as separate parsed keys.
secretGenerator follows the same pattern for Secret data:
secretGenerator:
- name: app-secret
literals:
- API_KEY=prod-demo-key
files:
- FILE_TOKEN=credentials.txtFor a Secret file to become a valid environment-variable key, map it explicitly with FILE_TOKEN=credentials.txt.
Reference generated objects
Reference the logical generator name in manifests. Kustomize automatically rewrites recognized references such as configMapRef.name and secretRef.name to the final prefixed, hashed name.
Development Deployment patch:
envFrom:
- configMapRef:
name: app-configProduction Deployment patch:
envFrom:
- secretRef:
name: app-secretNo separate “enable name reference updates” step is required.
Content hashes and old objects
Rendered ConfigMap and Secret names include a content hash by default (dev-app-config-d454g2957h and prod-app-secret-m6kccbfbk7 in this lab). When generator input changes, the suffix changes so a new object is created and recognized references update in the rendered Deployment.
Ordinary kubectl apply does not automatically prune the previous hashed object. Pruning or explicit deletion is a separate operation.
Treat kubectl kustomize output as sensitive when Secrets are rendered, even though values appear base64-encoded in YAML. See Kubernetes Secrets for mounting and access patterns.
Preview, Diff, Apply, and Verify
kubectl kustomize
kubectl kustomize prints the final multi-document YAML for a base or overlay directory. Use it whenever an overlay change does not look right on the cluster. The rendered document shows exact names after prefixes, namespace injection, and hash suffixes on generated objects.
kubectl apply -k
kubectl apply -k accepts a directory that contains kustomization.yaml, renders it with Kustomize, then applies the result. The -k option explicitly processes a kustomization directory and cannot be combined with -f.
Apply the development overlay:
kubectl apply -k overlays/development/Sample output:
configmap/dev-app-config-d454g2957h created
service/dev-web created
deployment.apps/dev-web createdEach line is a resource from the rendered set, not the kustomization.yaml file itself.
Wait until the Deployment becomes available before checking Pod status:
kubectl rollout status deployment/dev-web -n kustom-dev --timeout=120sSample output:
deployment "dev-web" successfully rolled outkubectl diff -k
kubectl diff -k compares the rendered overlay against objects already on the cluster.
kubectl diff -k overlays/development/Because the live resources match the rendered overlay, the command prints no diff and exits with status 0. Status 1 means differences were found, while a value greater than 1 indicates an error.
kubectl get deploy,svc,pods -n kustom-devSample output:
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/dev-web 1/1 1 1 15s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/dev-web NodePort 10.102.251.245 <none> 80:32618/TCP 15s
NAME READY STATUS RESTARTS AGE
pod/dev-web-64b8d5fcf8-6jfnx 1/1 Running 0 14sOne replica, the dev- prefix, and NodePort match the development overlay settings.
Verify both environments
Apply the production overlay:
kubectl apply -k overlays/production/Sample output:
secret/prod-app-secret-m6kccbfbk7 created
service/prod-web created
deployment.apps/prod-web createdWait for the production Deployment to finish rolling out:
kubectl rollout status deployment/prod-web -n kustom-prod --timeout=120sSample output:
deployment "prod-web" successfully rolled outVerify the core production customizations:
kubectl get deployment prod-web -n kustom-prod -o jsonpath='replicas={.spec.replicas} image={.spec.template.spec.containers[0].image}{"\n"}'Sample output:
replicas=4 image=nginx:1.27.0Verify the generated Secret reference was rewritten to the hashed name:
kubectl get deployment prod-web -n kustom-prod -o jsonpath='{.spec.template.spec.containers[0].envFrom[0].secretRef.name}{"\n"}'Sample output:
prod-app-secret-m6kccbfbk7The -k flag always points at a directory with kustomization.yaml, not at an individual manifest file.
Troubleshoot Common Kustomize Problems
| Symptom | Likely cause | Fix |
|---|---|---|
kustomization.yaml is not found |
-k points at a file or wrong directory |
Pass the directory that contains kustomization.yaml, for example kubectl apply -k overlays/development/ |
| Resource path cannot be resolved | Broken relative path in resources or patches |
Fix paths relative to the current kustomization.yaml (../../base from overlays/development/) |
| Patch does not match any resource | Wrong kind, name, or namespace in target |
Match the base resource name before namePrefix / nameSuffix, and include namespace in target when needed |
| Generated name changes after editing input | Content hash changed as designed | Reference the logical generator name such as app-config; Kustomize rewrites recognized references. Disable the suffix only when a stable name is intentionally required |
| Overlay changes do not appear on the cluster | Patch never matched, or wrong overlay applied | Run kubectl kustomize on the overlay and inspect the rendered YAML before apply -k |
kubectl apply -f ignores customizations |
-f bypasses overlay rendering, or -f on kustomization.yaml is invalid |
Use kubectl apply -k <directory> instead of applying individual files or kustomization.yaml with -f |
| Namespace not found on first apply | namespace set but namespace object missing |
Create the namespace separately before apply when you do not include a Namespace manifest in the overlay |
Disabling the hash suffix also removes the Pod-template name change that normally helps trigger a rollout. Generated ConfigMaps and Secrets receive the hash by default.
What's Next
- Kubernetes Pods and Pod Lifecycle
- Kubernetes Deployments, Rolling Updates and Rollbacks
- Kubernetes StatefulSet with Examples
References
- Kustomize documentation — bases, overlays, generators, and patch types
- Kubernetes documentation: Managing Kubernetes Objects Using Kustomize — official task guide
- kubectl reference: kubectl kustomize — built-in render command
Summary
You built a kustomize-demo tree with a shared nginx Deployment and Service base, then customized development and production overlays without copying YAML. Namespaces were created separately so namePrefix did not transform Namespace resources. Development added labels, a single replica, an inline Service patch, a generated ConfigMap wired through envFrom, and a hashed ConfigMap reference. Production raised replicas, swapped the nginx image tag, applied resource requests with a file patch, and generated a Secret referenced by the Deployment.
Preview with kubectl kustomize whenever an overlay change does not look right on the cluster. kubectl diff -k exits 0 when the rendered overlay matches the cluster. kubectl apply -k renders and applies the directory, while kubectl apply -f on ordinary files bypasses overlay transformations.
Kustomize fits teams that want plain Kubernetes manifests in Git without templating every field. For rollout mechanics after the image tag changes, continue with Deployments and rolling updates. For configuration and credentials consumed by Pods, see the ConfigMap and Secret guides linked above.

