DEV Community

secopslog for SecOpsLog

Posted on • Originally published at secopslog.com

Kustomize bases & overlays — one base, every environment

dev, staging, prod from one base.

A Kustomize base is a master recipe; overlays are the diners' tweaks. You cook one dish — a Deployment, a Service, a ConfigMap — that captures everything your app needs regardless of where it runs. Then, for each environment, you write a thin overlay that says "start from that recipe, but make dev quieter, staging half-sized, and prod cranked to eleven." Nothing is copied. The base stays the single source of truth, and every environment is expressed only as its difference from it. That difference is usually tiny — a handful of lines — which is exactly the point: the smaller the overlay, the less there is to drift, forget, or get wrong when you promote a change from dev to prod.

The base: your common denominator

A base is a complete, self-contained, deployable set of resources plus its own kustomization.yaml. That last part trips people up: a base is not a loose pile of YAML — it is itself a valid kustomization that lists the files it owns. Build it on its own and you get a working manifest for a generic, un-specialized deployment. Keep the base to the common denominator: the things that are true in every environment. The moment you find yourself adding a value that is only right for one environment, that value belongs in an overlay, not here. A useful test: could you hand this base to a teammate on a different cluster and have it apply cleanly? If it only works because dev's namespace happens to exist, it isn't really a base.

# myapp/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

# A base is a real kustomization: it names the raw files it owns.
# These are the resources EVERY environment shares, unspecialized.
resources:
  - deployment.yaml   # generic Deployment: image, ports, 1 replica
  - service.yaml      # generic ClusterIP Service
Enter fullscreen mode Exit fullscreen mode

Overlays: one per environment

An overlay is another kustomization.yaml that names the base in its resources: list — as a directory path, not a file. Below that reference sits only what makes this environment different: a distinct namespace, a name prefix so prod and dev objects never collide in a shared cluster, a replica count tuned to load. Kustomize reads the base, then layers your overlay changes on top, and emits the merged result. The overlay never edits the base's files; it declares deltas. This is why a three-line change to replica count in prod leaves dev and staging completely untouched — they don't share the overlay, only the base. Overlays can also stack — an overlay can itself serve as the base for another — but for dev/staging/prod a single flat base with three sibling overlays is the layout you want, and the one that stays legible a year later.

# myapp/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

# Point at the base by RELATIVE DIRECTORY path (not a file).
resources:
  - ../../base

# Everything below is only what makes prod differ from the base:
namespace: myapp-prod   # isolate prod objects
namePrefix: prod-       # avoid name collisions in shared clusters

# Bump replicas for production load.
# Match by the ORIGINAL base name (myapp); namePrefix is applied
# separately, so this still targets the renamed prod-myapp object.
# (Deeper surgical edits use strategic-merge or JSON6902 patches.)
replicas:
  - name: myapp
    count: 5
Enter fullscreen mode Exit fullscreen mode

Building each environment

Each environment is just a different directory to point the build at. kustomize build overlays/prod renders the fully-specialized prod manifests to stdout; kubectl apply -k overlays/prod does the same and applies them in one step. The key idea for overlays is that the same command, aimed at a different folder, produces a different environment from the identical base — no branching, no templating, no copy-paste drift between dev, staging, and prod. Diff two builds and the output is the set of deltas you declared, applied across the base.

myapp/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   └── service.yaml
└── overlays/
    ├── dev/kustomization.yaml
    ├── staging/kustomization.yaml
    └── prod/kustomization.yaml

# Same command, different folder = different environment:
kustomize build overlays/dev
kustomize build overlays/prod

# Or render + apply in one shot (-k runs kustomize on the dir):
kubectl apply -k overlays/prod
Enter fullscreen mode Exit fullscreen mode

⚠️ Paths resolve from the overlay, and 'bases:' is dead: Two things bite here. First, the old bases: field is deprecated — put base directories in the same resources: list as your files; kustomize treats a directory entry as a base and a file entry as a resource, so there is no separate field anymore. (A leftover bases: still builds but prints a deprecation warning; run kustomize edit fix to migrate it.) Second, paths in resources: are resolved relative to the overlay's own kustomization.yaml, not your shell's working directory — ../../base climbs up from overlays/prod/ to myapp/base/, and it will resolve the same no matter where you run the command from. By default, though, a kustomization cannot load a file from outside its own root: reference an individual YAML that sits above or beside your tree and the build fails with a security/load-restriction error. You can force it with kustomize build --load-restrictor LoadRestrictionsNone, but reaching for that flag is almost always a sign your layout is wrong, not that you need the escape hatch — restructure so the shared files live inside the tree instead.


This lesson is part of the free *Kustomize** course at SecOpsLog — hands-on, command-first DevSecOps tutorials. Read the original with the full interactive version →*

Top comments (0)