kustomization.yaml & resources
The file that ties it together.
A cargo ship's manifest holds no cargo. It's a single sheet that says which containers are aboard, what order they were loaded in, and where each one came from. Lose the manifest and you have a deck full of anonymous steel boxes. The file named kustomization.yaml is that manifest for a folder of Kubernetes config. It stores no running objects of its own. It names the files that make up your app and tells kustomize (the tool that assembles Kubernetes configuration from plain files) how to stitch them together.
Kubernetes never reads this file. kustomize does. It reads the manifest, gathers every file the manifest points to, applies any edits you asked for, and prints one long stream of finished YAML (the plain-text format Kubernetes uses for configuration) to your terminal. kubectl (the command-line tool that sends configuration to a Kubernetes cluster) takes that stream and applies it. So kustomization.yaml is the one place that turns a directory of loose YAML into a single buildable unit. Learn this file and the resources field it hangs everything on, and every other kustomize feature (generators, overlays, patches) becomes one more field in the same document.
The File At The Root
A kustomization is any directory holding a file named kustomization.yaml. kustomize also accepts kustomization.yml, or Kustomization with a capital K and no extension. Those three names are the entire list. Point kustomize at a directory that has none of them and it stops with an error instead of guessing. The file itself is a real Kubernetes-style object with its own apiVersion (which schema version it follows) and kind (what type of object it is), so schema validators and editor plugins treat it like any other manifest rather than a random text file.
You don't have to type that header by hand. Run kustomize create with the --autodetect flag and it walks the current directory, finds every manifest, and writes a fresh kustomization.yaml that lists them under resources. It's the quickest way to turn a folder of YAML into a kustomization you can build.
$ lsdeployment.yaml namespace.yaml service.yaml$ kustomize create --autodetect$ cat kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- deployment.yaml- namespace.yaml- service.yaml
The resources Field
resources is the heart of the file: an ordered list of paths. Each path is relative to the kustomization's own location, not to the folder your shell happens to be sitting in. You can run the build from three directories away and the paths still resolve against the file. Every entry is one of three things: a raw manifest file, a directory that has its own kustomization.yaml, or a remote git URL (a web address pointing at a git repository) that points at one. A single file may hold several documents separated by ---, and kustomize splits them apart for you.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- namespace.yaml # a single manifest- deployment.yaml # may hold several --- separated docs- service.yaml- ../base # a directory with its own kustomization.yaml- github.com/myorg/platform//monitoring?ref=v1.4.0 # a remote base, pinned to a tag
For a defender, resources is your allowlist. It is the exact set of objects that will ship to the cluster through this build. Nothing outside this list, and the trees it pulls in, reaches production this way. When you audit what an app deploys, you read this list first. When a reviewer sees a new entry show up in a pull request, that entry is a new thing entering the cluster, and it earns the same scrutiny as new code. The remote URL is the one to slow down on, because its content lives in someone else's repository.
What build Actually Emits
kustomize reads the list top to bottom, depth first: when it hits a directory or remote base it fully builds that before moving on. The order objects come out, though, is a separate question. By default kustomize runs a sort it calls 'legacy' over the finished output. Foundational kinds go first (Namespaces, then things like ConfigMaps, Secrets, and Services), and workloads such as Deployments go last, no matter how you listed them. Take a file that lists the Deployment first and the Namespace last.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- deployment.yaml # listed first- service.yaml- namespace.yaml # listed last
$ kustomize build .
apiVersion: v1kind: Namespacemetadata:name: shop---apiVersion: v1kind: Servicemetadata:name: webnamespace: shopspec:ports:- port: 80targetPort: 8080selector:app: web---apiVersion: apps/v1kind: Deploymentmetadata:name: webnamespace: shopspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- image: nginx:1.25name: web
The Namespace came out first even though you listed it last. That reordering is deliberate. It puts the foundational objects ahead of the workloads that depend on them, so a Namespace is defined before anything that lives inside it. For kubectl apply it rarely matters, because apply reconciles the whole set at once and retries what isn't ready yet. Ordering matters for tools sitting downstream of build that read the stream in sequence and act on each object as it arrives. If you need the output to mirror your list exactly, add sortOptions and set order to fifo (first in, first out).
# add near the top of the file, above resourcessortOptions:order: fifo # emit in the order resources are listed, no reordering
$ kustomize build . | grep '^kind:'
kind: Deploymentkind: Servicekind: Namespace
Now the output follows your list. One more thing worth knowing: kubectl carries a copy of kustomize inside it, so kubectl kustomize . builds without the standalone binary, and kubectl apply -k . builds and applies in a single step. The build itself is the same either way.
Why resources Can't Reach Outside The Root
A hotel key card opens your room and the pool, not the room next door. kustomize enforces a similar boundary. A component called the load restrictor (the part of kustomize that decides which paths a build is allowed to read) refuses, by default, any path in resources that climbs above the kustomization's own directory. Add a bare file from a parent folder, say - ../secrets/prod.yaml, and the build stops.
# run from /home/deploy/shop, building the overlay in ./app$ kustomize build ./app
Error: accumulating resources: accumulation err='accumulating resources from '../secrets/prod.yaml': security; file '/home/deploy/shop/secrets/prod.yaml' is not in or below '/home/deploy/shop/app'': must build at directory: '/home/deploy/shop/secrets/prod.yaml': file is not directory
Read the keyword in the middle: security. The restrictor exists so a kustomization can't quietly reach out and pull in files from outside its own tree, a stray private key sitting under /home, another team's secrets one directory over, a config file on the build host. You can still reference a parent, but only by pointing resources at a directory that carries its own kustomization.yaml. That is exactly why ../base is legal while ../secrets/prod.yaml is not. The base is a self-contained unit that declares what it exports; the loose file is just something grabbed from above with no declaration at all.
There is an override. Pass kustomize build with --load-restrictor LoadRestrictionsNone and the boundary turns off, letting resources read anything the build process can read on disk. It works, and it is occasionally the honest answer. It is also the setting you should reach for with a written reason attached, not out of habit.
Two smaller traps sit near this one. The old bases field still parses but is deprecated; list base directories under resources instead and you get the same result with fewer surprises. And paths are case-sensitive and always resolved against the file, not your terminal: kustomize build ./overlays/prod looks for resources under overlays/prod no matter which directory you launched the command from. A path that worked on your case-insensitive laptop can fail in a Linux build container where Base.yaml and base.yaml are two different files.
Verify The Build Before You Trust It
Before any of this touches a cluster, kustomize build is your dry run. It only reads files and prints text. It never contacts your cluster, and it changes nothing there. So make that output the thing you actually read. Count the objects and eyeball the kinds, then preview the change against the live cluster with a server-side dry run, which sends the manifests to the Kubernetes API (the cluster's control endpoint) for validation but writes nothing.
# what will ship: how many objects, and of what kind$ kustomize build . | grep '^kind:' | sort | uniq -c# preview against the real cluster without changing anything$ kustomize build . | kubectl apply -f - --dry-run=server
1 kind: Deployment1 kind: Namespace1 kind: Servicenamespace/shop created (server dry run)service/web created (server dry run)deployment.apps/web created (server dry run)
That count is the fastest sanity check you have. If you added one entry to resources and the object count jumps by five, a directory or remote base pulled in more than you expected, and you want to know that before it lands, not after. Get in the habit of diffing the object count in a pull request the same way you diff code.
Try this
Run deployment.yaml namespace.yaml service.yaml 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: a remote base runs on trust. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.