Build & apply
kustomize build / kubectl -k.
kustomize build is the compiler for your Kubernetes manifests. A manifest is a YAML file that describes what you want running in the cluster (YAML is a plain-text format for configuration). The files you edit are source code: the kustomization.yaml, the bases it pulls in, the patches that override a few fields, the generators that build a ConfigMap or Secret (its cousin for sensitive values) for you. On their own they never touch a cluster, the same way a source file never runs until a compiler turns it into a program. build reads that source, resolves every reference, runs every transformer in the right order, and prints one flat stream of plain Kubernetes YAML. A transformer is any step that rewrites the manifests along the way, like stamping on a name prefix or a label. That printed stream is the finished artifact. Nothing is live until you hand it to the API server, the cluster's control-plane front door that every change has to pass through.
Keeping the two steps apart, render then apply, is the one habit that makes Kustomize safe to run when you are tired or in a hurry. Render produces something you can read. Apply is the part you cannot take back. Separating them buys you a moment to look before anything changes in a shared cluster.
Render first, apply second
kustomize build <dir> prints the fully assembled manifests to stdout and changes nothing. stdout is standard output, the normal text stream a command writes to your terminal. This build is your dry run and your review surface at once. Read the output closely, because the flat result often looks nothing like the small overlay you edited. An overlay is the thin directory of edits you work in day to day. A base you inherit, a component you opted into, a name prefix, a content hash a generator stapled onto a ConfigMap: all of them show up only after assembly, not before. A base is the shared set of manifests your overlay builds on; a component is an optional, reusable slice of config; a ConfigMap is a Kubernetes object that stores configuration as key-value pairs.
$ kustomize build overlays/prod # render only, changes nothing
apiVersion: v1kind: Servicemetadata:name: prod-payments-apinamespace: paymentsspec:ports:- port: 80targetPort: 8080selector:app: payments-api---apiVersion: apps/v1kind: Deploymentmetadata:name: prod-payments-apinamespace: paymentsspec:replicas: 4selector:matchLabels:app: payments-apitemplate:metadata:labels:app: payments-apispec:containers:- image: registry.internal/payments-api:2.1.0name: appports:- containerPort: 8080
The overlay you edited was a handful of lines: a namespace, a name prefix, a replica count, an image tag. A namespace is a named partition that keeps one group of cluster objects apart from another. Yet the build above is the whole Service and Deployment with every one of those edits already baked in. A Service is the stable network address that sits in front of your pods; a Deployment is the controller that keeps a set of identical pods running, a pod being the smallest thing Kubernetes schedules, one or more containers that live and die together. When the rendered YAML matches what you expect, apply it. There are two ways to do that.
$ kustomize build overlays/prod | kubectl apply -f - # explicit: render, then apply$ kubectl apply -k overlays/prod # build + apply folded into one
service/prod-payments-api createddeployment.apps/prod-payments-api created
kubectl apply -k folds build and apply into a single command because Kustomize ships inside kubectl (the command-line tool you use to talk to a Kubernetes cluster). It is fine for a quick change. The explicit pipe (the | that feeds build's output straight into apply) does the same work but keeps the render in your hands: you can save it, scan it, or diff it before a single byte reaches the API server. For anything that matters, prefer the pipe, because the version you can read is the version you can review.
The artifact is your review surface
Reading the whole rendered output is the review step that catches the changes a small diff hides. A patch is a partial file. A remote base is code you did not write. Either one can set a field you would never approve if you saw it spelled out: a securityContext (the part of a pod spec that sets its privileges) flipped to privileged: true, a hostPath volume (a mount of a directory from the node's own filesystem straight into the pod) that reaches the host, an extra ClusterRole (a cluster-wide grant of permissions) that widens RBAC (role-based access control, the rules that decide who can do what in the cluster), or an image quietly swapped for one nobody has scanned. None of that is visible in the two-line overlay. All of it is visible in the build output.
So the defender's move is boring and it works: render, then read the assembled artifact, or at least grep it (search the text for a pattern) for the fields that decide privilege before you apply. In a pull request (a proposed change your teammates review before it merges), review the rendered YAML, not the overlay change alone, because the assembled result is what reaches the cluster.
$ kustomize build overlays/prod | grep -nE 'privileged|hostPath|allowPrivilegeEscalation'
52: privileged: true
That one line was contributed by a base three directories away. The overlay diff never mentioned it. The rendered artifact did.
Two kustomize engines, different versions
Most machines carry two Kustomize engines that are not the same version. One is the standalone kustomize binary you install yourself. The other is a copy compiled into kubectl, which is what runs when you type kubectl -k or kubectl kustomize. The embedded copy usually trails the standalone release, sometimes by a year. It is like keeping two dictionaries of different printings: a word can exist in the new one and be missing from the old. Here, the word might be a newer field, an updated API version, or the --enable-helm flag that lets a kustomization inflate a Helm chart (a packaged, templated bundle of Kubernetes manifests) during the build.
$ kustomize version # the standalone binary$ kubectl version --client # look at the embedded Kustomize Version line
v5.4.3Client Version: v1.29.6Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Two printings, one box. A kustomization that builds cleanly under the standalone kustomize can fail or render differently under kubectl -k on the same laptop, and differently again on a teammate's older kubectl. For anything past a quick apply, install the standalone binary, pin its exact version in CI (continuous integration, the automated pipeline that runs on every change), render with it, and pipe plain YAML to kubectl apply -f -. That decouples what you render from whichever kubectl happens to be installed, so the same commit renders to the same bytes on every laptop and every runner (a CI machine that executes the pipeline). For a security control that is the whole point: if the render is not deterministic, you cannot say what you actually deployed.
Preview against the live cluster
kubectl diff -k <dir> is the closest thing Kustomize gives you to a plan. It renders the overlay, then shows exactly which live objects would change and how, as an ordinary unified diff (the same before-and-after format git shows you). Nothing is applied. Read it like a code review of the running cluster.
$ kubectl diff -k overlays/prod
diff -u -N /tmp/LIVE-2891543/apps.v1.Deployment.payments.prod-payments-api /tmp/MERGED-4417820/apps.v1.Deployment.payments.prod-payments-api--- /tmp/LIVE-2891543/apps.v1.Deployment.payments.prod-payments-api 2026-07-17 09:14:02.117 +0000+++ /tmp/MERGED-4417820/apps.v1.Deployment.payments.prod-payments-api 2026-07-17 09:14:02.121 +0000@@ -8,6 +8,6 @@name: prod-payments-apinamespace: paymentsspec:- replicas: 2+ replicas: 4selector:matchLabels:@@ -34,6 +34,6 @@spec:containers:- name: app- image: registry.internal/payments-api:2.0.4+ image: registry.internal/payments-api:2.1.0ports:- containerPort: 8080
diff exits 0 when nothing would change and 1 when there is a difference (anything above 1 means the command itself failed). That makes it a clean gate. A pipeline step can fail loudly when a change you thought was a no-op actually moves something in production. In CI, go one step further and render every overlay on every commit, so a broken reference or a mistyped patch fails the pipeline instead of the cluster. Archive the rendered YAML as the artifact for the change: the exact bytes that will be applied.
$ for o in overlays/*/; do kustomize build "$o" >/dev/null || exit 1; done$ echo $?
0
Keep the build inside its root
A kustomization can read files off disk. That is fine for your own tree, but a remote base you inherit is code from someone else, and you do not want it reading files from outside its own directory. A fence around a kitchen makes the rule concrete: the cooks can use anything on the counter, but they cannot walk out to the street and grab whatever they like. --load-restrictor is that fence. It defaults to LoadRestrictionsRootOnly, which forbids reading any file above the kustomization root. Watch what happens when a base tries to climb out.
apiVersion: kustomize.config.k8s.io/v1beta1kind: KustomizationconfigMapGenerator:- name: pulledenvs:- ../../secret.env # a file at the repo root, outside this overlay
$ kustomize build overlays/evil
error: loading KV pairs: env source files: [../../secret.env]: security; file '/home/deploy/payments/secret.env' is not in or below '/home/deploy/payments/overlays/evil'
The default fence stopped it. Now loosen it, and the same build happily slurps that outside file into a ConfigMap you would then apply. This is exactly how a hostile base exfiltrates a secret or a host file into an object on your cluster: it asks the build to read something outside its tree, and a relaxed restrictor says yes.
$ kustomize build --load-restrictor LoadRestrictionsNone overlays/evil | grep -A1 '^data:'
data:API_TOKEN: super-secret
Keep the default. Treat any kustomization that needs LoadRestrictionsNone as a thing to read line by line before you trust it. One more flag earns its place in pipelines: -o writes the rendered manifests to a path instead of stdout. Point it at a directory (which must already exist) and Kustomize writes one file per object, named by group, version, kind, and name.
$ mkdir -p rendered$ kustomize build overlays/prod -o rendered/$ ls rendered/
apps_v1_deployment_prod-payments-api.yamlv1_service_prod-payments-api.yaml
After you apply, check that the render actually landed. kubectl get -k overlays/prod builds the overlay and lists exactly the objects it declares, so anything running that this get does not show is an orphan from an earlier, larger build. kubectl rollout status deploy/prod-payments-api -n payments confirms the new image rolled instead of silently sticking on the old pods. The build told you what should be true; those two commands tell you whether it is.
Try this
Run kustomize build overlays/prod # render only, changes nothing 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: apply -k never deletes what you removed. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.