CoursesKustomizekustomization.yaml & resources

kustomization.yaml & resources

The file that ties it together.

Intermediate12 min · lesson 2 of 12

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.

terminal
$ ls
deployment.yaml namespace.yaml service.yaml
$ kustomize create --autodetect
$ cat kustomization.yaml
output
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 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.

kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 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.

A remote base runs on trust
A git URL in resources pulls another repository's manifests, patches, and transformers into your build. Pin it to a branch (?ref=main) and you get whatever that branch says today, which someone else controls and can change under you. Pin remote bases to a commit SHA (the unique fingerprint git gives every commit) or to a tag you never move, and actually read what you are importing. A poisoned upstream base is a supply-chain path straight into your cluster.

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.

kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml # listed first
- service.yaml
- namespace.yaml # listed last
terminal
$ kustomize build .
output
apiVersion: v1
kind: Namespace
metadata:
name: shop
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: shop
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: shop
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: nginx:1.25
name: 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).

kustomization.yaml
# add near the top of the file, above resources
sortOptions:
order: fifo # emit in the order resources are listed, no reordering
terminal
$ kustomize build . | grep '^kind:'
output
kind: Deployment
kind: Service
kind: 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.

What kustomize build does with your resources list
1A directory with kustomization.yaml
the root kustomize builds from
2Read resources, top to bottom
paths relative to the file, depth first
3Gather every target
files, sub-kustomizations, remote bases
4Split and apply edits
one --- stream, patches and transformers run
5Sort the output
legacy kinds order by default, fifo keeps your order
6Print finished YAML
kubectl apply -f - takes it from here

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.

terminal
# run from /home/deploy/shop, building the overlay in ./app
$ kustomize build ./app
output
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.

LoadRestrictionsNone is a door you rarely want open
Turning off the load restrictor lets a kustomization read any file the build user can read, which is precisely what an attacker who can edit your manifests wants: point resources at a secret elsewhere on disk and let the build fold it into the output stream. GitOps controllers (systems like Argo CD and Flux that keep a cluster matching a git repository) keep the restrictor on for this reason, and many refuse to disable it at all. If you flip it, you have widened the build's blast radius, so treat it as a logged exception, not a default.

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.

terminal
# 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
output
1 kind: Deployment
1 kind: Namespace
1 kind: Service
namespace/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.

Quick check
01An overlay builds fine with ../base in resources, but adding ../secrets/prod.yaml stops the build with a 'security; file ... is not in or below' error. What separates the two entries?
Incorrect — The check runs on every entry no matter where you launch from, since paths resolve against the kustomization file and not your shell.
Correct — That announcement is the whole difference. A directory carrying a kustomization states its own contents, so composing it stays inside the rules.
Incorrect — Position in the list has no bearing on the boundary. Move that secret entry anywhere you like and the same security error comes back.
Incorrect — Rename the file to anything you please and it still fails, because the objection is to a path climbing above the kustomization root.
02resources lists deployment.yaml, service.yaml, namespace.yaml in that order and the file has no sortOptions. What does kustomize build . | grep '^kind:' print?
Incorrect — That is the output you get once sortOptions is set with order: fifo. Leave it out and kustomize reorders the stream before printing.
Incorrect — Sorting by letter is not what happens here. The default groups by role, so a Namespace leads and a Deployment trails whatever the alphabet says.
Correct — The default sort is called legacy and it always runs, so the Namespace is defined ahead of the Deployment that sits inside it.
Incorrect — Output order is repeatable. Run the build ten times over the same input and the kind lines come back identical every time.
03A pull request adds one line to resources: github.com/other-org/platform//monitoring?ref=main, and the build passes. What should the reviewer say about it?
Incorrect — There is no copy step. Every build fetches from that URL, so whatever the branch points at today is what lands in your output.
Incorrect — The restrictor rules on local paths that climb above the root. A git URL is a supported resources entry and builds without complaint.
Incorrect — Copying works but costs you every future update. A SHA or an unmoving tag buys the same repeatability while letting you take upstream changes on purpose.
Correct — A branch is a moving target by design. Pin to something that cannot change and the import becomes one you can review once and rely on.

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.

Related