Kustomize’s base and overlay system is the most efficient way to manage Kubernetes configurations across multiple environments, but most people end up fighting it because they don’t grasp its fundamental, almost recursive, nature.

Let’s look at a simple deployment:

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: nginx:1.21.6
        ports:
        - containerPort: 80

This is our base. Kustomize treats this as the immutable core of our configuration. Anything we want to change for a specific environment will be done in an overlay.

Now, let’s create an overlay for development:

# overlays/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base

patchesStrategicMerge:
- deployment-patch.yaml

And the patch itself:

# overlays/development/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: my-app
        image: nginx:1.21.6-alpine # Updated image

When we run kustomize build overlays/development, Kustomize takes the base and applies the deployment-patch.yaml. It finds the Deployment named my-app and merges the specified fields. The replicas change from 1 to 2, and the image is updated to nginx:1.21.6-alpine.

Here’s the output:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 2 # Changed
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: nginx:1.21.6-alpine # Changed
        name: my-app
        ports:
        - containerPort: 80

Now, let’s make a staging overlay that builds upon the development overlay, rather than starting from the base again. This is where the recursive nature comes in.

# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../development # This points to the development overlay's kustomization.yaml

patchesStrategicMerge:
- deployment-patch.yaml

And a new patch for staging:

# overlays/staging/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 5 # Even more replicas
  template:
    spec:
      containers:
      - name: my-app
        image: nginx:1.21.6 # Back to the original image for staging

When you run kustomize build overlays/staging, Kustomize first resolves ../development. It effectively incorporates the output of kustomize build overlays/development as its starting point. Then, it applies overlays/staging/deployment-patch.yaml to that result.

The final output for staging will have 5 replicas and use the nginx:1.21.6 image.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 5 # Changed from dev's 2 to staging's 5
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: nginx:1.21.6 # Changed from dev's alpine to staging's original
        name: my-app
        ports:
        - containerPort: 80

The key here is that resources in a kustomization.yaml can point to another kustomization.yaml file, not just directories containing Kubernetes manifests. This allows for inheritance and composition. You can also use bases in kustomization.yaml to explicitly define a base that is then patched.

The power comes from layering. You can have a base for your core application, then an overlay for dev that adds debugging sidecars, another overlay for staging that increases replicas and uses a different registry, and a final prod overlay that applies network policies and more aggressive scaling. Each overlay inherits from the one below it, or directly from the base.

The most common mistake is treating overlays as isolated copies of the base. They are not. They are transformations that are applied sequentially. If you need to add a new resource (like a Service) to all environments, you add it to the base. If you need to change a replica count for one environment, you patch it in that environment’s overlay.

Kustomize also supports patches (for simple JSON merge patch) and patchesJson6902 (for JSON patch). patchesStrategicMerge is generally preferred for Kubernetes objects because it understands how to merge lists and complex types more intelligently. You can also use commonLabels, commonAnnotations, images, and replicas directly in the kustomization.yaml for simpler modifications without explicit patches.

The images field in kustomization.yaml is particularly useful for managing image tags across environments. You can specify newName and newTag to remap an image. For example, in overlays/development/kustomization.yaml:

images:
- name: nginx
  newName: my-docker-registry/nginx
  newTag: 1.21.6-alpine

This will automatically update any nginx image found in the base resources to use my-docker-registry/nginx:1.21.6-alpine. This is much cleaner than patching the image in every single deployment/statefulset/etc.

The next logical step after mastering base and overlays is understanding Kustomize plugins, which allow you to extend Kustomize’s functionality with custom generators and transformers written in Go or any executable.

Want structured learning?

Take the full Kustomize course →