Managing Kubernetes manifests starts simple. You create a Deployment, add a Service, maybe add a ConfigMap, and apply everything with kubectl apply -f.
Then the application grows.
You need one version for development, another for staging, and another for production. The development environment might use one replica. Production might use three replicas, resource limits, different labels, a different namespace, and maybe a different image tag. Without a configuration management tool, the usual result is duplicated YAML files with small differences spread across multiple folders.
That is exactly the problem Kustomize solves.
Kustomize allows you to declaratively customize Kubernetes resources while keeping the original manifests reusable. Instead of copying the same YAML files for every environment, you define a common base and then apply environment-specific overlays on top of it.
Kustomize is especially useful when you want to stay close to plain Kubernetes YAML. It does not require a template language. It works by transforming real Kubernetes manifests.
What Is Kustomize?
Kustomize is a Kubernetes-native configuration management tool. It allows you to customize Kubernetes objects through a kustomization.yaml file.
The main idea is simple:
- You keep common Kubernetes manifests in one place.
- You declare which changes should be applied.
- Kustomize renders the final YAML.
- You apply the rendered result to the cluster.
For example, you can use Kustomize to:
- Add the same namespace to multiple resources.
- Add common labels and annotations.
- Add a name prefix or suffix.
- Patch only specific fields of a resource.
- Generate
ConfigMapandSecretresources from files or literals. - Change container image names or tags.
- Reuse the same base configuration for
dev,staging, andprod.
Kustomize is available as a standalone binary, but it is also built into kubectl. That means you can render manifests with:
kubectl kustomize <directory>
And you can apply them directly with:
kubectl apply -k <directory>
The -k flag tells kubectl to look for a kustomization.yaml file instead of applying every YAML file blindly.
Why Plain YAML Becomes Hard to Maintain
Imagine this basic Kubernetes setup:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27.0
ports:
- containerPort: 80
This is fine for one environment.
But now suppose production needs:
replicas: 3- namespace
prod - different labels
- CPU and memory limits
- a production image tag
- stricter annotations
A common but poor solution is to copy the same file into another directory and change a few lines. That works once. It becomes painful when you have tens of manifests.
The problem is not only duplication. The bigger problem is drift. If the base deployment changes, you must remember to update every copied version. One missed update can create subtle differences between environments.
Kustomize avoids this by separating shared configuration from environment-specific customization.
Core Concepts
Base
A base is a directory that contains reusable Kubernetes manifests and a kustomization.yaml file.
The base should describe the application in a generic way. It usually contains resources such as:
DeploymentServiceConfigMapServiceAccountIngressNetworkPolicy
The base should avoid environment-specific details where possible.
Overlay
An overlay is another Kustomize directory that references a base and applies changes on top of it.
For example:
app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── replicas-patch.yaml
└── prod/
├── kustomization.yaml
├── replicas-patch.yaml
└── resources-patch.yaml
The base contains the common application. The dev and prod overlays reuse it and modify only what they need.
Patches
Patches allow you to change only specific parts of a resource.
For example, you can patch only the number of replicas:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
You do not need to repeat the full deployment YAML.
Cross-Cutting Fields
Cross-cutting fields are changes that apply across multiple resources.
Common examples include:
namespacenamePrefixnameSuffix- labels
- annotations
Instead of adding the same namespace manually to every manifest, you can declare it once in kustomization.yaml.
First Kustomize Project
Let’s create a small NGINX application with a Deployment and a Service.
Create this folder structure:
kustomize-nginx/
└── base/
├── deployment.yaml
├── service.yaml
└── kustomization.yaml
Deployment
Create base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/component: web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/component: web
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/component: web
spec:
containers:
- name: nginx
image: nginx:1.27.0
ports:
- containerPort: 80
name: http
Service
Create base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/component: web
spec:
selector:
app.kubernetes.io/name: nginx
app.kubernetes.io/component: web
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
Kustomization File
Create base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
Now render the result:
kubectl kustomize base
Kustomize will output the combined manifests for the Deployment and the Service.
To apply the base directly:
kubectl apply -k base
To delete the resources managed by this kustomization:
kubectl delete -k base
kubectl apply -f vs kubectl apply -k
This distinction is important.
When you run:
kubectl apply -f .
kubectl applies the manifest files it finds in the directory.
When you run:
kubectl apply -k .
kubectl looks for a kustomization.yaml file, renders the configured resources, applies transformations and patches, and then applies the final result.
With -k, the kustomization.yaml file controls what is included. A YAML file sitting in the same directory is not automatically applied unless it is referenced by the kustomization.
This makes the deployment more intentional.
Adding a Namespace with Kustomize
Suppose we want to deploy the application into a dev namespace.
One option is to add this to every resource:
metadata:
namespace: dev
That works, but it creates repetition. With Kustomize, we can declare the namespace once:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- deployment.yaml
- service.yaml
Now the rendered Deployment and Service will include:
metadata:
namespace: dev
However, the namespace itself must exist before namespaced resources are created. You can create it separately:
kubectl create namespace dev
Or with a manifest:
apiVersion: v1
kind: Namespace
metadata:
name: dev
In many real projects, namespaces are treated as platform-level resources rather than application-level resources. That means I usually prefer to manage namespaces separately from the application kustomization, unless the application is truly responsible for creating its own namespace.
Creating Dev and Prod Overlays
Now let’s create a more realistic structure:
kustomize-nginx/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── replicas-patch.yaml
└── prod/
├── kustomization.yaml
├── replicas-patch.yaml
└── resources-patch.yaml
Dev Overlay
Create overlays/dev/replicas-patch.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
Create overlays/dev/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
namePrefix: dev-
labels:
- pairs:
environment: dev
includeSelectors: false
resources:
- ../../base
patches:
- path: replicas-patch.yaml
images:
- name: nginx
newTag: 1.27.0
Render it:
kubectl kustomize overlays/dev
Apply it:
kubectl create namespace dev
kubectl apply -k overlays/dev
The final resources will be named with the dev- prefix, for example:
dev-nginx
Prod Overlay
Create overlays/prod/replicas-patch.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
Create overlays/prod/resources-patch.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
Create overlays/prod/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
namePrefix: prod-
labels:
- pairs:
environment: prod
includeSelectors: false
commonAnnotations:
managed-by: kustomize
owner: platform-team
resources:
- ../../base
patches:
- path: replicas-patch.yaml
- path: resources-patch.yaml
images:
- name: nginx
newTag: 1.27.0
Apply it:
kubectl create namespace prod
kubectl apply -k overlays/prod
Now the same base application can be deployed differently into dev and prod.
Previewing Changes Before Applying
One of the best habits with Kustomize is to always inspect the rendered YAML before applying it.
Use:
kubectl kustomize overlays/prod
You can also compare what would change in the cluster:
kubectl diff -k overlays/prod
Then apply:
kubectl apply -k overlays/prod
This is a clean workflow for CI/CD pipelines as well:
kubectl kustomize overlays/prod > rendered.yaml
kubectl diff -f rendered.yaml
kubectl apply -f rendered.yaml
Or simply:
kubectl diff -k overlays/prod
kubectl apply -k overlays/prod
Working with Patches
Kustomize supports different patching approaches. The two most common are:
- strategic merge style patches
- JSON 6902 patches
Strategic Merge Style Patch
This is the style used above:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
It looks like a partial Kubernetes manifest. Kustomize matches it to the target resource and merges the specified fields.
This is very readable for common Kubernetes objects.
JSON 6902 Patch
A JSON patch is more explicit. It uses operations such as add, replace, and remove.
For example, to replace the replica count:
- op: replace
path: /spec/replicas
value: 3
Then reference it from kustomization.yaml:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: nginx
path: replicas-json-patch.yaml
JSON patches are useful when you need precise control over a specific field, especially with custom resources or fields where strategic merge behavior is not enough.
Changing Images Without Patches
A common environment difference is the image tag.
Kustomize provides the images field so you do not need to patch the container manually.
images:
- name: nginx
newName: nginx
newTag: 1.27.0
You can also replace the registry:
images:
- name: nginx
newName: registry.example.com/platform/nginx
newTag: 1.27.0
This is useful in CI/CD when the base manifest contains a stable image name and the overlay or pipeline injects the final tag.
For stronger production reproducibility, consider pinning images by digest:
images:
- name: nginx
digest: sha256:exampledigest
Generating ConfigMaps
Kustomize can generate ConfigMap resources from literals, files, or environment files.
Example:
base/
├── app.properties
└── kustomization.yaml
Create base/app.properties:
APP_MODE=production
LOG_LEVEL=info
Then add this to base/kustomization.yaml:
configMapGenerator:
- name: nginx-config
files:
- app.properties
Kustomize will generate a ConfigMap.
By default, generated ConfigMap and Secret names include a content hash suffix. This is useful because when the content changes, the generated resource name changes too. Workloads that reference it can then roll out with the new configuration.
Example generated name:
nginx-config-g4hk9g2ff8
You can disable the suffix:
generatorOptions:
disableNameSuffixHash: true
But be careful. Disabling the hash can make configuration changes less visible to workload rollout mechanisms.
Generating Secrets
Kustomize can also generate Kubernetes Secret resources.
Example:
secretGenerator:
- name: nginx-secret
literals:
- username=admin
- password=change-me
This is convenient for local labs and examples, but be careful in real projects.
A Kubernetes Secret is not automatically a secure secret-management strategy. Kustomize can generate the Secret object, but it does not magically protect plaintext values in your Git repository. For production, consider using an external secret management approach such as:
- External Secrets Operator
- Sealed Secrets
- SOPS with Flux or another GitOps workflow
- cloud-provider secret managers
A safe rule is: do not commit real plaintext credentials just because they are under secretGenerator.
Common Labels and Annotations
Labels and annotations help with grouping, selection, ownership, auditing, and automation.
Example:
labels:
- pairs:
app.kubernetes.io/part-of: demo-platform
environment: prod
includeSelectors: false
commonAnnotations:
managed-by: kustomize
contact: platform-team
Be careful with selector labels. Changing labels that are part of selectors can be dangerous because some selectors are immutable after creation, and changing pod-template labels can affect how services and deployments match pods.
Use includeSelectors: true only when you intentionally want Kustomize to update selectors as well.
Name Prefixes and Suffixes
Kustomize can add a prefix or suffix to resource names:
namePrefix: prod-
nameSuffix: -v1
This can produce names such as:
prod-nginx-v1
This is useful when multiple variants of the same base are deployed into the same cluster.
However, do not overuse name transformations. Stable and predictable names are often easier to debug. Prefixes and suffixes are useful when they express a real distinction, such as environment, tenant, or release variant.
Kustomize vs Helm
Kustomize and Helm are often compared, but they solve different problems.
| Dimension | Kustomize | Helm |
|---|---|---|
| Main purpose | Customize existing Kubernetes YAML manifests | Package, install, upgrade, and version Kubernetes applications |
| Configuration style | Plain YAML transformations | Templates plus values files |
| Template language | No template language | Go template language |
| Best for | Environment overlays and small-to-medium customization | Application packaging, dependency management, reusable charts |
| Learning curve | Lower if you already know Kubernetes YAML | Higher because you must learn chart structure and templating |
| Output | Rendered Kubernetes manifests | Rendered Kubernetes manifests applied as Helm releases |
| Versioning model | Usually handled by Git or external tooling | Built into chart versions and releases |
Use Kustomize when you want to customize raw Kubernetes manifests without introducing templating.
Use Helm when you want to package an application, manage chart dependencies, publish reusable charts, or operate with Helm release history.
They are not mutually exclusive. A common pattern is:
- Use Helm to install a third-party application.
- Use Kustomize to patch the rendered output or manage environment-specific overlays.
That said, avoid combining tools without a clear reason. Every extra layer adds operational complexity.
A Practical Full Example
Here is a complete minimal project.
kustomize-nginx/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── replicas-patch.yaml
└── prod/
├── kustomization.yaml
├── replicas-patch.yaml
└── resources-patch.yaml
Base Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
Dev Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
namePrefix: dev-
resources:
- ../../base
patches:
- path: replicas-patch.yaml
images:
- name: nginx
newTag: 1.27.0
Prod Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
namePrefix: prod-
labels:
- pairs:
environment: prod
includeSelectors: false
commonAnnotations:
managed-by: kustomize
resources:
- ../../base
patches:
- path: replicas-patch.yaml
- path: resources-patch.yaml
images:
- name: nginx
newTag: 1.27.0
Commands
Preview development:
kubectl kustomize overlays/dev
Apply development:
kubectl create namespace dev
kubectl apply -k overlays/dev
Preview production:
kubectl kustomize overlays/prod
Check production changes:
kubectl diff -k overlays/prod
Apply production:
kubectl create namespace prod
kubectl apply -k overlays/prod
Delete development:
kubectl delete -k overlays/dev
Delete production:
kubectl delete -k overlays/prod
Recommended Project Structure
For most applications, this structure works well:
my-app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patches/
│ └── replicas.yaml
├── staging/
│ ├── kustomization.yaml
│ └── patches/
│ └── replicas.yaml
└── prod/
├── kustomization.yaml
└── patches/
├── replicas.yaml
└── resources.yaml
Keep the base boring and reusable. Put environment-specific decisions in overlays.
Best Practices
Keep the Base Environment-Neutral
The base should not know whether it is deployed to dev, staging, or prod.
Avoid putting these in the base unless they are truly universal:
- namespace
- replica count for a specific environment
- production-only annotations
- environment-specific image tags
- environment-specific resource limits
Use Small Patches
Small patches are easier to review.
Prefer this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
Over a large patch that repeats the entire deployment.
Render Before You Apply
Always inspect the result:
kubectl kustomize overlays/prod
For production, use:
kubectl diff -k overlays/prod
This helps catch wrong namespaces, wrong names, incorrect image tags, and broken patches before they reach the cluster.
Be Careful with Selectors
Selectors are not just metadata. They control how resources match other resources.
For example:
- A
Serviceselector decides which pods receive traffic. - A
Deploymentselector decides which pods belong to the deployment.
Changing selector labels carelessly can break traffic routing or cause immutable-field errors.
Do Not Store Real Secrets in Git
Kustomize can generate Kubernetes Secret objects, but it does not encrypt your Git repository.
For production secrets, use a proper secret-management workflow.
Prefer Explicit Over Clever
Kustomize is powerful, but your manifests should still be easy to understand.
Avoid overly complex overlays where nobody can predict the rendered YAML without running the build. The final result should be clear, reviewable, and easy to debug.
Common Troubleshooting
kubectl apply -k Does Not Apply a File in the Directory
Kustomize only applies resources referenced by kustomization.yaml.
Check:
resources:
- deployment.yaml
- service.yaml
If a file is not listed, it is not part of the rendered output.
Namespace Not Found
If your kustomization sets:
namespace: dev
The namespace still needs to exist, unless you include a namespace manifest in the rendered resources.
Create it first:
kubectl create namespace dev
Patch Is Not Applied
Check that the patch target matches the resource name before name transformations.
For example, if the base deployment is named nginx, the patch should usually target:
metadata:
name: nginx
Even if the overlay later adds:
namePrefix: prod-
Kustomize applies transformations in a controlled build process, so patches usually refer to the original resource identity.
Generated ConfigMap Name Keeps Changing
That is expected when the content changes. Kustomize appends a hash suffix to generated ConfigMap and Secret names by default.
This is useful for rollout behavior. Disable it only when you have a clear reason:
generatorOptions:
disableNameSuffixHash: true
When Should You Use Kustomize?
Kustomize is a strong choice when:
- You already have plain Kubernetes manifests.
- You need environment-specific variations.
- You want to avoid YAML duplication.
- You do not need a full package manager.
- You want a low-friction tool built into
kubectl. - You want Git-friendly, reviewable configuration changes.
It is less ideal when:
- You need reusable application packages for many teams.
- You need dependency management between packaged applications.
- You want chart repositories and release versioning.
- You need advanced template logic with loops and conditionals.
In those cases, Helm may be a better fit.
Final Thoughts
Kustomize gives Kubernetes teams a practical way to manage configuration without abandoning native YAML.
The key benefit is separation:
- The base describes what the application is.
- The overlay describes how that application changes for a specific environment.
This keeps manifests reusable, reduces duplication, and makes environment differences explicit. For many Kubernetes projects, especially internal applications with dev, staging, and prod environments, Kustomize offers a clean and maintainable approach.
Start simple:
kubectl kustomize overlays/dev
kubectl apply -k overlays/dev
Then grow from there with patches, image overrides, generated ConfigMaps, generated Secrets, labels, annotations, and production review workflows.
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: Declarative Management of Kubernetes Objects Using Configuration Files - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/declarative-config/
- Kustomize: Kubernetes Native Configuration Management - kustomize[.]io/
- Helm: Charts - helm[.]sh/docs/topics/charts/
- Helm: Chart Template Guide - helm[.]sh/docs/chart_template_guide/
- Argo CD: Kustomize User Guide - argo-cd[.]readthedocs[.]io/en/release-3.0/user-guide/kustomize/
- Flux: Kustomization API - fluxcd[.]io/flux/components/kustomize/kustomizations/