Declaratively Customize Kubernetes Resources with Kustomize

0


Managing Kubernetes manifests starts simple. You create a Deployment, add a Service, maybe add a ConfigMap, and apply everything with kubectl apply -f.

Then the application grows.

You need one version for development, another for staging, and another for production. The development environment might use one replica. Production might use three replicas, resource limits, different labels, a different namespace, and maybe a different image tag. Without a configuration management tool, the usual result is duplicated YAML files with small differences spread across multiple folders.

That is exactly the problem Kustomize solves.

Kustomize allows you to declaratively customize Kubernetes resources while keeping the original manifests reusable. Instead of copying the same YAML files for every environment, you define a common base and then apply environment-specific overlays on top of it.

Kustomize is especially useful when you want to stay close to plain Kubernetes YAML. It does not require a template language. It works by transforming real Kubernetes manifests.

What Is Kustomize?

Kustomize is a Kubernetes-native configuration management tool. It allows you to customize Kubernetes objects through a kustomization.yaml file.

The main idea is simple:

  • You keep common Kubernetes manifests in one place.
  • You declare which changes should be applied.
  • Kustomize renders the final YAML.
  • You apply the rendered result to the cluster.

For example, you can use Kustomize to:

  • Add the same namespace to multiple resources.
  • Add common labels and annotations.
  • Add a name prefix or suffix.
  • Patch only specific fields of a resource.
  • Generate ConfigMap and Secret resources from files or literals.
  • Change container image names or tags.
  • Reuse the same base configuration for devstaging, and prod.

Kustomize is available as a standalone binary, but it is also built into kubectl. That means you can render manifests with:

kubectl kustomize <directory>

And you can apply them directly with:

kubectl apply -k <directory>

The -k flag tells kubectl to look for a kustomization.yaml file instead of applying every YAML file blindly.

Why Plain YAML Becomes Hard to Maintain

Imagine this basic Kubernetes setup:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0
          ports:
            - containerPort: 80

This is fine for one environment.

But now suppose production needs:

  • replicas: 3
  • namespace prod
  • different labels
  • CPU and memory limits
  • a production image tag
  • stricter annotations

A common but poor solution is to copy the same file into another directory and change a few lines. That works once. It becomes painful when you have tens of manifests.

The problem is not only duplication. The bigger problem is drift. If the base deployment changes, you must remember to update every copied version. One missed update can create subtle differences between environments.

Kustomize avoids this by separating shared configuration from environment-specific customization.

Core Concepts

Base

A base is a directory that contains reusable Kubernetes manifests and a kustomization.yaml file.

The base should describe the application in a generic way. It usually contains resources such as:

  • Deployment
  • Service
  • ConfigMap
  • ServiceAccount
  • Ingress
  • NetworkPolicy

The base should avoid environment-specific details where possible.

Overlay

An overlay is another Kustomize directory that references a base and applies changes on top of it.

For example:

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

The base contains the common application. The dev and prod overlays reuse it and modify only what they need.

Patches

Patches allow you to change only specific parts of a resource.

For example, you can patch only the number of replicas:

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

You do not need to repeat the full deployment YAML.

Cross-Cutting Fields

Cross-cutting fields are changes that apply across multiple resources.

Common examples include:

  • namespace
  • namePrefix
  • nameSuffix
  • labels
  • annotations

Instead of adding the same namespace manually to every manifest, you can declare it once in kustomization.yaml.

First Kustomize Project

Let’s create a small NGINX application with a Deployment and a Service.

Create this folder structure:

kustomize-nginx/
└── base/
    ├── deployment.yaml
    ├── service.yaml
    └── kustomization.yaml

Deployment

Create base/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/component: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: nginx
      app.kubernetes.io/component: web
  template:
    metadata:
      labels:
        app.kubernetes.io/name: nginx
        app.kubernetes.io/component: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0
          ports:
            - containerPort: 80
              name: http

Service

Create base/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/component: web
spec:
  selector:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/component: web
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http

Kustomization File

Create base/kustomization.yaml:

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

resources:
  - deployment.yaml
  - service.yaml

Now render the result:

kubectl kustomize base

Kustomize will output the combined manifests for the Deployment and the Service.

To apply the base directly:

kubectl apply -k base

To delete the resources managed by this kustomization:

kubectl delete -k base

kubectl apply -f vs kubectl apply -k

This distinction is important.

When you run:

kubectl apply -f .

kubectl applies the manifest files it finds in the directory.

When you run:

kubectl apply -k .

kubectl looks for a kustomization.yaml file, renders the configured resources, applies transformations and patches, and then applies the final result.

With -k, the kustomization.yaml file controls what is included. A YAML file sitting in the same directory is not automatically applied unless it is referenced by the kustomization.

This makes the deployment more intentional.

Adding a Namespace with Kustomize

Suppose we want to deploy the application into a dev namespace.

One option is to add this to every resource:

metadata:
  namespace: dev

That works, but it creates repetition. With Kustomize, we can declare the namespace once:

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

namespace: dev

resources:
  - deployment.yaml
  - service.yaml

Now the rendered Deployment and Service will include:

metadata:
  namespace: dev

However, the namespace itself must exist before namespaced resources are created. You can create it separately:

kubectl create namespace dev

Or with a manifest:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

In many real projects, namespaces are treated as platform-level resources rather than application-level resources. That means I usually prefer to manage namespaces separately from the application kustomization, unless the application is truly responsible for creating its own namespace.

Creating Dev and Prod Overlays

Now let’s create a more realistic structure:

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

Dev Overlay

Create overlays/dev/replicas-patch.yaml:

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

Create overlays/dev/kustomization.yaml:

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

namespace: dev
namePrefix: dev-

labels:
  - pairs:
      environment: dev
    includeSelectors: false

resources:
  - ../../base

patches:
  - path: replicas-patch.yaml

images:
  - name: nginx
    newTag: 1.27.0

Render it:

kubectl kustomize overlays/dev

Apply it:

kubectl create namespace dev
kubectl apply -k overlays/dev

The final resources will be named with the dev- prefix, for example:

dev-nginx

Prod Overlay

Create overlays/prod/replicas-patch.yaml:

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

Create overlays/prod/resources-patch.yaml:

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

Create overlays/prod/kustomization.yaml:

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

namespace: prod
namePrefix: prod-

labels:
  - pairs:
      environment: prod
    includeSelectors: false

commonAnnotations:
  managed-by: kustomize
  owner: platform-team

resources:
  - ../../base

patches:
  - path: replicas-patch.yaml
  - path: resources-patch.yaml

images:
  - name: nginx
    newTag: 1.27.0

Apply it:

kubectl create namespace prod
kubectl apply -k overlays/prod

Now the same base application can be deployed differently into dev and prod.

Previewing Changes Before Applying

One of the best habits with Kustomize is to always inspect the rendered YAML before applying it.

Use:

kubectl kustomize overlays/prod

You can also compare what would change in the cluster:

kubectl diff -k overlays/prod

Then apply:

kubectl apply -k overlays/prod

This is a clean workflow for CI/CD pipelines as well:

kubectl kustomize overlays/prod > rendered.yaml
kubectl diff -f rendered.yaml
kubectl apply -f rendered.yaml

Or simply:

kubectl diff -k overlays/prod
kubectl apply -k overlays/prod

Working with Patches

Kustomize supports different patching approaches. The two most common are:

  • strategic merge style patches
  • JSON 6902 patches

Strategic Merge Style Patch

This is the style used above:

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

It looks like a partial Kubernetes manifest. Kustomize matches it to the target resource and merges the specified fields.

This is very readable for common Kubernetes objects.

JSON 6902 Patch

A JSON patch is more explicit. It uses operations such as addreplace, and remove.

For example, to replace the replica count:

- op: replace
  path: /spec/replicas
  value: 3

Then reference it from kustomization.yaml:

patches:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx
    path: replicas-json-patch.yaml

JSON patches are useful when you need precise control over a specific field, especially with custom resources or fields where strategic merge behavior is not enough.

Changing Images Without Patches

A common environment difference is the image tag.

Kustomize provides the images field so you do not need to patch the container manually.

images:
  - name: nginx
    newName: nginx
    newTag: 1.27.0

You can also replace the registry:

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

This is useful in CI/CD when the base manifest contains a stable image name and the overlay or pipeline injects the final tag.

For stronger production reproducibility, consider pinning images by digest:

images:
  - name: nginx
    digest: sha256:exampledigest

Generating ConfigMaps

Kustomize can generate ConfigMap resources from literals, files, or environment files.

Example:

base/
├── app.properties
└── kustomization.yaml

Create base/app.properties:

APP_MODE=production
LOG_LEVEL=info

Then add this to base/kustomization.yaml:

configMapGenerator:
  - name: nginx-config
    files:
      - app.properties

Kustomize will generate a ConfigMap.

By default, generated ConfigMap and Secret names include a content hash suffix. This is useful because when the content changes, the generated resource name changes too. Workloads that reference it can then roll out with the new configuration.

Example generated name:

nginx-config-g4hk9g2ff8

You can disable the suffix:

generatorOptions:
  disableNameSuffixHash: true

But be careful. Disabling the hash can make configuration changes less visible to workload rollout mechanisms.

Generating Secrets

Kustomize can also generate Kubernetes Secret resources.

Example:

secretGenerator:
  - name: nginx-secret
    literals:
      - username=admin
      - password=change-me

This is convenient for local labs and examples, but be careful in real projects.

A Kubernetes Secret is not automatically a secure secret-management strategy. Kustomize can generate the Secret object, but it does not magically protect plaintext values in your Git repository. For production, consider using an external secret management approach such as:

  • External Secrets Operator
  • Sealed Secrets
  • SOPS with Flux or another GitOps workflow
  • cloud-provider secret managers

A safe rule is: do not commit real plaintext credentials just because they are under secretGenerator.

Common Labels and Annotations

Labels and annotations help with grouping, selection, ownership, auditing, and automation.

Example:

labels:
  - pairs:
      app.kubernetes.io/part-of: demo-platform
      environment: prod
    includeSelectors: false

commonAnnotations:
  managed-by: kustomize
  contact: platform-team

Be careful with selector labels. Changing labels that are part of selectors can be dangerous because some selectors are immutable after creation, and changing pod-template labels can affect how services and deployments match pods.

Use includeSelectors: true only when you intentionally want Kustomize to update selectors as well.

Name Prefixes and Suffixes

Kustomize can add a prefix or suffix to resource names:

namePrefix: prod-
nameSuffix: -v1

This can produce names such as:

prod-nginx-v1

This is useful when multiple variants of the same base are deployed into the same cluster.

However, do not overuse name transformations. Stable and predictable names are often easier to debug. Prefixes and suffixes are useful when they express a real distinction, such as environment, tenant, or release variant.

Kustomize vs Helm

Kustomize and Helm are often compared, but they solve different problems.

DimensionKustomizeHelm
Main purposeCustomize existing Kubernetes YAML manifestsPackage, install, upgrade, and version Kubernetes applications
Configuration stylePlain YAML transformationsTemplates plus values files
Template languageNo template languageGo template language
Best forEnvironment overlays and small-to-medium customizationApplication packaging, dependency management, reusable charts
Learning curveLower if you already know Kubernetes YAMLHigher because you must learn chart structure and templating
OutputRendered Kubernetes manifestsRendered Kubernetes manifests applied as Helm releases
Versioning modelUsually handled by Git or external toolingBuilt into chart versions and releases

Use Kustomize when you want to customize raw Kubernetes manifests without introducing templating.

Use Helm when you want to package an application, manage chart dependencies, publish reusable charts, or operate with Helm release history.

They are not mutually exclusive. A common pattern is:

  • Use Helm to install a third-party application.
  • Use Kustomize to patch the rendered output or manage environment-specific overlays.

That said, avoid combining tools without a clear reason. Every extra layer adds operational complexity.

A Practical Full Example

Here is a complete minimal project.

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

Base Kustomization

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

resources:
  - deployment.yaml
  - service.yaml

Dev Kustomization

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

namespace: dev
namePrefix: dev-

resources:
  - ../../base

patches:
  - path: replicas-patch.yaml

images:
  - name: nginx
    newTag: 1.27.0

Prod Kustomization

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

namespace: prod
namePrefix: prod-

labels:
  - pairs:
      environment: prod
    includeSelectors: false

commonAnnotations:
  managed-by: kustomize

resources:
  - ../../base

patches:
  - path: replicas-patch.yaml
  - path: resources-patch.yaml

images:
  - name: nginx
    newTag: 1.27.0

Commands

Preview development:

kubectl kustomize overlays/dev

Apply development:

kubectl create namespace dev
kubectl apply -k overlays/dev

Preview production:

kubectl kustomize overlays/prod

Check production changes:

kubectl diff -k overlays/prod

Apply production:

kubectl create namespace prod
kubectl apply -k overlays/prod

Delete development:

kubectl delete -k overlays/dev

Delete production:

kubectl delete -k overlays/prod

Recommended Project Structure

For most applications, this structure works well:

my-app/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patches/
    │       └── replicas.yaml
    ├── staging/
    │   ├── kustomization.yaml
    │   └── patches/
    │       └── replicas.yaml
    └── prod/
        ├── kustomization.yaml
        └── patches/
            ├── replicas.yaml
            └── resources.yaml

Keep the base boring and reusable. Put environment-specific decisions in overlays.

Best Practices

Keep the Base Environment-Neutral

The base should not know whether it is deployed to devstaging, or prod.

Avoid putting these in the base unless they are truly universal:

  • namespace
  • replica count for a specific environment
  • production-only annotations
  • environment-specific image tags
  • environment-specific resource limits

Use Small Patches

Small patches are easier to review.

Prefer this:

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

Over a large patch that repeats the entire deployment.

Render Before You Apply

Always inspect the result:

kubectl kustomize overlays/prod

For production, use:

kubectl diff -k overlays/prod

This helps catch wrong namespaces, wrong names, incorrect image tags, and broken patches before they reach the cluster.

Be Careful with Selectors

Selectors are not just metadata. They control how resources match other resources.

For example:

  • Service selector decides which pods receive traffic.
  • Deployment selector decides which pods belong to the deployment.

Changing selector labels carelessly can break traffic routing or cause immutable-field errors.

Do Not Store Real Secrets in Git

Kustomize can generate Kubernetes Secret objects, but it does not encrypt your Git repository.

For production secrets, use a proper secret-management workflow.

Prefer Explicit Over Clever

Kustomize is powerful, but your manifests should still be easy to understand.

Avoid overly complex overlays where nobody can predict the rendered YAML without running the build. The final result should be clear, reviewable, and easy to debug.

Common Troubleshooting

kubectl apply -k Does Not Apply a File in the Directory

Kustomize only applies resources referenced by kustomization.yaml.

Check:

resources:
  - deployment.yaml
  - service.yaml

If a file is not listed, it is not part of the rendered output.

Namespace Not Found

If your kustomization sets:

namespace: dev

The namespace still needs to exist, unless you include a namespace manifest in the rendered resources.

Create it first:

kubectl create namespace dev

Patch Is Not Applied

Check that the patch target matches the resource name before name transformations.

For example, if the base deployment is named nginx, the patch should usually target:

metadata:
  name: nginx

Even if the overlay later adds:

namePrefix: prod-

Kustomize applies transformations in a controlled build process, so patches usually refer to the original resource identity.

Generated ConfigMap Name Keeps Changing

That is expected when the content changes. Kustomize appends a hash suffix to generated ConfigMap and Secret names by default.

This is useful for rollout behavior. Disable it only when you have a clear reason:

generatorOptions:
  disableNameSuffixHash: true

When Should You Use Kustomize?

Kustomize is a strong choice when:

  • You already have plain Kubernetes manifests.
  • You need environment-specific variations.
  • You want to avoid YAML duplication.
  • You do not need a full package manager.
  • You want a low-friction tool built into kubectl.
  • You want Git-friendly, reviewable configuration changes.

It is less ideal when:

  • You need reusable application packages for many teams.
  • You need dependency management between packaged applications.
  • You want chart repositories and release versioning.
  • You need advanced template logic with loops and conditionals.

In those cases, Helm may be a better fit.

Final Thoughts

Kustomize gives Kubernetes teams a practical way to manage configuration without abandoning native YAML.

The key benefit is separation:

  • The base describes what the application is.
  • The overlay describes how that application changes for a specific environment.

This keeps manifests reusable, reduces duplication, and makes environment differences explicit. For many Kubernetes projects, especially internal applications with devstaging, and prod environments, Kustomize offers a clean and maintainable approach.

Start simple:

kubectl kustomize overlays/dev
kubectl apply -k overlays/dev

Then grow from there with patches, image overrides, generated ConfigMaps, generated Secrets, labels, annotations, and production review workflows.

References

  • Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
  • Kubernetes: kubectl kustomize reference - kubernetes[.]io/docs/reference/kubectl/generated/kubectl_kustomize/
  • Kubernetes: Declarative Management of Kubernetes Objects Using Configuration Files - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/declarative-config/
  • Kustomize: Kubernetes Native Configuration Management - kustomize[.]io/
  • Helm: Charts - helm[.]sh/docs/topics/charts/
  • Helm: Chart Template Guide - helm[.]sh/docs/chart_template_guide/
  • Argo CD: Kustomize User Guide - argo-cd[.]readthedocs[.]io/en/release-3.0/user-guide/kustomize/
  • Flux: Kustomization API - fluxcd[.]io/flux/components/kustomize/kustomizations/

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!