Kustomize: Native Kubernetes Configuration Management
Use Kustomize for declarative Kubernetes configuration management without Helm's templating—overlays, patches, and environment-specific customization.
Introduction
Managing Kubernetes manifests across multiple environments starts simple and gets unwieldy fast. Base config, dev overrides, staging overrides, prod overrides — copying and pasting YAML across all of that is a full-time job that nobody wants. Kustomize solves this without a templating engine or new programming language.
It is built into kubectl since 1.14, so nothing to install separately. You get a base with your canonical manifests, then overlays that only contain the differences for each environment. Look at any overlay and you see exactly what changed from base — no template rendering in your head required.
This post covers the basics: base and overlay structure, patches for modifying resources, generators for ConfigMaps and Secrets, and components for reusable blocks. You will also learn when Kustomize makes sense versus Helm, and where teams run into trouble managing configuration across large deployments.
When to Use / When Not to Use
When Kustomize makes sense
Kustomize is a good fit when your team owns the application manifests directly and wants environment-specific variations without introducing a new templating layer. If your dev, staging, and production configs differ in straightforward ways (replica counts, image tags, namespace names), overlays let you express those differences clearly without Go template syntax.
GitOps workflows benefit from Kustomize because the source YAML stays readable and diffable. You do not need to mentally render a template to understand what will be deployed.
For platform teams building reusable bases that other teams consume as a starting point, Kustomize components provide composition without publishing packages.
When to choose Helm instead
If you need to distribute a reusable package to users who should not see the underlying template logic, Helm charts are better. The extensive ecosystem of Bitnami and public charts gives you off-the-shelf solutions for databases, caches, and middleware.
When your configuration varies in complex ways that do not map cleanly to overlays and patches, Helm templating offers more expressive power.
Kustomize Workflow Flow
flowchart TD
A[base/<br/>kustomization.yaml] --> B[Overlays]
B --> C[development/<br/>kustomization.yaml]
B --> D[staging/<br/>kustomization.yaml]
B --> E[production/<br/>kustomization.yaml]
C --> F[kubectl kustomize<br/>./overlays/development]
D --> G[kubectl kustomize<br/>./overlays/staging]
E --> H[kubectl kustomize<br/>./overlays/production]
F --> I[Transformed<br/>YAML manifests]
G --> I
H --> I
Kustomize Overview and kubectl Integration
Kustomize is built into kubectl since Kubernetes 1.14, so you do not need separate installation. It reads kustomization.yaml files and produces transformed Kubernetes manifests.
# Basic usage with kubectl
kubectl apply -k ./overlays/production
# Build without applying
kubectl kustomize ./base
# View diff before applying
kubectl diff -k ./overlays/production
A simple Kustomize structure:
app/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── development/
│ └── kustomization.yaml
└── production/
└── kustomization.yaml
Base and Overlay Structure
The base directory contains your canonical configuration. Overlays modify the base for specific environments.
Base kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app.kubernetes.io/part-of: myapp
images:
- name: nginx
newTag: "1.21"
Base deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
Development overlay:
# overlays/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
namespace: myapp-dev
namePrefix: dev-
commonLabels:
environment: development
replicas:
- name: myapp
count: 1
images:
- name: nginx
newTag: "1.21-debug"
Production overlay:
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
namespace: myapp-prod
namePrefix: prod-
commonLabels:
environment: production
replicas:
- name: myapp
count: 5
images:
- name: nginx
newTag: "1.21.1"
Patches and Strategic Merge
Kustomize supports two patch strategies: Strategic Merge Patches (similar to kubectl patch) and JSON 6902 patches (RFC 6902 JSON Pointer).
Strategic merge patches:
# patches/replica-change.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 10
# kustomization.yaml
patches:
- path: patches/replica-change.yaml
JSON 6902 patches for fine-grained control:
# patches/resource-limits.json
[{
"op": "add",
"path": "/spec/template/spec/containers/0/resources/limits/memory",
"value": "512Mi"
}]
# kustomization.yaml
patches:
- target:
kind: Deployment
name: myapp
path: patches/resource-limits.json
Targeted patches:
# Patch only resources matching labels
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: 3
target:
labelSelector: tier=frontend
Generators (ConfigMap and Secret)
Kustomize can generate ConfigMaps and Secrets from files, literals, or env files.
Literal-based ConfigMap:
# kustomization.yaml
configMapGenerator:
- name: app-config
literals:
- DATABASE_HOST=localhost
- DATABASE_PORT=5432
- LOG_LEVEL=info
File-based ConfigMap:
# config/app.properties
database.url=jdbc:postgresql://localhost:5432/mydb
cache.enabled=true
# kustomization.yaml
configMapGenerator:
- name: app-config
files:
- config/app.properties
Env file ConfigMap:
# envvars.txt
PORT=8080
WORKERS=4
# kustomization.yaml
configMapGenerator:
- name: app-env
envs:
- envvars.txt
Generate Secret:
configMapGenerator:
- name: app-secret
literals:
- api-key=$(API_KEY)
envs:
- secrets.txt
Kustomize Components for Reuse
Components allow reusable configuration blocks that can be imported across multiple kustomizations.
Define a monitoring component:
# components/monitoring/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha
kind: Component
resources:
- deployment-metrics.yaml
- service-monitor.yaml
patches:
- patch: |-
- op: add
path: /spec/template/spec/containers/-
value:
name: prometheus
image: prometheus:latest
ports:
- containerPort: 9090
target:
kind: Deployment
Use the component:
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
components:
- ../../components/monitoring
patches:
- path: patches/production-config.yaml
Kustomize vs Helm Comparison
Both tools solve configuration management but with different philosophies:
| Aspect | Kustomize | Helm |
|---|---|---|
| Approach | Overlay/patch | Template with values |
| Learning curve | Lower (no new syntax) | Higher (Go templates) |
| Flexibility | Limited to kustomize features | Highly flexible |
| Debugging | Direct YAML output | Rendered templates |
| Secret management | Built-in generators | External tools needed |
| Ecosystem | Native to Kubernetes | Large chart repository |
| Best for | App-centric deployments | Off-the-shelf packages |
Choose Kustomize when you want direct control over your YAML and do not need to package complex application logic. Choose Helm when you want to distribute reusable packages or need the extensive ecosystem of public charts.
Production Failure Scenarios
Name prefix collisions across teams
If two teams both use namePrefix: dev- in their overlays and deploy to the same cluster, resources can collide. The dev-api Deployment from team A overwrites the dev-api Deployment from team B.
Use team-specific prefixes or deploy to isolated namespaces.
Patch targets wrong resources
Strategic merge patches apply to all resources matching the selector. If you have multiple Deployments with the same labels, a replica patch affects all of them simultaneously.
Always use precise target selectors:
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: 10
target:
name: myapp
kind: Deployment
ConfigMap generator creates new resources on every run
Kustomize generates ConfigMaps with content hashes in the name by default. Changing a ConfigMap literal creates a new ConfigMap with a different hash and deletes the old one. If a Deployment references the old ConfigMap name, the old ConfigMap cannot be deleted because the Deployment still needs it.
Pin ConfigMap generator outputs explicitly or use behavior: merge to update existing ConfigMaps rather than replacing them.
Overlay path not found
If you reference ../../base in an overlay and the path is wrong, Kustomize fails with a confusing error about a missing file. The error message does not clearly indicate it is a path resolution problem.
Use absolute paths or ensure your relative path references are correct before running in CI.
Common Pitfalls / Anti-Patterns
Overly deep directory nesting
Nesting overlays five levels deep (base, then environment, then region, then cluster, then tenant) makes it impossible to understand what the final manifest looks like without running kubectl kustomize. Keep nesting shallow and use components for composition.
Duplicate resources across overlays
If base defines a resource and an overlay also defines the same resource without using patches or strategic merge, Kustomize produces duplicate resource errors. This is especially confusing because the error appears at apply time, not at kustomize build time.
Audit your overlays regularly with kubectl kustomize ./overlay | kubectl apply --dry-run=server to catch duplicates.
Not using —dry-run in CI
Deploying without testing the kustomize output first means you find out about problems after they happen in production. Always run kubectl diff -k or kubectl kustomize | kubectl apply --dry-run=server in CI before applying.
Missing version pinning for kustomize
Kustomize versions differ in behavior. A kustomization that works with kustomize 3.8 may fail or produce different output with kustomize 5.0. Pin your kustomize version in CI and align it with your cluster’s kubectl version.
Observability Hooks
Track the health and correctness of your Kustomize deployments with these practices.
What to monitor:
- Kustomize build success/failure rate in CI
- Time taken for
kubectl kustomizeto complete across environments - Number of resources generated per overlay (sudden changes indicate unintended patches)
- YAML validation errors caught in CI vs at apply time
CI/CD observability:
# Example: Kustomize build step with error capture
- name: Build Kustomize manifests
id: kustomize-build
run: |
mkdir -p build
kubectl kustomize ./overlays/production > build/manifests.yaml
RESOURCE_COUNT=$(kubectl kustomize ./overlays/production | kubectl apply --dry-run=server -f - 2>&1 | grep -c "created\|configured")
echo "resources::$RESOURCE_COUNT"
# Detect significant changes
kubectl kustomize ./overlays/production | sha256sum > build/prod-checksum.txt
continue-on-error: false
# Track diff size
- name: Check manifest diff
run: |
kubectl kustomize ./overlays/production > build/new.yaml
DIFF_COUNT=$(diff -u build/base.yaml build/new.yaml | grep -c "^[+-]")
echo "Changed lines: $DIFF_COUNT"
if [ "$DIFF_COUNT" -gt 100 ]; then
echo "WARNING: Large diff detected, verify intentional changes"
fi
Debugging commands:
# View all generated resources
kubectl kustomize ./overlays/production
# Validate against cluster without applying
kubectl apply --dry-run=server -k ./overlays/production
# Check what will change (diff mode)
kubectl diff -k ./overlays/production
# Count resources generated
kubectl kustomize ./overlays/production | kubectl apply --dry-run=server -f - 2>&1 | grep -c "resource"
# Validate YAML syntax
kubectl kustomize ./overlays/production | kubectl validate --ignore-missing-schema
# Check for deprecated APIs
kubectl kustomize ./overlays/production | kubectl-convert --dry-run=client -o yaml | head -20
Alert on deployment anomalies:
# Alert if a deployment has significantly more/fewer resources than expected
- alert: KustomizeResourceCountAnomaly
expr: |
(count(kustomize_generated_resources{env="production"}) by (app)
/ avg(count(kustomize_generated_resources{env="production"}) by (app))) > 1.5
labels:
severity: warning
annotations:
summary: "Kustomize resource count anomaly for {{ $labels.app }}"
description: "Production overlay generating {{ $value }}x more resources than average. Verify intentional changes."
# Alert if kustomize builds fail repeatedly in CI
- alert: KustomizeBuildFailures
expr: increase(kustomize_build_errors_total[10m]) > 3
labels:
severity: critical
annotations:
summary: "Multiple kustomize build failures detected"
description: "{{ $value }} kustomize build failures in the last 10 minutes. Check CI logs."
Trade-off Summary
| Aspect | Kustomize | Helm | plain YAML |
|---|---|---|---|
| Templating | No (patches/overlays) | Yes (Go templates) | No |
| Readability | High (plain YAML) | Medium (templates) | Highest |
| Reusability | Git-based | Chart repos | None |
| Secret handling | Sealed secrets + generators | helm-secrets | Manual |
| GitOps integration | Native kubectl | Flux/ArgoCD | Flux/ArgoCD |
| Debugging | Diff is clear | Render first | Plain kubectl |
| Ecosystem | Growing | Massive (Bitnami) | N/A |
Interview Questions
Expected answer points:
- Kustomize uses overlays and patches—base YAML stays readable and diffable without rendering templates
- Helm uses Go templates with values files—produces rendered templates but adds a layer of indirection
- Kustomize keeps YAML human-readable at every stage; Helm requires mental template rendering to understand final output
- Kustomize is native to kubectl (1.14+); Helm is a separate CLI with its own ecosystem of charts
Expected answer points:
- Strategic merge patches resemble kubectl patch syntax and work well for common fields like replicas, images, and labels
- JSON 6902 patches (RFC 6902) use JSON Pointer notation for fine-grained control over any JSON path
- Strategic merge does not support array element targeting by index; JSON 6902 can add/replace/remove specific array elements
- JSON 6902 patches are more expressive but less readable for simple modifications
Expected answer points:
- Components (kustomize.config.k8s.io/v1alpha1) are reusable configuration blocks importable across multiple kustomizations
- They enable composition without publishing packages—unlike Helm charts, you reference local paths
- Use cases: shared monitoring sidecar, common secret rotation logic, standardized networking policies
- Components solve the reuse problem for platform teams building bases for other teams to consume
Expected answer points:
- Kustomize generates ConfigMaps with content hashes in the name by default—changing a literal creates a new ConfigMap and deletes the old
- Manual ConfigMaps have static names; Kustomize's generator produces deterministic but different names when content changes
- Use `behavior: merge` to update existing ConfigMaps instead of replacing them to avoid Deployment reference issues
- The hash-based naming ensures rolled Deployments pick up updated ConfigMaps automatically
Expected answer points:
- When two teams both use `namePrefix: dev-` in overlays and deploy to the same cluster, resources collide (dev-api from team A overwrites dev-api from team B)
- Prevention options: use team-specific prefixes (dev-teamA-, dev-teamB-), deploy to isolated namespaces, or use organizational naming conventions
- Name collisions are particularly dangerous in shared clusters with multiple teams
Expected answer points:
- `kubectl kustomize ./overlay` builds and prints the generated YAML without applying
- `kubectl diff -k ./overlay` compares the generated manifests against what's currently running in the cluster
- `kubectl apply --dry-run=server -k ./overlay` validates against the cluster schema without making changes
- Always run these in CI before applying to catch issues early
Expected answer points:
- Kustomize has no package distribution system—you share via Git, not chart repositories
- No built-in templating logic for complex conditional rendering (Helm's Go templates are more expressive)
- Secret management requires external tools (Sealed Secrets, external-secrets) whereas Helm has helm-secrets
- Ecosystem is smaller—Bitnami and other public Helm charts do not have Kustomize equivalents for every off-the-shelf software
Expected answer points:
- Choose Kustomize when your team owns application manifests directly and wants readable, diffable YAML
- Good fit for GitOps workflows where plain YAML in source control matters more than package distribution
- Choose Helm when you need to distribute reusable packages to users who should not see template logic
- Choose Helm when your configuration varies in complex ways that do not map cleanly to overlays and patches
Expected answer points:
- `commonLabels` adds labels to all resources created from a kustomization—every Deployment, Service, ConfigMap gets the same labels
- Use case: standardized labeling across environments (app name, version, environment, team)
- Works with `namePrefix` to create environment-specific resource names while maintaining label consistency
- Critical for tools that select resources by labels (monitoring, network policies, RBAC)
Expected answer points:
- Typical structure: `app/base/` with kustomization.yaml + resource YAMLs, then `app/overlays/development/`, `app/overlays/production/`
- Base contains canonical configuration; overlays reference base via `bases: [../../base]`
- Keeps nesting shallow (2-3 levels max) to keep final YAML understandable
- Components live at `app/components/
/` and are imported into overlays for reuse
Expected answer points:
- Use `kustomize edit create secret` to generate encrypted or reference-based secrets
- Store sealed secrets or reference values only — not plaintext secrets in Git
- Use External Secrets Operator to pull secrets from AWS Secrets Manager, HashiCorp Vault, or similar
- Sealed Secrets: encrypt Kubernetes secrets with a public key; only sealed-secrets controller can decrypt
- Use `.gitignore` to exclude `secrets.yaml` — only commit `secrets.yaml.s-example` or template
- CDK8s or other tools can generate secrets from external sources during build
Expected answer points:
- `bases` reference other kustomization directories — creates hierarchical composition (base → overlay)
- `components` (kustomize.config.k8s.io/v1alpha1) are reusable configuration blocks importable into any kustomization
- Components can be used in multiple overlays without hierarchical inheritance
- Use components for: monitoring sidecar, secret rotation logic, network policies shared across teams
- Use bases/overlays for: environment-specific differences (dev has 1 replica, prod has 5)
- Components solve the reuse problem where hierarchical inheritance doesn't fit cleanly
Expected answer points:
- `kubectl kustomize ./overlay` prints generated YAML without applying — first step for debugging
- `kubectl diff -k ./overlay` shows differences between current cluster state and overlay
- Use `--load-restrictor=LoadRestrictionsNone` if bases are outside the kustomization directory
- Check for duplicate resources: `kubectl kustomize | grep -c "kind:"` — high count may indicate duplicates
- Validate with `kubectl apply --dry-run=server -k ./overlay` — catches schema validation errors
- Use `kustomize build --enable-alpha-plugins` for debugging plugin-based generators
Expected answer points:
- Strategic merge patch (SMP) merges fields: overlay values override base values for matching fields
- If overlay's patch matches resource name/kind in base, fields merge; otherwise new resource created
- JSON 6902 patches (RFC 6902) use JSON Pointer to target specific array elements or nested paths
- If both patches target same field, the later patch in kustomization.yaml order wins
- For conflicting patches: use `patchesStrategicMerge` with explicit targeting by name and kind
- Debug: run `kubectl kustomize` and check if resulting YAML matches expectation
Expected answer points:
- `commonLabels` adds labels to ALL resources in the kustomization — Deployments, Services, ConfigMaps, etc.
- Use for: environment label, team label, version label applied consistently across all resources
- Works with `namePrefix` to create environment-specific names while keeping labels consistent
- `commonAnnotations` adds annotations to all resources — useful for tool metadata, ownership
- Labels are critical for selectors, RBAC, and network policies that match resources by label
- Without commonLabels, you would need to add labels manually to each resource in base
Expected answer points:
- `kubectl kustomize ./overlay` validates YAML syntax and generates final manifests
- `kubectl diff -k ./overlay` compares generated manifests against currently deployed state
- `kubectl apply --dry-run=server -k ./overlay` validates against cluster API schema
- Run in CI before any apply: fail pipeline if diff shows unexpected changes
- Use kubeval or conftest to validate generated YAML against custom policies
- Count resources: compare `kubectl kustomize | grep -c "kind:"` between overlays to detect unexpected changes
Expected answer points:
- `replicas` field in kustomization.yaml uses SMP to replace `spec.replicas` in matching Deployment
- Target by name: `replicas: - name: myapp\n count: 5` sets myapp Deployment replicas to 5
- If base has `replicas: 3` and overlay sets `replicas: 5`, overlay wins for matching name
- If overlay defines same Deployment resource directly (not via patch), causes duplicate resource error
- For multiple Deployments: can target each by name with different replica counts
- Verify with `kubectl kustomize ./overlay | grep -A2 "replicas"` to check final values
Expected answer points:
- Store feature flags in ConfigMap, patch different ConfigMap in each overlay
- Overlay creates env-specific ConfigMap with `configMapGenerator` and references it in patches
- Use `patchesJson6902` to replace specific values in ConfigMap without full replacement
- Inject via environment variable: `env` field in container spec pulls from ConfigMap
- Alternatively: use sealed secrets for feature flag values, overlay contains decryption key
- Keep feature flag logic in ConfigMap, not hardcoded in application template
Expected answer points:
- Use namespace-level isolation: each tenant gets own namespace with network policies
- Base kustomization includes namespace, resource quotas, RBAC for tenant
- Overlays per cluster/environment inherit from tenant base — not single global base
- Use `namePrefix` per tenant to avoid resource name collisions across tenants
- Platform team provides base with security controls; tenant teams customize workload overlay
- RBAC: ServiceAccount per tenant, bound to tenant namespace only — prevents cross-tenant access
Expected answer points:
- Both ArgoCD and Flux support both Kustomize and Helm — choice depends on team preference
- Kustomize: YAML is Git-native, diffs are readable, no template rendering step
- Helm: larger ecosystem of charts, more expressive templating for complex configurations
- ArgoCD with Kustomize: Application set points to Git repo, ArgoCD syncs manifest generated by `kubectl kustomize`
- ArgoCD with Helm: Application references chart repository or local chart, ArgoCD renders with `helm template`
- Flux with Kustomize: Source controller pulls Git, Kustomize controller generates manifests
- For teams owning their manifests: Kustomize's Git-native approach fits naturally
- For teams consuming off-the-shelf software: Helm's chart ecosystem reduces work
Further Reading
- GitOps - GitOps patterns and deployment strategies
- Helm Charts - Alternative templating approach to Kubernetes configuration
- Container Registry - Managing container images at scale
- Kubernetes - Core Kubernetes component internals
- Kustomize Official Documentation
- Kubernetes SIG Kustomize
Conclusion
Key Takeaways
- Kustomize uses overlays and patches instead of templates, keeping YAML readable and diffable
- The built-in
kubectl kustomizerequires no separate installation (Kubernetes 1.14+) - Name prefixes and common labels help standardize resources across environments
- Components provide reusable configuration blocks without publishing packages
- Choose Kustomize for app-centric GitOps; choose Helm for distributable packages
Kustomize Checklist
# Test kustomize output without applying
kubectl kustomize ./overlays/production
# Diff against cluster state
kubectl diff -k ./overlays/production
# Apply with dry-run to catch errors
kubectl apply -k ./overlays/production --dry-run=server
# Validate build in CI
kustomize build ./overlays/production | kubeval --strict Category
Related Posts
GitOps: Declarative Deployments with ArgoCD and Flux
Implement GitOps for declarative, auditable infrastructure and application deployments using ArgoCD or Flux as your deployment operator.
ConfigMaps and Secrets: Managing Application Configuration in Kubernetes
Inject configuration data and sensitive information into Kubernetes pods using ConfigMaps and Secrets. Learn about mounting strategies, environment variables, and security best practices.
GitOps: Infrastructure as Code with Git for Microservices
Discover GitOps principles and practices for managing microservices infrastructure using Git as the single source of truth.