What Kustomize is: the overlay model
Template-free customization.
You keep one master recipe card for bread: flour, water, salt, and the method. When you bake for a party, you don't rewrite the card. You clip a sticky note to it that reads 'double it, make two loaves.' The card stays clean. The note holds only what changed. Anyone can read the card on its own, and anyone can read the note on its own. Kustomize customizes Kubernetes configuration the same way.
Kubernetes (the system that runs your containers across a fleet of machines) is driven by manifests. A manifest is a YAML file (YAML is a plain-text format for writing configuration) that describes one thing you want in the cluster. That thing might be a Deployment (an object that keeps a set of identical Pods running, where a Pod is the smallest unit Kubernetes schedules: one or more containers that live and die together), or a Service (a stable network name that sits in front of those Pods so other things can reach them). Kustomize is a tool that takes a folder of those plain manifests and produces a customized version of them, without ever turning them into templates.
Helm, the most common Kubernetes packaging tool, customizes YAML by punching holes in it. You write placeholders like {{ .Values.replicas }} and hand it a values.yaml file to fill them in. That turns a manifest into a small program that prints YAML. Kustomize refuses to do that. Your files stay real, valid manifests that kubectl (the command-line tool that talks to your cluster) could apply exactly as they sit on disk. The customization lives in a separate file, kustomization.yaml, that says what to assemble and what to change.
The base is the recipe everyone shares
A base is the master recipe card: the manifests every environment agrees on, with nothing environment-specific baked in. Here is a small app laid out the usual way, a base folder plus one folder per environment.
tree app
app├── base│ ├── deployment.yaml│ ├── kustomization.yaml│ └── service.yaml└── overlays├── dev│ └── kustomization.yaml└── prod├── kustomization.yaml└── replicas.yaml4 directories, 6 files
The base has its own kustomization.yaml. It does one job here: name the manifests that belong to this set.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- deployment.yaml- service.yaml
The two files it lists are ordinary Kubernetes YAML. Here is the Deployment. The Service next to it is a plain Service, which defaults to a ClusterIP (an internal-only address inside the cluster).
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 1selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: webimage: ghcr.io/acme/web:1.4.0ports:- containerPort: 8080
Notice replicas: 1 and the image tag 1.4.0. Nothing in this file knows the word 'prod.' You could run kubectl apply -f on it today and it would work. That is the whole point of a base: it is complete and valid on its own, so every tool and every reader can make sense of it without any extra step.
An overlay is the sticky note on top
An overlay is the sticky note. It is its own kustomization.yaml that points back at the base and lists only the differences for one environment. Here is the prod overlay.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: prodnamePrefix: prod-resources:- ../../baseimages:- name: ghcr.io/acme/webnewTag: 2.1.0patches:- path: replicas.yaml
Read it top to bottom. resources points at the base folder, so everything in the base comes along. namespace and namePrefix are transformers (rules that rewrite one field across every object the build produces): they drop every object into the prod namespace (a named partition inside the cluster) and rename web to prod-web. images swaps the tag on that one image to 2.1.0 without editing the file that declares it. patches points at a small file for a change a transformer can't express cleanly.
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 4
That patch file is a strategic merge patch: a partial manifest that names the object by kind and name, then lists only the fields to change. It says 'the Deployment called web, set replicas to 4,' and stays silent about everything else. Notice it targets the original name web, not prod-web, because patches are applied before namePrefix renames anything. The base still reads replicas: 1; the difference lives entirely in the overlay, where anyone reviewing prod can see it in one place.
Build it, then look before you ship
Nothing has touched the cluster yet. To see the assembled result, ask kubectl to render the overlay. kubectl kustomize prints the final YAML to your terminal and stops. (Kustomize also ships as a standalone binary, where the same command is kustomize build; the built-in kubectl version needs nothing installed.)
kubectl kustomize app/overlays/prod
apiVersion: v1kind: Servicemetadata:name: prod-webnamespace: prodspec:ports:- port: 80targetPort: 8080selector:app: web---apiVersion: apps/v1kind: Deploymentmetadata:name: prod-webnamespace: prodspec:replicas: 4selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: webimage: ghcr.io/acme/web:2.1.0ports:- containerPort: 8080
Read what came out. The Service and the Deployment are both there, both renamed to prod-web, both in the prod namespace. The image is 2.1.0. The replica count is 4. This is exactly what would land in the cluster, character for character. There is no hidden templating step left to run, which means the rendered text is not a preview of the real thing, it is the real thing.
Two more commands close the loop. kubectl diff -k renders the overlay and shows how it differs from what is running right now, before you change anything. kubectl apply -k builds and applies in one step.
kubectl diff -k app/overlays/prod
diff -u -N /tmp/LIVE-1837/apps.v1.Deployment.prod.prod-web /tmp/MERGED-2044/apps.v1.Deployment.prod.prod-web--- /tmp/LIVE-1837/apps.v1.Deployment.prod.prod-web 2026-07-20 10:15:33.482910245 +0000+++ /tmp/MERGED-2044/apps.v1.Deployment.prod.prod-web 2026-07-20 10:15:33.500114872 +0000@@ -16,7 +16,7 @@name: prod-webnamespace: prodspec:- replicas: 2+ replicas: 4selector:matchLabels:app: web@@ -30,5 +30,5 @@containers:- name: web- image: ghcr.io/acme/web:2.0.0+ image: ghcr.io/acme/web:2.1.0ports:- containerPort: 8080
kubectl apply -k app/overlays/prod
service/prod-web unchangeddeployment.apps/prod-web configured
Template-free, and why that helps you
Because a manifest never stops being a manifest, every tool that understands Kubernetes YAML keeps working on your files. Your editor autocompletes fields. A schema checker (a tool that flags an invalid field name or a wrong type) reads them directly. A security scanner such as kube-linter or Checkov can inspect both the base and the rendered output. With Helm's placeholders, those tools see a broken half-YAML file until Helm fills the holes at build time. There is no whitespace-sensitive template language to fight, and no second mental model to hold: what you read is what deploys.
The trade-off is reach. Helm's templates and its packaging (a chart you can version and hand to strangers) do more for distributing an app you don't control, where the person installing it needs dozens of knobs. Kustomize aims lower and hits it cleanly: take manifests you already own and bend them per environment. A common split is Kustomize for your own overlays and Helm for third-party apps you install, and later lessons cover running the two together.
Review the assembled output, not the overlay
Here is the operational catch. A prod overlay can be four lines and still change something that matters, because most of what deploys comes from the base and the patches, not from the short file you are reading. A patch can strip a securityContext (the block that decides whether a container runs as root or can gain extra privileges). A base or a transformer can widen RBAC (role-based access control, the rules for who can do what in the cluster). An image line can point somewhere new. None of that is visible in a tidy diff of the overlay file. The assembled output is the only honest source of truth, so review that.
The habit is a one-liner. Render the exact overlay you plan to apply, then grep the result for the fields you care about before it goes anywhere.
kubectl kustomize app/overlays/prod | grep -nE 'privileged|allowPrivilegeEscalation|hostPath|runAsUser|NET_ADMIN'
Nothing matched, so nothing in this build asks for privilege it shouldn't have. If a teammate's patch, or a base you pulled in, had added privileged: true or a hostPath mount (a container mounting a path from the node's own filesystem, a classic route off the container and onto the host), this is where it would surface, in the rendered object, while the overlay file still looked innocent. Reading the sticky note is not enough. You verify the finished loaf.
Try this
Run kubectl kustomize app/overlays/prod on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: the overlay is not the whole story. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.