You don’t need a templating engine to manage environment-specific Kubernetes configs. You need overlays. Here’s how Kustomize gives you environment-aware configuration without a single curly brace.
Kubernetes Zero to Hero · Bonus Episode ·
Every team hits the same wall eventually.
You’ve written clean Kubernetes manifests for your application. They work perfectly in development. Now you need to deploy to staging — same app, but fewer replicas, different resource limits, a different image tag, and a different ConfigMap value or two. Then production — same again, but more replicas, stricter resource limits, a real domain name, and TLS enabled.
The naive solution is to copy your YAML three times and edit each copy. Now you have three files to keep in sync. Every change to the base configuration needs to be made in three places. You will miss one. You always miss one.
The slightly-less-naive solution is Helm. But Helm templating — replacing values with {{ .Values.replicaCount }} — turns readable YAML into a programming language with moustaches. Small charts are fine. Large charts become something you debug rather than read.
Kustomize is the third path. No copying. No templates. No curly braces. Just a base configuration and environment-specific patches layered on top — all in pure, readable YAML. And it’s built directly into kubectl.
What Kustomize Actually Does
Kustomize works on a simple principle: start with a base — your common configuration — and apply overlays on top of it for each environment. The overlay only describes what’s different about that environment. Everything else comes from the base unchanged.
base/
├── kustomization.yaml ← declares what's in the base
├── deployment.yaml
├── service.yaml
└── configmap.yaml
overlays/
├── development/
│ ├── kustomization.yaml ← declares patches on top of base
│ └── patch-replicas.yaml
├── staging/
│ ├── kustomization.yaml
│ ├── patch-replicas.yaml
│ └── patch-resources.yaml
└── production/
├── kustomization.yaml
├── patch-replicas.yaml
├── patch-resources.yaml
└── patch-ingress.yaml
The output of Kustomize is plain Kubernetes YAML — no custom resource types, no new concepts. kubectl apply -k feeds it directly to the cluster.
# Build and preview what Kustomize will apply
kubectl kustomize overlays/production
# Apply it directly
kubectl apply -k overlays/production
# Kustomize is also a standalone binary if you need the latest features
kustomize build overlays/production | kubectl apply -f -
A Complete Example: Base + Three Overlays
Let’s build a real example from scratch — a web application deployed across development, staging, and production with meaningful differences between environments.
The Base
The base contains the configuration that’s common to all environments. No environment-specific values here.
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 1
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: my-registry/web-app:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: web-app-config
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
# base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: web-app
spec:
selector:
app: web-app
ports:
- port: 80
targetPort: 8080
# base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: web-app-config
data:
LOG_LEVEL: "info"
CACHE_TTL: "3600"
MAX_CONNECTIONS: "10"
Preview the base output
kubectl kustomize base/
# Outputs all three resources as-is — no patches applied yet
Overlay: Development
Development needs debug logging, a single replica, and relaxed resources.
# overlays/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: dev- # all resource names get "dev-" prefix
namespace: development # all resources go in the development namespace
bases:
- ../../base # relative path to the base
patches:
- path: patch-deployment.yaml
configMapGenerator:
- name: web-app-config
behavior: merge # merge with base ConfigMap, don't replace
literals:
- LOG_LEVEL=debug # override just this value
- MAX_CONNECTIONS=5
# overlays/development/patch-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app # must match the base resource name (before prefix)
spec:
replicas: 1 # explicit single replica
template:
spec:
containers:
- name: web-app
resources:
requests:
cpu: 50m # lighter resources in dev
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
# Preview development output
kubectl kustomize overlays/development/
# Apply to development namespace
kubectl apply -k overlays/development/
Overlay: Staging
Staging needs 2 replicas, moderate resources, and a specific image tag.
# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: staging-
namespace: staging
bases:
- ../../base
images:
- name: my-registry/web-app # target image name in the base
newTag: v1.4.2 # pin to a specific tag for staging
patches:
- path: patch-deployment.yaml
configMapGenerator:
- name: web-app-config
behavior: merge
literals:
- LOG_LEVEL=info
- CACHE_TTL=1800
# overlays/staging/patch-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 2
template:
spec:
containers:
- name: web-app
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
Overlay: Production
Production needs 5 replicas, full resources, a specific image tag, and an Ingress with TLS.
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
bases:
- ../../base
images:
- name: my-registry/web-app
newTag: v1.4.2 # same tag as staging — proven stable
patches:
- path: patch-deployment.yaml
resources:
- ingress.yaml # add production-only Ingress resource
configMapGenerator:
- name: web-app-config
behavior: merge
literals:
- LOG_LEVEL=warning # less verbose in production
- CACHE_TTL=7200 # longer cache in production
- MAX_CONNECTIONS=50 # more connections in production
commonLabels:
environment: production # add this label to ALL resources
# overlays/production/patch-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 5
template:
spec:
containers:
- name: web-app
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
# overlays/production/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: web-app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80
# Preview what production will look like — no cluster needed
kubectl kustomize overlays/production/
# Diff against what's currently running
kubectl diff -k overlays/production/
# Apply to production
kubectl apply -k overlays/production/
Core Kustomize Features
images: Changing Image Tags
The images transformer is one of Kustomize's most useful features — it finds every container in every resource that uses a given image name and updates the tag, without touching anything else.
# kustomization.yaml
images:
- name: my-registry/web-app # find this image name anywhere in resources
newTag: v2.0.1 # replace its tag
- name: nginx
newTag: 1.26 # pin nginx to a specific version
- name: my-registry/web-app
newName: my-registry/web-app-eu # also rename the image (useful for regional registries)
newTag: v2.0.1
This is particularly powerful in CI/CD. Your pipeline builds an image, tags it with the commit SHA, then patches the kustomization:
# In your CI pipeline
IMAGE_TAG=$(git rev-parse --short HEAD)
# Update the image tag in your kustomization
kustomize edit set image my-registry/web-app=my-registry/web-app:$IMAGE_TAG
# Commit the change (GitOps pattern)
git add overlays/production/kustomization.yaml
git commit -m "Deploy $IMAGE_TAG to production"
git push
configMapGenerator and secretGenerator
Kustomize can generate ConfigMaps and Secrets from files or literals — and automatically appends a hash suffix to the name to force pod restarts when config changes.
# kustomization.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- APP_PORT=8080
files:
- configs/app.properties # file contents become a key in the ConfigMap
- nginx.conf=configs/nginx.conf # explicit key name=filepath
secretGenerator:
- name: db-credentials
literals:
- username=admin
- password=s3cr3t
type: Opaque
- name: tls-secret
files:
- tls.crt
- tls.key
type: kubernetes.io/tls
kubectl kustomize .
# Output includes ConfigMap name like: app-config-8k7m9n2p
# The hash changes when content changes → pods automatically restart
💡 The hash suffix is the feature, not a bug. ConfigMap changes don’t automatically restart pods — they only update the mounted file over time. By appending a hash, Kustomize forces the Deployment to update its ConfigMap reference, which triggers a rolling restart. Your configuration changes are guaranteed to reach running pods.
To disable the hash suffix (for ConfigMaps referenced by name externally):
configMapGenerator:
- name: app-config
literals:
- KEY=value
options:
disableNameSuffixHash: true
patches: Strategic Merge vs JSON 6902
Kustomize supports two patch formats. Know both — they’re used in different situations.
Strategic Merge Patch — looks like a partial copy of the resource. Kubernetes intelligently merges it with the base. The most readable option.
# patch-deployment.yaml — strategic merge
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 5 # only this changes
template:
spec:
containers:
- name: web-app # must identify the container by name
resources:
limits:
memory: 2Gi # only this changes
JSON 6902 Patch — precise operations (add, remove, replace, move, copy) using JSON Patch notation. Use when strategic merge can’t express what you need — removing a field, adding to a list in a specific position, or targeting a specific list element by value.
# kustomization.yaml
patches:
- target:
group: apps
version: v1
kind: Deployment
name: web-app
patch: |-
- op: replace
path: /spec/replicas
value: 5
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: NEW_ENV_VAR
value: "production"
- op: remove
path: /spec/template/spec/containers/0/livenessProbe
# Or as a separate file
patches:
- path: patch.yaml
target:
kind: Deployment
name: web-app
namePrefix and nameSuffix: Environment Namespacing
Add a prefix or suffix to all resource names — useful when deploying multiple environments to the same namespace, or to clearly identify resources by environment.
# kustomization.yaml
namePrefix: prod-
nameSuffix: -v2
# web-app Deployment becomes: prod-web-app-v2
# web-app Service becomes: prod-web-app-v2
# References in Selectors and ServiceMonitors are updated automatically
commonLabels and commonAnnotations: Tagging Everything
Apply labels and annotations to every resource in one place:
# kustomization.yaml
commonLabels:
app.kubernetes.io/part-of: my-platform
environment: production
managed-by: kustomize
commonAnnotations:
owner: platform-team@company.com
documentation: https://wiki.internal/services/web-app
components: Reusable Feature Modules
Components are reusable Kustomize modules — a way to package optional features that can be included in any overlay. Perfect for cross-cutting concerns like monitoring, security policies, or debug tooling.
components/
├── monitoring/
│ ├── kustomization.yaml # kind: Component
│ └── servicemonitor.yaml
├── network-policy/
│ ├── kustomization.yaml
│ └── networkpolicy.yaml
└── debug/
├── kustomization.yaml
└── debug-sidecar-patch.yaml
# components/monitoring/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- servicemonitor.yaml
# overlays/production/kustomization.yaml
bases:
- ../../base
components:
- ../../components/monitoring # opt-in to monitoring in production
- ../../components/network-policy # opt-in to network policy in production
# debug component deliberately excluded from production
# overlays/development/kustomization.yaml
bases:
- ../../base
components:
- ../../components/debug # include debug tooling only in dev
Kustomize in a GitOps Workflow
Kustomize pairs naturally with GitOps tools like ArgoCD and Flux. Your Git repository becomes the source of truth. Every environment is a directory. Changes to any overlay are reconciled automatically.
repo/
├── base/
├── overlays/
│ ├── development/
│ ├── staging/
│ └── production/
└── components/
# ArgoCD Application pointing to a Kustomize overlay
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-app-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-configs
targetRevision: main
path: overlays/production # Kustomize overlay path
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
ArgoCD detects that the path contains a kustomization.yaml and automatically runs Kustomize. Every Git commit to overlays/production/ triggers a reconciliation. Rollback is git revert.
Kustomize vs Helm: When to Use Which
This is the question everyone asks. Honest answer: they solve different problems.
……………………….Kustomize → Helm
Config format → Pure YAML patches → YAML templates with Go templating Learning curve → Low — it’s still just YAML → Medium — requires learning template syntax
Readability → High — patches are explicit → Lower — templates obscure the final output
Built into kubectl → Yes — kubectl apply -k → No — separate binary required
Packaging for distribution → Not designed for it → Core feature — charts on Artifact Hub
Complex conditional logic → Painful → Natural
Hooks (pre/post install) → Not supported → Built in
Rollback → Via Git → helm rollback
Third-party apps → Patch existing charts → Install charts directly
Best for → Your own apps, GitOps → Third-party software, complex apps
Use Kustomize when: managing your own application configs across environments, following a GitOps workflow, or when you want to patch Helm-rendered output.
Use Helm when: installing third-party software (Prometheus, cert-manager, NGINX Ingress), packaging software for distribution, or when you need complex conditional logic and hooks.
Use both: Install third-party apps with Helm, manage the output with Kustomize patches. This is increasingly common in mature teams.
# kustomization.yaml — patch a Helm-rendered output
resources:
- helm-rendered-output.yaml # pre-rendered from: helm template ...
patches:
- path: patch-helm-values.yaml # override specific values Helm didn't expose
Common Patterns and Gotchas
Gotcha 1: Resource names must match exactly
Patches target resources by apiVersion, kind, and metadata.name. If the name in your patch doesn't exactly match the base resource name (before any prefix/suffix is applied), the patch silently doesn't apply.
# Always preview before applying
kubectl kustomize overlays/production/ | grep "kind: Deployment" -A 5
# Verify the patch actually took effect
Gotcha 2: List merging in strategic merge patches
Strategic merge patches handle lists differently depending on the resource type. For containers (identified by name) it merges by name. For env vars it also merges by name. But for some lists, it replaces the entire list.
# This ADDS to the env list (containers use name as merge key)
spec:
template:
spec:
containers:
- name: web-app # identifies which container
env:
- name: NEW_VAR # adds this env var
value: "new-value"
When in doubt, use JSON 6902 patch for precise list operations.
Gotcha 3: configMapGenerator creates new resources
If you use configMapGenerator in an overlay with behavior: merge, Kustomize merges and creates a new ConfigMap. But if you also have a ConfigMap defined in the base resources, you'll end up with two ConfigMaps. Use behavior: merge and remove the ConfigMap from resources in the base, or use behavior: replace.
Gotcha 4: Patches don’t create new fields on resources
Strategic merge patches can only modify fields that exist in the base resource or add to lists. To add an entirely new field (like a topologySpreadConstraints that doesn't exist in the base), use JSON 6902 add operation or add it to the base.
Hands-On Lab
# 1. Create the directory structure
mkdir -p kustomize-lab/base
mkdir -p kustomize-lab/overlays/{development,staging,production}
# 2. Create base manifests
cd kustomize-lab
cat > base/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 1
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
EOF
cat > base/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: web-app
spec:
selector:
app: web-app
ports:
- port: 80
targetPort: 80
EOF
cat > base/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
EOF
# 3. Preview base
kubectl kustomize base/
# 4. Create development overlay
cat > overlays/development/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
bases:
- ../../base
patches:
- path: patch.yaml
EOF
cat > overlays/development/patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 1
template:
spec:
containers:
- name: web-app
image: nginx:1.25
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
EOF
# 5. Create production overlay with different replicas and image tag
cat > overlays/production/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
bases:
- ../../base
images:
- name: nginx
newTag: "1.26"
patches:
- path: patch.yaml
commonLabels:
environment: production
EOF
cat > overlays/production/patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
template:
spec:
containers:
- name: web-app
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
EOF
# 6. Preview both overlays and diff them
echo "=== DEVELOPMENT ===" && kubectl kustomize overlays/development/
echo "=== PRODUCTION ===" && kubectl kustomize overlays/production/
# See the differences clearly
diff <(kubectl kustomize overlays/development/) \
<(kubectl kustomize overlays/production/)
# 7. Apply development overlay
kubectl apply -k overlays/development/
# 8. Check what was deployed
kubectl get deployment web-app -o yaml | grep -E 'replicas:|image:|cpu:|memory:'
# 9. Apply production overlay (different replicas and image)
kubectl apply -k overlays/production/
kubectl get deployment web-app -o yaml | grep -E 'replicas:|image:|cpu:|memory:'
# 10. Dry-run to preview changes before applying
kubectl apply -k overlays/production/ --dry-run=client
# 11. Clean up
kubectl delete -k overlays/production/
cd .. && rm -rf kustomize-lab
The diff command in step 6 is the most instructive moment of this lab. Seeing development and production side by side — same structure, different values, no duplication — makes the entire Kustomize mental model click instantly.
Quick Reference
# Build (preview output without applying)
kubectl kustomize <dir>
kustomize build <dir>
# Apply
kubectl apply -k <dir>
kustomize build <dir> | kubectl apply -f -
# Dry run
kubectl apply -k <dir> --dry-run=client
# Diff against running cluster
kubectl diff -k <dir>
# Delete resources
kubectl delete -k <dir>
# Edit image tag (updates kustomization.yaml in place)
cd overlays/production
kustomize edit set image my-app=my-app:v2.0.1
# Add a resource to kustomization.yaml
kustomize edit add resource ingress.yaml
# Add a patch to kustomization.yaml
kustomize edit add patch patch-replicas.yaml
What’s Next
We return to the main series for the final episode — Episode 10: Production Hardening & Helm Charts. We’ll package our application as a Helm chart, apply the complete production hardening checklist across everything we’ve covered, and close out the series with the operational practices that make the difference between a cluster you trust and one you worry about.
Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 Pods, Deployments & ReplicaSets
EP 04 Services & Networking
EP 05 ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
Bonus Setting Up a Cluster with kubeadm on EC2
Bonus Network Policy Deep Dive
EP 07 Ingress Controllers & Load Balancing
EP 08 RBAC & Security Fundamentals Bonus Namespaces Deep Dive
EP 09 Monitoring with Prometheus & Grafana
Bonus ← You are here Kustomize: Config Management Without Templates EP 10 Production Hardening & Helm Charts
Before the Next Article
Three questions to test your understanding:
- Your base deployment has replicas: 1. Your production overlay patches it to replicas: 5. What does kubectl kustomize overlays/production/ output for the replica count — 1, 5, or an error?
- You add a configMapGenerator to your overlay. The generated ConfigMap name has a hash suffix. Why does Kustomize add this suffix, and what problem does it solve?
- A teammate says “we should use Helm instead of Kustomize.” What questions would you ask to figure out which one is actually right for your situation?
You’ve just gained the ability to manage configuration across environments without copying YAML, without template syntax, and without the hidden complexity that makes large Helm charts painful to maintain. One base. Layered overlays. Pure readable YAML all the way down. See you in the final episode.