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
- Base: The foundational set of Kubernetes resources (deployments, services, configmaps, etc.) stored in a directory with a
kustomization.yamlfile. - Overlay: A directory that references one or more bases and applies patches, name transformations, or additional resources on top of them.
- Patch: A partial YAML fragment that modifies specific fields in a resource (Strategic Merge Patch or JSON Patch).
- Generator: A built-in mechanism to dynamically create ConfigMaps, Secrets, or other resources from files, literals, or environment variables.
- Transformer: Rules that alter resource metadata, labels, annotations, namespaces, or images without explicit patches.
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
- No template syntax: You work directly with valid YAML. Every intermediate and final output is pure, valid Kubernetes YAML that you can validate independently.
- Declarative intent: You describe what changes you want rather than how to render them. This makes configurations easier to reason about.
- Reusability: A single base can serve dozens of overlays. Changes to the base automatically propagate to all overlays.
- Built-in kubectl support: Since Kustomize is integrated into kubectl, you don't need external tools or complex CI/CD pipelines to render configurations.
- Composability: Overlays can reference multiple bases, allowing you to compose complex applications from simpler building blocks.
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.