← Back to DevBytes

Kustomize Configuration Management: Complete Implementation Guide

What is Kustomize?

Kustomize is a configuration management tool for Kubernetes that enables you to customize YAML manifests without modifying the original files. It operates on a purely declarative, template-free approach—unlike Helm which uses Go templates, Kustomize applies a series of transformations (patches, overlays, generators) directly onto raw YAML resources. It is now built directly into kubectl via the kubectl apply -k command, making it a first-class citizen in the Kubernetes ecosystem.

At its core, Kustomize follows a "base and overlay" model. A base represents the common configuration shared across environments, while overlays represent environment-specific customizations that layer on top of the base. This separation allows you to define a single source of truth and then derive variants for development, staging, production, or even different clusters—all without duplicating YAML files.

Key Concepts

Why Kustomize Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Managing Kubernetes configurations across multiple environments traditionally leads to massive YAML duplication. Teams often resort to copy-paste with minor tweaks for each environment—changing replica counts, image tags, or resource limits. This approach is error-prone and creates a maintenance nightmare. Kustomize solves this by introducing a clean separation of concerns:

Benefits Over Template-Based Tools

Installation and Setup

Kustomize is available in several forms. The most convenient is the version embedded in kubectl (v1.14+). However, the standalone binary often provides newer features ahead of the kubectl integration.

Using the Standalone Binary

# On Linux/macOS via curl
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash

# Move to PATH
sudo mv kustomize /usr/local/bin/

# Verify installation
kustomize version

Using kubectl's Built-in Kustomize

# Check if your kubectl supports -k flag
kubectl apply -k --help

# The -k flag triggers Kustomize rendering before applying
kubectl apply -k ./path/to/overlay/

Directory Structure

A typical Kustomize project follows this pattern. The base/ directory holds the common configuration, and each overlay directory (such as dev/, staging/, prod/) contains its own kustomization.yaml with environment-specific customizations.

my-app/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── configmap.yaml
├── overlays/
│   ├── dev/
│   │   ├── kustomization.yaml
│   │   ├── replica-patch.yaml
│   │   └── configmap-patch.yaml
│   ├── staging/
│   │   ├── kustomization.yaml
│   │   └── ingress-patch.yaml
│   └── prod/
│       ├── kustomization.yaml
│       ├── resource-limits-patch.yaml
│       └── node-selector-patch.yaml

Creating Your First Base

A base is simply a directory containing a kustomization.yaml file that lists the resources you want to manage. Let's create a base for a simple web application.

Step 1: Write the Base Resources

Create 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
        ports:
        - containerPort: 80
        envFrom:
        - configMapRef:
            name: my-app-config

Create base/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
  labels:
    app: my-app
spec:
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 80

Create base/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  DATABASE_URL: "postgres://localhost:5432/default"
  LOG_LEVEL: "info"

Step 2: Create the Base Kustomization File

Create base/kustomization.yaml:

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

resources:
- deployment.yaml
- service.yaml
- configmap.yaml

# Optional: add a common label to all resources
commonLabels:
  team: platform
  env: base

Step 3: Verify the Base

# Build and view the rendered output
kustomize build ./base/

# Or using kubectl
kubectl kustomize ./base/

Building Overlays

Overlays allow you to customize the base for specific environments. Each overlay has its own kustomization.yaml that references the base and applies modifications.

Creating a Development Overlay

Create overlays/dev/kustomization.yaml:

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

# Reference the base
resources:
- ../../base

# Change the namespace
namespace: dev

# Add a name prefix to avoid clashes
namePrefix: dev-

# Override the number of replicas via a patch
patches:
- path: replica-patch.yaml
  target:
    kind: Deployment
    name: my-app
- path: configmap-patch.yaml
  target:
    kind: ConfigMap
    name: my-app-config

# Add dev-specific labels
commonLabels:
  env: development

# Override image tags
images:
- name: nginx
  newTag: "1.22-alpine"

Create overlays/dev/replica-patch.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: my-app
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"

Create overlays/dev/configmap-patch.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  DATABASE_URL: "postgres://dev-db:5432/devdb"
  LOG_LEVEL: "debug"
  FEATURE_TOGGLE: "true"

Build the dev overlay:

kustomize build ./overlays/dev/

Creating a Production Overlay

Create overlays/prod/kustomization.yaml:

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

resources:
- ../../base

namespace: production

namePrefix: prod-

patches:
- path: replica-patch.yaml
  target:
    kind: Deployment
    name: my-app
- path: configmap-patch.yaml
  target:
    kind: ConfigMap
    name: my-app-config

commonLabels:
  env: production

images:
- name: nginx
  newTag: "1.21.6"

Create overlays/prod/replica-patch.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: my-app
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"

Create overlays/prod/configmap-patch.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  DATABASE_URL: "postgres://prod-db-primary:5432/proddb"
  LOG_LEVEL: "warn"
  FEATURE_TOGGLE: "false"

Working with Patches

Kustomize supports multiple patching strategies. Understanding when to use each is crucial for maintainable configurations.

Strategic Merge Patches

Strategic Merge Patch (SMP) is the default patching mechanism. It uses the Kubernetes API's understanding of each resource kind to merge changes intelligently. For example, when patching a Deployment's container list, it merges by container name rather than blindly replacing the entire list.

# patches/strategic-merge.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      # This merges by name, so only the "my-app" container is modified
      - name: my-app
        env:
        - name: ENV_SPECIFIC_VAR
          value: "overridden-value"

In your kustomization.yaml, reference it as:

patchesStrategicMerge:
- patches/strategic-merge.yaml

JSON 6902 Patches

JSON Patch (RFC 6902) provides precise, path-based modifications. This is useful when you need to target deeply nested fields or perform operations like add, replace, remove, copy, or move.

# patches/json-patch.yaml
- op: replace
  path: /spec/template/spec/containers/0/image
  value: nginx:1.23-alpine

- op: add
  path: /spec/template/spec/containers/0/env/-
  value:
    name: NEW_ENV_VAR
    value: "added-via-json-patch"

- op: remove
  path: /metadata/labels/deprecated-label

Reference JSON patches in kustomization.yaml:

patchesJson6902:
- target:
    group: apps
    version: v1
    kind: Deployment
    name: my-app
  path: patches/json-patch.yaml

Inline Patches

For small changes, you can define patches directly in the kustomization.yaml file using the patches field:

patches:
- patch: |-
    - op: replace
      path: /spec/replicas
      value: 5
  target:
    kind: Deployment
    name: my-app

ConfigMap and Secret Generators

Generators dynamically create resources at build time, which is especially useful for ConfigMaps and Secrets that need to be environment-specific or contain file contents.

ConfigMap Generator

There are three ways to generate ConfigMaps: from literals, from files, or from environment files.

# In kustomization.yaml
configMapGenerator:
# Generate from literal key-value pairs
- name: app-config-literals
  literals:
  - APP_NAME=MyKustomizeApp
  - APP_VERSION=2.0.1
  - ENABLE_FEATURE_X=true

# Generate from a properties file
- name: app-config-from-file
  files:
  - application.properties
  - database.properties

# Generate from an env file
- name: app-config-from-envfile
  envs:
  - .env.production

The generated ConfigMap automatically gets a content-based hash suffix appended to its name (e.g., app-config-literals-7k4m2f). This triggers a rolling update in Deployments that reference it, ensuring pods pick up the new configuration without manual intervention.

Secret Generator

Secret generators work identically but store values as base64-encoded Secrets:

# In kustomization.yaml
secretGenerator:
- name: app-secrets
  literals:
  - DB_PASSWORD=s3cretPass!
  - API_KEY=sk-1234567890abcdef

- name: tls-secrets
  files:
  - tls.crt
  - tls.key

# Disable the hash suffix if needed (rarely recommended)
# type: Opaque
# options:
#   disableNameSuffixHash: true

Generator Options

You can control generator behavior with options:

configMapGenerator:
- name: special-config
  options:
    # Disable the content hash suffix
    disableNameSuffixHash: true
    # Add immutable annotation
    immutable: true
  literals:
  - STABLE_KEY=never-changes

Transformers and Metadata Management

Kustomize provides several built-in transformers that modify resource metadata without explicit patches.

Common Labels and Annotations

# In kustomization.yaml
commonLabels:
  app: my-app
  version: v2.0.0
  managed-by: kustomize

commonAnnotations:
  deployment.kubernetes.io/revision: "5"
  note: "Applied by CI/CD pipeline on 2024-01-15"

Name Prefix and Suffix

namePrefix: dev-
# Results in: dev-my-app, dev-my-app-service

nameSuffix: -v2
# Results in: my-app-v2, my-app-service-v2

Namespace Transformation

namespace: my-namespace
# Sets the namespace on ALL resources that support it

Image Transformation

The images field overrides image references across all resources without needing patches:

images:
# Override just the tag
- name: nginx
  newTag: "1.23.3-alpine"

# Override the entire image reference including registry
- name: my-app
  newName: registry.example.com/my-app
  newTag: "sha256:abc123..."

# Override the digest
- name: postgres
  digest: sha256:abcdef1234567890

Advanced Composition: Multiple Bases

Kustomize shines when composing complex applications from multiple independent bases. You can reference several bases in a single overlay.

# overlays/full-stack/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../frontend/base
- ../../backend/base
- ../../database/base
- ../../monitoring/base

# Apply consistent labels across all resources from all bases
commonLabels:
  stack: full-stack
  env: integration

# Override images in all bases at once
images:
- name: frontend-app
  newTag: "v2.1.0"
- name: backend-api
  newTag: "v1.5.3"

Variable Substitution with Vars

Sometimes you need to reference a value from one resource in another. Kustomize's vars feature allows limited variable substitution using placeholder syntax.

First, mark a resource field as a variable source:

# In your base kustomization.yaml
vars:
- name: SERVICE_NAMESPACE
  objref:
    kind: Service
    name: my-app-service
    apiVersion: v1
  fieldref:
    fieldpath: metadata.namespace

Then reference it in another resource using $(SERVICE_NAMESPACE):

# In a ConfigMap or command argument
data:
  SERVICE_ENDPOINT: "http://my-app-service.$(SERVICE_NAMESPACE).svc.cluster.local"

Note: The vars feature is deprecated in favor of replacement transformers in newer Kustomize versions. Use the replacements field for complex value propagation.

The Replacements Feature

Introduced in Kustomize v4.0+, replacements provide a more robust way to copy values between resources.

# In kustomization.yaml
replacements:
- source:
    kind: ConfigMap
    name: app-config
    fieldPath: data.SERVICE_PORT
  targets:
  - select:
      kind: Deployment
      name: my-app
    fieldPaths:
    - spec.template.spec.containers.0.env.0.value
    options:
      create: true

This copies the value from app-config's SERVICE_PORT data key into the first environment variable of the first container in the my-app Deployment.

Working with Helm Charts

Kustomize can integrate with Helm charts, allowing you to use Helm as a package manager while leveraging Kustomize for post-render customizations.

# kustomization.yaml referencing a Helm chart
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

helmCharts:
- name: ingress-nginx
  repo: https://kubernetes.github.io/ingress-nginx
  version: 4.7.1
  releaseName: my-ingress
  namespace: ingress-system
  valuesFile: ingress-values.yaml
  includeCRDs: true

# Apply Kustomize transformations on top of Helm output
patches:
- path: ingress-patch.yaml
  target:
    kind: Deployment
    name: my-ingress-ingress-nginx-controller

Build it with:

kustomize build --enable-helm .

Component-Based Architecture

Components are reusable Kustomize modules that can be included in multiple overlays. Unlike bases, components don't produce standalone output—they augment existing resources.

Create a component directory components/health-checks/:

# components/health-checks/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component

patches:
- path: add-healthcheck-patch.yaml
  target:
    kind: Deployment
    labelSelector: "app.kubernetes.io/healthcheck-enabled=true"
# components/health-checks/add-healthcheck-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: any
spec:
  template:
    spec:
      containers:
      - name: "*"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

Use the component in an overlay:

# overlays/with-healthchecks/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

components:
- ../../components/health-checks

Testing and Validation

Kustomize provides commands to validate your configuration before applying it to a cluster.

Build and Inspect

# Build and view the full rendered output
kustomize build ./overlays/prod/

# Build and output as a single multi-document YAML stream
kustomize build ./overlays/prod/ > rendered-manifests.yaml

# Build with verbose output to see transformations
kustomize build --verbose ./overlays/prod/

Dry-Run with kubectl

# Preview what would be applied without actually applying
kubectl apply -k ./overlays/prod/ --dry-run=client -o yaml

# Save the dry-run output for review
kubectl apply -k ./overlays/prod/ --dry-run=client -o yaml > preview.yaml

Validate Against a Live Cluster (Server-Side Dry-Run)

# Validate against the cluster API without persisting resources
kubectl apply -k ./overlays/prod/ --dry-run=server

CI/CD Integration Patterns

Kustomize fits naturally into GitOps workflows and CI/CD pipelines. Here are common integration patterns.

Pattern 1: Build-and-Apply Pipeline

# Simple CI/CD script snippet
#!/bin/bash
set -e

# Build the overlay for the target environment
kustomize build overlays/prod/ > deploy/prod-manifests.yaml

# Apply to cluster
kubectl apply -f deploy/prod-manifests.yaml

# Wait for rollout
kubectl rollout status deployment/prod-my-app -n production --timeout=300s

Pattern 2: GitOps with ArgoCD or Flux

ArgoCD natively supports Kustomize. Point it at your repository and specify the overlay path:

# Example ArgoCD Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-prod
spec:
  source:
    repoURL: https://github.com/myorg/my-app-config
    path: overlays/prod
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Pattern 3: Kustomize in Tekton/GitHub Actions

# GitHub Actions step
- name: Build and Deploy Kustomize Overlay
  run: |
    # Use kubectl's built-in kustomize
    kubectl kustomize ./overlays/staging/ \
      | kubectl apply -f - --dry-run=server \
      && kubectl apply -f -
  env:
    KUBECONFIG: ${{ secrets.KUBECONFIG }}

Best Practices

1. Keep Bases Minimal and Generic

A base should contain only the resources that are truly common across all environments. Avoid putting environment-specific values (like replica counts, resource limits, or endpoint URLs) in the base. Instead, define sensible defaults and override them in overlays.

# Good: Base deployment has minimal defaults
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: my-app
        image: my-app  # No tag specified—overlays set it

2. Use Generators for Dynamic Content

Never commit generated ConfigMaps or Secrets with content-based hashes to Git. Let Kustomize generate them at build time. This ensures freshness and avoids stale configurations.

# In .gitignore
# Ignore rendered output
rendered/
deploy/generated/

3. Structure Overlays by Environment and Cluster

For multi-cluster deployments, consider a two-dimensional overlay structure:

overlays/
├── dev/
│   ├── cluster-a/
│   │   └── kustomization.yaml
│   └── cluster-b/
│       └── kustomization.yaml
├── prod/
│   ├── cluster-a/
│   │   └── kustomization.yaml
│   └── cluster-b/
│       └── kustomization.yaml

4. Leverage Strategic Merge Patches for Containers

When patching Deployments or StatefulSets, always use Strategic Merge Patches for container modifications. The Kubernetes API merges containers by name, so you only need to specify the container name and the fields you want to change.

# Patch only the resource limits for the "my-app" container
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: my-app
        resources:
          limits:
            cpu: "2"

5. Use JSON Patches Sparingly

JSON Patches are powerful but brittle—they rely on array indices that can change. Reserve them for cases where Strategic Merge Patches cannot express the desired change (like removing a specific list element or modifying a field that Strategic Merge Patch doesn't understand).

6. Document Your Kustomization Structure

Include a README.md in the repository root explaining the directory layout, what each overlay represents, and how to build and deploy. This reduces onboarding friction for new team members.

# Example README.md content
## Kustomize Layout

- `base/` - Common resources shared by all environments
- `overlays/dev/` - Development environment (minimal resources, debug logging)
- `overlays/staging/` - Pre-production (moderate resources, integration endpoints)
- `overlays/prod/` - Production (high availability, resource limits, node selectors)

## Building
bash
kustomize build overlays/dev/

7. Pin Image Versions in Overlays

Never use the :latest tag. Overlays should explicitly set image tags or digests. This ensures reproducible deployments and simplifies rollbacks.

images:
- name: my-app
  newTag: "v1.2.3"
  # Or even better, use a digest
  digest: sha256:abc123def456...

8. Validate Before Applying

Always run kustomize build and inspect the output or use --dry-run=server before applying to a live cluster. This catches configuration errors early.

9. Keep Overlay Customizations Focused

Each overlay should have a clear, single responsibility. For example, one overlay for development settings, another for production settings. Avoid creating overlays that mix concerns (like "dev-cluster-a-with-experimental-features" and "dev-cluster-a-without-experimental-features"). Instead, use multiple bases and compose them.

10. Use Components for Cross-Cutting Concerns

When you have modifications that apply across many different applications (like adding a sidecar proxy, enabling mTLS, or injecting health checks), create a Component. This avoids duplicating the same patches across multiple overlays.

Common Pitfalls and Troubleshooting

Pitfall: Strategic Merge Patch Doesn't Work

SMP relies on the name field within lists (like containers or volumes). If you omit the name in your patch, Kustomize cannot identify which element to merge and may append a new element instead of modifying the existing one.

# Wrong: Missing container name—this will ADD a new container
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - resources:
          limits:
            cpu: "1"

# Correct: Specifies the container name for proper merging
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: my-app
        resources:
          limits:
            cpu: "1"

Pitfall: Name Conflicts After Transformation

When using namePrefix or nameSuffix, be aware that cross-resource references (like Service selectors pointing to Deployment labels) may break if you don't also update the references. Use commonLabels to ensure selectors remain consistent.

Pitfall: Large File Counts Slow Down Builds

If your base contains hundreds of resource files, builds can become slow. Consider splitting large bases into smaller, composable bases and combining them in overlays as needed.

Migration from Other Tools

Migrating from Helm

If you're migrating from Helm to Kustomize, start by rendering your Helm chart to static YAML:

# Render Helm chart to static files
helm template my-release ./chart/ --values values.yaml > helm-output.yaml

# Split into individual resource files
# Then organize into a Kustomize base structure

Alternatively, use the helmCharts feature in Kustomize to gradually transition—start by wrapping your Helm chart in a Kustomize overlay, then progressively extract resources into Kustomize-managed files.

Migrating from Plain YAML with envsubst

Replace environment variable substitution with Kustomize overlays. Each overlay becomes the equivalent of a set of environment-specific values:

# Old: envsubst with $REPLICAS placeholder
# New: Kustomize overlay patch setting replicas directly

Conclusion

Kustomize represents a fundamental shift in how teams manage Kubernetes configurations—moving away from template-driven generation toward declarative, layered customization. By embracing the base-and-overlay model, you eliminate YAML duplication, reduce the risk of configuration drift, and create a single source of truth that serves all environments consistently. The tight integration with kubectl means zero additional tooling is required to adopt it, and the support for generators, transformers, patches, and components gives you a rich toolkit to handle even complex multi-cluster, multi-environment deployments. Start small by converting one application's static YAML into a base with a few overlays, then gradually expand as your team gains confidence. The result is a configuration management system that is transparent, auditable, and fully aligned with Kubernetes-native patterns—exactly what modern cloud-native delivery pipelines demand.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles