Kustomize: Bases & Overlays
Helm solves "same app, different settings" with templates. Kustomize takes the opposite approach: no templates at all. You keep ordinary, valid YAML as a base, and each environment has a small overlay that says how to change it: a different image tag, more replicas, a name prefix, a patch. Kustomize is built into kubectl (you saw Kustomize Version: v5.8.1 in kubectl version), so there is nothing extra to install.
In this lesson you will turn the menu app into a base with dev and prod overlays, generate ConfigMaps that trigger rollouts when they change, and see when to pick Kustomize or Helm.
The layout
menu/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
├── kustomization.yaml
└── resources-patch.yamlEvery folder that Kustomize builds has a kustomization.yaml. The base lists its manifests; deployment.yaml and service.yaml are the plain menu Deployment and Service from earlier lessons, untouched:
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/part-of: tea-stallslabels adds a label to every object. By default it does not touch selectors, which is what you want, because Deployment selectors cannot change after creation.
An overlay for production
The production overlay points at the base and describes only the differences:
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop-prod
namePrefix: prod-
resources:
- ../../base
images:
- name: nginx
newTag: 1.30-alpine
replicas:
- name: menu
count: 4
configMapGenerator:
- name: menu-settings
literals:
- SHOP_CITY=Pune
- OPEN_HOURS=7am-11pm
patches:
- path: resources-patch.yaml
target:
kind: Deployment
name: menunamespaceputs every object intoshop-prod.namePrefixrenames everything (menubecomesprod-menu) and updates references to those names, such as a Deployment's reference to its ConfigMap.imageschanges the tag of every container that uses the imagenginx, without touching the YAML in the base.replicassets the replica count of the named Deployment.configMapGeneratorcreates a ConfigMap from literals, files or.envfiles.patchesapplies a patch file to matching objects.
The patch is a fragment of a Deployment containing only the fields to add or change. Kustomize merges it into the base using the same rules as kubectl apply (a strategic merge patch), which matches list items such as containers by their name:
# overlays/prod/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: menu
spec:
template:
spec:
containers:
- name: web
envFrom:
- configMapRef:
name: menu-settings
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 128MiNotice that the patch uses the base names, menu and menu-settings; Kustomize applies the prefix and the hash suffix afterwards and fixes the references.
Build, preview and apply
kubectl kustomize overlays/prod # print the final YAML
kubectl diff -k overlays/prod # compare with the cluster
kubectl apply -k overlays/prod # apply it
kubectl get deployment -n shop-prodNAME READY UP-TO-DATE AVAILABLE AGE prod-menu 4/4 4 4 40s
(The output of the first three commands is long and left out.) -k means "build this folder with Kustomize first", and kubectl kustomize shows exactly what would be sent, so check it before every change. The namespace must exist; add a Namespace manifest to the overlay or create it once by hand.
Why the generated ConfigMap has a hash
In the built output the ConfigMap is not called prod-menu-settings but prod-menu-settings- followed by a short hash of its contents, and the Deployment refers to that full name. Change OPEN_HOURS and the hash changes, so the ConfigMap gets a new name, so the Deployment's Pod template changes, so a normal rolling update starts and the Pods pick up the new settings. This solves the problem from ConfigMaps & Secrets that environment variables never update in running Pods, and a rollback brings back the old settings too. secretGenerator works the same way for Secrets. Old generated ConfigMaps are left behind; tools such as Argo CD can prune them.
A dev overlay
Development is the same app with one replica and the latest test image:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop-dev
namePrefix: dev-
resources:
- ../../base
images:
- name: nginx
newTag: 1.31-alpine
replicas:
- name: menu
count: 1Both overlays share every line of the base. Fix a bug in base/deployment.yaml and both environments get it; change only overlays/prod and only production changes. This "one base, small overlays" shape reads very well in Git reviews.
Kustomize or Helm?
| Kustomize | Helm | |
|---|---|---|
| Approach | Patches on plain YAML | Templates plus values |
| Install | Built into kubectl | Separate CLI |
| Sharing an app with the world | Awkward | Excellent: versioned charts in repositories |
| Release history and rollback | No (use Git) | Yes (helm rollback) |
| Learning curve | Small | Medium (template language) |
A common pattern is to use Helm for third-party software (databases, ingress or gateway controllers, monitoring) and Kustomize for your own apps. They also combine: Kustomize can render Helm charts, and GitOps tools such as Argo CD and Flux understand both natively, as the next lesson shows.
Common mistakes
- Patching with the prefixed name. Patches and
replicasuse the base names (menu), notprod-menu. - Applying the base directly in production. Apply the overlay; the base is a shared starting point.
- Turning on
includeSelectorsfor an existing app. Selectors change and the API server rejects the Deployment. - Disabling the ConfigMap hash suffix and then wondering why Pods keep old settings.
Key points
- Kustomize customises plain YAML with no templates; it is built into
kubectl(kubectl kustomize,apply -k,diff -k). - A base holds shared manifests; overlays reference the base and add namespaces, prefixes, image tags, replica counts and patches.
- Strategic merge patches are partial manifests that match list items such as containers by name.
configMapGeneratorandsecretGeneratoradd a content hash to names, so changes trigger rolling updates.- Use Kustomize for your own apps and Helm for packaged third-party software; GitOps tools support both.
Exercise
Add a staging overlay for the menu app: namespace shop-staging, name prefix staging-, 2 replicas, image tag 1.31-alpine, and a generated ConfigMap menu-settings with SHOP_CITY=Kochi. Write its kustomization.yaml and the two commands to preview and apply it.
Show solution
# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop-staging
namePrefix: staging-
resources:
- ../../base
images:
- name: nginx
newTag: 1.31-alpine
replicas:
- name: menu
count: 2
configMapGenerator:
- name: menu-settings
literals:
- SHOP_CITY=Kochikubectl kustomize overlays/staging
kubectl apply -k overlays/stagingThe generated ConfigMap is only used once a container refers to it, so in a real setup you would also copy the envFrom patch from the prod overlay (or move it into the base).
Quick Quiz
1. What does an overlay contain?
Overlays stay small because the base holds the shared YAML.
2. Why does a ConfigMap from configMapGenerator get a hash in its name?
New contents mean a new name, and the reference update starts a rolling update.
3. Which command prints the final YAML of an overlay without applying it?
kubectl diff -k compares it with the cluster as a next step.
4. With namePrefix: prod-, which name do you use in a patch's target?
Kustomize applies patches before adding the prefix.
Learn Kubernetes with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
