Kustomize is a Kubernetes-native configuration management tool that lets us reuse plain YAML manifests and apply environment-specific changes on top of them. Instead of copying a full Deployment or Service manifest for every environment, we keep a shared base and create overlays for development, staging, production, or any other deployment variant.
A very important part of Kustomize is that not every change requires a patch. Some changes are common enough that Kustomize provides top-level transformer fields for them. These transformations are direct, readable, and usually easier to maintain than patch files.
In this article, we will focus on the straightforward transformations that can be configured directly in a kustomization.yaml file:
namespacenamePrefixnameSuffixlabelscommonAnnotationsimagesreplicas
We will also discuss practical gotchas, including selector immutability, modern label syntax, image replacement behavior, and when a normal patch is still the better choice.
Why Direct Transformations Matter
When we manage Kubernetes manifests for multiple environments, we often need the same application structure with small differences.
For example:
| Environment | Namespace | Image tag | Replicas | Naming |
|---|---|---|---|---|
| Development | dev | 1.27.1 | 1 | dev-...-alpha |
| Staging | staging | 1.27.1-rc | 2 | staging-... |
| Production | prod | 1.27.0 | 4 | prod-... |
Without Kustomize, it is common to end up with duplicated YAML files. That creates maintenance overhead because every structural change has to be copied to multiple places.
Kustomize solves this by allowing us to keep the shared resources in a base and place environment-specific changes in overlays.
The base remains untouched. The overlay describes how the final rendered manifests should be changed.
The Base and Overlay Pattern
A typical Kustomize project looks like this:
nginx-app/
base/
deployment.yaml
service.yaml
kustomization.yaml
overlays/
dev/
kustomization.yaml
prod/
kustomization.yaml
The base directory contains reusable Kubernetes resources. The overlays directory contains environment-specific configuration.
The base does not need to know about the overlays. This is important because the same base can be reused by multiple overlays.
Example Base Manifests
Let’s start with a small NGINX application.
Create the directory structure:
mkdir -p nginx-app/base nginx-app/overlays/dev nginx-app/overlays/prod
Create nginx-app/base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
template:
metadata:
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx:1.27.0
ports:
- containerPort: 80
Create nginx-app/base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
selector:
app.kubernetes.io/name: nginx
ports:
- name: http
port: 80
targetPort: 80
Create nginx-app/base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
Now render the base:
kubectl kustomize nginx-app/base
This command only builds and prints the final YAML. It does not apply anything to the cluster.
A Development Overlay With Direct Transformations
Now let’s create a development overlay that changes the namespace, adds a name prefix and suffix, applies common metadata, changes the image tag, and sets the replica count.
Create nginx-app/overlays/dev/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: dev
namePrefix: dev-
nameSuffix: -alpha
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: ecommerce
environment: dev
tier: frontend
commonAnnotations:
maintainer: finance@example.com
repository: https://git.example.com/ecommerce/nginx-app
images:
- name: nginx
newTag: "1.27.1"
replicas:
- name: nginx
count: 1
Render the development overlay:
kubectl kustomize nginx-app/overlays/dev
The rendered output will include changes like these:
metadata:
name: dev-nginx-alpha
namespace: dev
The Deployment image will become:
image: nginx:1.27.1
The labels will be added to resource metadata, selectors, and pod templates because includeSelectors: true is enabled.
Transformation Overview
The following table summarizes the most useful direct transformations.
| Field | Example | Effect |
|---|---|---|
namespace | namespace: dev | Assigns namespaced resources to a specific namespace. |
namePrefix | namePrefix: dev- | Prepends a string to resource names. |
nameSuffix | nameSuffix: -alpha | Appends a string to resource names. |
labels | labels: [...] | Adds labels to resources, and optionally to selectors and pod templates. |
commonAnnotations | commonAnnotations: {...} | Adds annotations to resources and pod templates. |
images | newTag: "1.27.1" | Rewrites container image names, tags, or digests. |
replicas | count: 3 | Sets the replica count for matching workload resources. |
These transformations are especially useful for overlays because they describe environment-level changes clearly.
The namespace Transformer
The namespace field sets the namespace on namespaced resources.
namespace: dev
This is useful when the same base should be deployed to different namespaces:
overlays/
dev/ -> namespace: dev
staging/ -> namespace: staging
prod/ -> namespace: prod
Important detail: setting namespace: dev does not automatically guarantee that the namespace exists in the cluster.
You have two common options:
- Create the namespace separately:
kubectl create namespace dev
- Add a
Namespacemanifest to the overlay:
apiVersion: v1
kind: Namespace
metadata:
name: dev
Then include it in the overlay:
resources:
- ../../base
- namespace.yaml
For small labs, creating the namespace manually is fine. For GitOps-style environments, it is usually better to manage the namespace declaratively.
The namePrefix and nameSuffix Transformers
The namePrefix and nameSuffix fields modify resource names.
namePrefix: dev-
nameSuffix: -alpha
A base resource called nginx becomes:
dev-nginx-alpha
This is useful when you deploy multiple variants of the same application into the same namespace. For example, you might run a development and production-like version side by side:
dev-nginx-alpha
prod-nginx
Kustomize also updates many Kubernetes references automatically. For example, if a Deployment references a ConfigMap that is also managed by the same Kustomize build, Kustomize can update the reference to the transformed name.
This helps prevent manual find-and-replace mistakes.
When Name Prefixes and Suffixes Are Useful
Name transformations are useful when:
- multiple overlays are deployed into the same namespace;
- you want generated resources to be easy to identify;
- your CI/CD pipeline creates temporary preview environments;
- you want to distinguish alpha, beta, canary, or stable variants.
When to Be Careful
Be careful with very long prefixes and suffixes. Kubernetes resource names often have DNS-related length and character restrictions. Workload-generated resources, such as Pods created by Deployments, also derive their names from the parent resource name. Long names can become awkward quickly.
Use names that are clear but short.
Good examples:
namePrefix: dev-
nameSuffix: -v1
Avoid overly long names:
namePrefix: development-environment-for-finance-team-
nameSuffix: -experimental-alpha-release-candidate
Labels: Modern Syntax Instead of commonLabels
Older Kustomize examples often use commonLabels:
commonLabels:
app: nginx
environment: dev
In modern Kustomize, prefer the labels field because it gives more control over where labels are applied.
Example:
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: ecommerce
environment: dev
tier: frontend
With includeSelectors: true, Kustomize adds the labels not only to metadata.labels, but also to selectors and pod templates where appropriate.
For a Deployment, this can affect:
metadata:
labels: ...
spec:
selector:
matchLabels: ...
spec:
template:
metadata:
labels: ...
For a Service, this can affect:
metadata:
labels: ...
spec:
selector: ...
This is powerful, but it also means you must think carefully before adding labels to selectors.
Selector Labels Are Not Just Metadata
A label used in metadata.labels is metadata. A label used in a selector is part of how Kubernetes connects resources.
For example, a Service selects Pods using labels:
spec:
selector:
app.kubernetes.io/name: nginx
A Deployment also uses a selector to determine which Pods belong to it:
spec:
selector:
matchLabels:
app.kubernetes.io/name: nginx
This is why includeSelectors: true is powerful. It helps keep selectors and pod labels aligned.
But selector changes are not harmless.
For Deployments in apps/v1, the selector is immutable after the Deployment is created. If you deploy a resource first and later change the selector labels through Kustomize, Kubernetes can reject the update with an error similar to:
The Deployment "dev-nginx-alpha" is invalid: spec.selector: Invalid value: field is immutable
This commonly happens when you add a new labels transformer with includeSelectors: true to an overlay that was already applied.
Practical Rule
Use includeSelectors: true only for stable identity labels that you decide before the first deployment.
Good selector labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: ecommerce
tier: frontend
Riskier selector labels:
version: "1.27.1"
commit: "a1b2c3d"
deployment-date: "2026-05-10"
Version and commit labels can be useful on pod templates, but they should usually not become part of stable selectors.
Applying Labels Without Changing Selectors
Sometimes you want labels on resources and pod templates, but not on selectors.
Use includeSelectors: false:
labels:
- includeSelectors: false
pairs:
cost-center: finance
owner: platform-team
Depending on your Kustomize version, you may also use includeTemplates to explicitly control whether labels are applied to pod templates.
Example:
labels:
- includeSelectors: false
includeTemplates: true
pairs:
commit: "a1b2c3d"
release: "2026-05-10"
This pattern is useful for labels that help with observability, cost allocation, or traceability, but should not participate in resource selection.
The commonAnnotations Transformer
Annotations are also key-value metadata, but they are intended for non-identifying information. They are commonly used for tooling, automation, documentation, integrations, and operational hints.
Example:
commonAnnotations:
maintainer: finance@example.com
repository: https://git.example.com/ecommerce/nginx-app
runbook: https://wiki.example.com/runbooks/nginx-app
Kustomize applies these annotations to resources and workload pod templates.
This is useful for:
- maintainers and ownership;
- repository links;
- runbook links;
- monitoring hints;
- deployment metadata;
- policy or governance tooling.
Unlike labels, annotations are not normally used by Kubernetes selectors. This makes them a better place for information that should not be used to group or select resources.
The images Transformer
The images transformer lets you change container images without writing a patch.
Base Deployment:
containers:
- name: nginx
image: nginx:1.27.0
Development overlay:
images:
- name: nginx
newTag: "1.27.1"
Rendered result:
image: nginx:1.27.1
You can also replace the image registry or repository:
images:
- name: nginx
newName: registry.example.com/platform/nginx
newTag: "1.27.1"
Rendered result:
image: registry.example.com/platform/nginx:1.27.1
You can also use an image digest:
images:
- name: nginx
newName: registry.example.com/platform/nginx
digest: sha256:1111111111111111111111111111111111111111111111111111111111111111
A digest is more immutable than a tag. In production, using image digests can make deployments more reproducible because the digest points to a specific image content hash.
Image Transformer Tips
Use the images transformer when:
- dev and prod use different image tags;
- CI/CD updates image tags during release;
- production uses an internal registry mirror;
- you want to pin an image by digest.
Avoid using latest in long-lived environments. It makes it harder to understand exactly what version is running.
The replicas Transformer
The replicas transformer lets each overlay set its desired replica count.
Development overlay:
replicas:
- name: nginx
count: 1
Production overlay:
replicas:
- name: nginx
count: 4
This is cleaner than patching spec.replicas in every environment.
Create nginx-app/overlays/prod/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: prod
namePrefix: prod-
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: ecommerce
environment: prod
tier: frontend
commonAnnotations:
maintainer: finance@example.com
repository: https://git.example.com/ecommerce/nginx-app
images:
- name: nginx
newTag: "1.27.0"
replicas:
- name: nginx
count: 4
Render it:
kubectl kustomize nginx-app/overlays/prod
The production Deployment will have:
replicas: 4
Replicas and Horizontal Pod Autoscaling
Be careful when a HorizontalPodAutoscaler manages the same workload. If an HPA is responsible for scaling, you generally should not keep fighting it by repeatedly applying a fixed spec.replicas value from Git.
A common approach is:
- set fixed replicas in simple environments;
- use HPA in environments where autoscaling is required;
- avoid unnecessary manual changes to
spec.replicaswhen HPA owns scaling behavior.
Preview, Apply, and Delete
Kustomize is easy to test because you can render the final YAML before applying it.
Preview the development overlay:
kubectl kustomize nginx-app/overlays/dev
Apply the development overlay:
kubectl apply -k nginx-app/overlays/dev
Delete the development overlay:
kubectl delete -k nginx-app/overlays/dev
Previewing first is a good habit. It allows you to inspect the final manifests and confirm that prefixes, suffixes, labels, annotations, images, and replicas are exactly what you expect.
Useful Inspection Commands
After applying an overlay, inspect the generated resources:
kubectl get deployment,service -n dev
Check labels:
kubectl get pods -n dev --show-labels
Check the rendered Deployment:
kubectl get deployment dev-nginx-alpha -n dev -o yaml
Check which image is running:
kubectl get deployment dev-nginx-alpha -n dev \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
Check replica count:
kubectl get deployment dev-nginx-alpha -n dev \
-o jsonpath='{.spec.replicas}{"\n"}'
Direct Transformations vs Patches
Direct transformations are best for changes that are common, structured, and supported by Kustomize as first-class fields.
Use direct transformations for:
| Change | Recommended field |
|---|---|
| Put resources in a namespace | namespace |
| Add environment prefixes or suffixes | namePrefix, nameSuffix |
| Add shared labels | labels |
| Add shared annotations | commonAnnotations |
| Change image tags or registries | images |
| Change replica count | replicas |
Use patches when you need targeted changes to specific fields that are not covered by direct transformers.
Good patch use cases:
- change container resource requests and limits;
- add or change environment variables;
- add volumes and volume mounts;
- change probes;
- modify Service ports;
- change Ingress hosts;
- customize CRD-specific fields.
Example of a patch-worthy change:
patches:
- target:
kind: Deployment
name: nginx
patch: |-
- op: add
path: /spec/template/spec/containers/0/env
value:
- name: FEATURE_FLAG
value: "true"
A good rule is simple: if Kustomize has a clear top-level transformer for the change, use it. If the change is resource-specific or deeply nested, use a patch.
Common Pitfalls
1. The Namespace Does Not Exist
This overlay:
namespace: dev
does not automatically create the dev namespace unless you also include a Namespace resource or create it separately.
2. Selector Labels Changed After Initial Deployment
If you add new selector labels to an existing Deployment, Kubernetes may reject the change because Deployment selectors are immutable.
Plan selector labels up front.
3. commonLabels in Older Examples
Many tutorials still show:
commonLabels:
environment: dev
Prefer modern syntax:
labels:
- includeSelectors: true
pairs:
environment: dev
The modern form is more explicit and gives better control over selectors and templates.
4. Image Names Do Not Match
If the images transformer does not update the image, check that the name matches the image used in the base manifest.
Base:
image: nginx:1.27.0
Overlay:
images:
- name: nginx
newTag: "1.27.1"
If your base uses a full registry path, match the image accordingly:
image: registry.example.com/platform/nginx:1.27.0
Overlay:
images:
- name: registry.example.com/platform/nginx
newTag: "1.27.1"
5. Too Much Logic in Overlays
Kustomize is not a programming language. It is not designed around conditionals, loops, or complex templating.
If your overlays become too clever, consider whether you need:
- simpler overlays;
- components;
- a GitOps controller configuration;
- Helm for chart-style parameterization;
- a higher-level platform pipeline that renders manifests before Kustomize.
Recommended Overlay Style
A clean overlay should be easy to read.
Example:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: dev
namePrefix: dev-
nameSuffix: -alpha
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: ecommerce
environment: dev
tier: frontend
commonAnnotations:
maintainer: finance@example.com
repository: https://git.example.com/ecommerce/nginx-app
images:
- name: nginx
newTag: "1.27.1"
replicas:
- name: nginx
count: 1
This overlay tells a clear story:
- deploy the base resources;
- place them in the
devnamespace; - rename resources with a dev-specific prefix and suffix;
- add shared labels and annotations;
- use a development image tag;
- run one replica.
That is exactly what an overlay should do.
Practical Workflow
A practical Kustomize workflow looks like this:
# 1. Render and inspect
kubectl kustomize nginx-app/overlays/dev
# 2. Apply
kubectl apply -k nginx-app/overlays/dev
# 3. Verify
kubectl get all -n dev
# 4. Inspect final resource details
kubectl get deployment dev-nginx-alpha -n dev -o yaml
When changing an overlay, render the manifest before applying it:
kubectl kustomize nginx-app/overlays/dev > rendered-dev.yaml
Then inspect the file:
less rendered-dev.yaml
This is especially useful in code reviews because reviewers can see the final generated Kubernetes manifests.
Conclusion
Kustomize overlays are powerful because they allow us to reuse a shared base while applying environment-specific changes declaratively. Many of these changes do not require patches.
For common environment-level differences, use direct transformer fields:
- use
namespaceto assign resources to a namespace; - use
namePrefixandnameSuffixto distinguish resource names; - use
labelsto apply consistent metadata and, when appropriate, selectors; - use
commonAnnotationsfor non-identifying operational metadata; - use
imagesto update image names, tags, or digests; - use
replicasto control workload size per environment.
The key is to keep the base clean and reusable, while keeping overlays clear, explicit, and small. When a change is cross-cutting and supported by a Kustomize transformer, use the transformer. When the change is resource-specific or deeply nested, use a patch.
That balance keeps Kubernetes configuration manageable without duplicating YAML across every environment.
References
- Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
- Kubernetes: kubectl kustomize Reference - kubernetes[.]io/docs/reference/kubectl/generated/kubectl_kustomize/
- Kubernetes: Labels and Selectors - kubernetes[.]io/docs/concepts/overview/working-with-objects/labels/
- Kubernetes: Deployments - kubernetes[.]io/docs/concepts/workloads/controllers/deployment/
- Kubernetes SIGs: Kustomize Repository - github[.]com/kubernetes-sigs/kustomize
- Kubernetes SIGs: Kustomize Transformer Configurations - github[.]com/kubernetes-sigs/kustomize/blob/master/examples/transformerconfigs/README.md