Kustomize Bases and Overlays: Managing Kubernetes Environments Without Duplicating YAML

0

Managing Kubernetes manifests is easy when an application has one environment and only a few resources. A Deployment, a Service, maybe a ConfigMap, and everything looks manageable.

The complexity appears when the same application needs to run in multiple environments:

  • Development may need one replica and a debug-friendly image tag.
  • Staging may need production-like settings but smaller resource limits.
  • Production may need more replicas, stricter labels, and a different namespace.
  • Some environments may need extra resources, while others should not include them.

Without a configuration tool, the common approach is often to copy the YAML files and change a few values. That works for a short time, but it creates duplication. When the base Deployment changes, every copied version must be updated manually. This is exactly the kind of configuration drift that makes Kubernetes environments harder to maintain.

Kustomize solves this problem with a simple but powerful model: bases and overlays.

base contains the shared Kubernetes resources. An overlay references that base and applies environment-specific changes on top of it. The base remains untouched, while each overlay generates the final manifests for a specific environment.

What Kustomize Does

Kustomize is a Kubernetes-native configuration management tool. It works with plain YAML manifests and does not require a separate templating language. Instead of writing templates with variables, conditionals, and loops, you define normal Kubernetes objects and then describe how they should be transformed.

You can use Kustomize in two common ways:

kubectl kustomize <kustomization-directory>

This renders the final YAML to the terminal.

kubectl apply -k <kustomization-directory>

This applies the rendered resources to the cluster.

The important part is that kubectl apply -k expects a directory, not a single YAML file. That directory must contain a kustomization.yaml file.

The Core Idea: Base Plus Overlay

A base is the reusable foundation of your application configuration. It usually contains resources that are common across environments:

  • Deployment
  • Service
  • ConfigMap
  • Secret references
  • ServiceAccount
  • Ingress
  • HorizontalPodAutoscaler
  • NetworkPolicy

An overlay is another Kustomize directory that points to the base and adds changes for a specific environment. Typical overlay changes include:

  • Namespace
  • Replica count
  • Image tag
  • Labels and annotations
  • Resource requests and limits
  • Environment-specific ConfigMaps
  • Ingress hostnames
  • Feature-specific patches

The relationship looks like this:

base resources + dev overlay  = final dev manifests
base resources + prod overlay = final prod manifests

The base does not know anything about the overlays. This is important. A clean base should stay environment-neutral unless the application is truly bound to one environment by design.

Recommended Directory Structure

A common structure is to keep one application folder, one base folder, and one folder per environment under overlays.

nginx-app/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── dev-replicas.yaml
    └── prod/
        ├── kustomization.yaml
        └── prod-replicas.yaml

This is not mandatory. Kustomize does not force a specific folder structure. However, this structure is simple, readable, and works well in Git repositories and GitOps workflows.

Creating the Base

Start with the base Deployment. This should be the shared version of the application. Do not put environment-specific values here unless they are genuinely common to all environments.

Create nginx-app/base/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/part-of: nginx-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: nginx
        app.kubernetes.io/part-of: nginx-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80

Now create the Service.

Create nginx-app/base/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/part-of: nginx-app
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: nginx
  ports:
    - name: http
      port: 80
      targetPort: 80

Finally, create the base kustomization.yaml.

Create nginx-app/base/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml

At this point, the base can be rendered directly:

kubectl kustomize nginx-app/base

This prints the Deployment and Service to the terminal. Nothing is applied to the cluster yet.

Creating the Dev Overlay

The dev overlay references the base and adds development-specific configuration.

Create nginx-app/overlays/dev/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: dev

resources:
  - ../../base

patches:
  - path: dev-replicas.yaml

images:
  - name: nginx
    newTag: "1.27"

Create nginx-app/overlays/dev/dev-replicas.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1

Now render the dev overlay:

kubectl kustomize nginx-app/overlays/dev

The output will contain the base Deployment and Service, but with the dev namespace applied.

Creating the Prod Overlay

Production can use the same base, but apply different settings.

Create nginx-app/overlays/prod/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: prod

resources:
  - ../../base

patches:
  - path: prod-replicas.yaml

images:
  - name: nginx
    newTag: "1.27"

Create nginx-app/overlays/prod/prod-replicas.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3

Render the production overlay:

kubectl kustomize nginx-app/overlays/prod

The same base is now used to generate production manifests, but with a different namespace and replica count.

Important Namespace Detail

The namespace field in kustomization.yaml sets the namespace on namespaced resources. It does not automatically create the Namespace object in the cluster.

This means the following overlay configuration:

namespace: dev

will add metadata.namespace: dev to resources such as Deployments and Services, but the dev Namespace must already exist.

You can create namespaces separately:

kubectl create namespace dev
kubectl create namespace prod

Or you can manage namespace objects declaratively:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

Whether namespace YAML belongs inside the overlay depends on ownership. If the namespace is part of the application lifecycle, include it in the overlay. If namespaces are managed centrally by the platform team, keep them outside the application Kustomize tree.

Applying the Overlays

After creating the namespaces, apply the dev overlay:

kubectl apply -k nginx-app/overlays/dev

Apply the production overlay:

kubectl apply -k nginx-app/overlays/prod

Check the resources:

kubectl get pods -n dev
kubectl get svc -n dev

kubectl get pods -n prod
kubectl get svc -n prod

You should see one NGINX Deployment in the dev namespace and another NGINX Deployment in the prod namespace. Both came from the same base manifests.

Preview Before Applying

One of the most useful Kustomize habits is to render before applying:

kubectl kustomize nginx-app/overlays/prod

You can also compare the desired state with the current cluster state:

kubectl diff -k nginx-app/overlays/prod

This is especially useful in CI/CD pipelines and GitOps workflows because it makes the final generated YAML reviewable.

Deleting Resources

Kustomize also works with kubectl delete -k.

kubectl delete -k nginx-app/overlays/dev
kubectl delete -k nginx-app/overlays/prod

This deletes the resources generated by each overlay. If Namespace objects are managed outside the overlay, they will not be deleted by this command.

What Actually Happens During a Kustomize Build

When you run:

kubectl kustomize nginx-app/overlays/prod

Kustomize performs a build process:

  1. It reads the overlay kustomization.yaml.
  2. It follows the resources path to the base.
  3. It loads the resources declared by the base.
  4. It applies overlay-level transformations, such as namespaceimages, labels, annotations, and patches.
  5. It emits the final YAML.

The base files are not modified. The result exists only as rendered output unless you apply it to the cluster.

This is the key difference between Kustomize and manual copying. You keep one shared source of truth and generate environment-specific output from it.

Customizing with Patches

Patches are one of the most important parts of Kustomize. They let you change selected fields without duplicating the whole resource.

A patch can be small and focused. For example, this patch changes only the replica count:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3

This patch does not need to repeat the entire Deployment. Kustomize matches it to the Deployment named nginx and merges the specified fields.

Strategic Merge Style Patch

For native Kubernetes resources such as Deployments, strategic merge style patches are often readable and convenient.

Example:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi

This patch changes the resource requests and limits for the container named nginx.

You can reference it from the overlay:

patches:
  - path: prod-resources.yaml

JSON 6902 Style Patch

Sometimes you need a more precise patch. JSON 6902 patches are useful when you want explicit operations such as addreplace, or remove.

Example:

patches:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 3

This is more verbose, but also very explicit. It is useful when patching arbitrary fields, custom resources, or fields where strategic merge behavior is not suitable.

Customizing Image Tags

Changing image tags is such a common operation that Kustomize has a dedicated images field.

Example:

images:
  - name: nginx
    newTag: "1.27.4"

You can also rewrite the image registry or repository:

images:
  - name: nginx
    newName: registry.example.com/platform/nginx
    newTag: "1.27.4"

This is cleaner than patching the full container spec when the only thing you need to change is the image reference.

For production, avoid relying on floating tags such as latest. Prefer immutable version tags or digests so that the same Git revision always produces a predictable deployment.

Adding Labels and Annotations

Overlays are also useful for cross-cutting metadata.

Example:

labels:
  - pairs:
      environment: prod
      app.kubernetes.io/managed-by: kustomize
    includeSelectors: false
    includeTemplates: true

commonAnnotations:
  owner: platform-team

Be careful with labels that affect selectors. Changing selectors on Deployments or Services can have operational consequences. For environment labels, it is often safer to apply them to resources and pod templates without automatically changing selectors.

Multiple Bases

An overlay can reference more than one base.

resources:
  - ../../base
  - ../../../shared/monitoring-sidecar
  - ../../../shared/network-policy

This can be useful when you have shared platform resources that should be composed with an application.

However, use multiple bases carefully. If every environment pulls in too many shared layers, it can become harder to understand where a field comes from. Keep the structure simple enough that a reviewer can still follow the generated output.

Bases, Overlays, and GitOps

Kustomize fits naturally with GitOps tools because the generated state is fully declarative.

A GitOps controller such as Argo CD can point directly to an overlay path:

nginx-app/overlays/prod

The controller renders that overlay and applies the resulting resources to the cluster. This keeps the Git repository clean:

  • The base contains common application manifests.
  • The overlay contains environment-specific intent.
  • The cluster receives fully rendered Kubernetes resources.

This also makes promotion easier. For example, a team can promote a new version by updating the image tag in the staging overlay, testing it, and then applying the same image tag to the production overlay.

When to Use Bases and Overlays

Bases and overlays are a good fit when you have mostly similar environments with predictable differences.

Good use cases include:

  • Same application deployed to dev, staging, and production.
  • Same service deployed to multiple clusters.
  • Different image tags per environment.
  • Different replica counts per environment.
  • Environment-specific Ingress hosts.
  • Optional production-only resource limits or autoscaling.

Kustomize may be less comfortable when you need heavy conditional logic, loops, or complex value interpolation. In those cases, Helm or another templating system may be a better fit. The strength of Kustomize is that it keeps the YAML close to normal Kubernetes YAML.

Best Practices

Keep the base environment-neutral. A base should usually not contain namespace: dev, production-only annotations, or environment-specific replica counts.

Keep patches small. A patch that changes one thing is easier to review than a patch that changes replicas, image tags, environment variables, probes, and resource limits at the same time.

Prefer the modern patches field. Older examples often use fields such as patchesStrategicMerge or patchesJson6902. The unified patches field is clearer for new configurations.

Render before applying. Use kubectl kustomize and kubectl diff -k as part of your normal workflow.

Do not hide too much in overlays. If a patch becomes almost as large as the original resource, consider whether the base is too generic or whether the environment really needs its own resource definition.

Be intentional with labels and selectors. Automatically changing selectors can break relationships between Services, Deployments, and Pods if not handled carefully.

Avoid generated output in Git unless you have a specific reason. Usually, Git should store the source manifests and overlays, not the rendered YAML.

Common Mistakes

MistakeWhy it causes problemsBetter approach
Copying full YAML files per environmentCreates drift and repeated maintenanceKeep common resources in base and patch differences in overlays
Putting namespaces in the baseMakes the base less reusableSet namespace in each overlay
Expecting namespace: to create a NamespaceKustomize only sets the namespace field on resourcesCreate Namespace objects separately or include them as resources
Using large patchesMakes reviews difficultSplit patches by concern
Applying without renderingHides the final stateRun kubectl kustomize or kubectl diff -k first
Using floating image tags in productionMakes deployments less predictableUse fixed tags or digests

Complete Example

Final directory tree:

nginx-app/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── dev-replicas.yaml
    │   └── kustomization.yaml
    └── prod/
        ├── prod-replicas.yaml
        └── kustomization.yaml

Render dev:

kubectl kustomize nginx-app/overlays/dev

Render prod:

kubectl kustomize nginx-app/overlays/prod

Apply dev:

kubectl apply -k nginx-app/overlays/dev

Apply prod:

kubectl apply -k nginx-app/overlays/prod

Delete dev:

kubectl delete -k nginx-app/overlays/dev

Delete prod:

kubectl delete -k nginx-app/overlays/prod

Conclusion

Kustomize bases and overlays provide a clean way to manage Kubernetes environments without duplicating YAML. The base acts as the reusable source of truth, while overlays describe what needs to change for each environment.

This model is simple, but it scales well. You can start with a Deployment and a Service, then gradually add patches, image overrides, metadata, ConfigMaps, Secrets, Ingress resources, and GitOps integration.

The main discipline is to keep the base clean and the overlays focused. When each overlay clearly describes its environment-specific differences, Kubernetes configuration becomes easier to review, easier to promote, and easier to maintain.

References

  • Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
  • Kustomize: Kubernetes native configuration management - kustomize[.]io/
  • Kubernetes SIG CLI: Kustomize GitHub repository - github[.]com/kubernetes-sigs/kustomize
  • Argo CD: Kustomize user guide - argo-cd.readthedocs[.]io/en/stable/user-guide/kustomize/

Post a Comment

0 Comments

Post a Comment (0)

#buttons=(Ok, Go it!) #days=(20)

This site uses cookies from Google to deliver its services and analyze traffic. Your IP address and user-agent are shared with Google along with performance and security metrics to ensure quality of service, generate usage statistics, and to detect and address abuse. More Info
Ok, Go it!