Skip to content
Codeloom
Kubernetes

Kubernetes Kustomize: Manage Configs Without Helm

Learn Kustomize for Kubernetes config management. Covers overlays, patches, generators, and multi-environment deployments without templating.

·7 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • Structure a Kustomize project with bases and overlays
  • Use patches to modify resources per environment
  • Generate ConfigMaps and Secrets from files
  • Deploy multi-environment apps without Helm or templates

Prerequisites

  • Basic Kubernetes knowledge — see /blog/kubernetes-pods-deployments-services
  • Familiarity with YAML and kubectl

Kustomize is a configuration management tool built directly into kubectl. It lets you customize Kubernetes manifests without templates, variable substitution, or a separate tool installation. Instead of parameterizing YAML files with placeholders, you declare modifications as overlays on top of a base configuration.

The result is plain Kubernetes YAML that you can read, validate, and version control. No curly braces, no Go templates, no values files.

Why Kustomize Over Helm

Helm is a package manager with templating. Kustomize is a configuration customizer without templating. They solve overlapping but different problems:

  • Helm excels when you distribute reusable charts that others consume. It supports complex logic, conditionals, and loops.
  • Kustomize excels when you manage your own deployments across environments. It keeps manifests readable and avoids template complexity.

You can use both together — Kustomize can post-process Helm output. But for many teams managing their own services, Kustomize alone is sufficient and simpler.

Project Structure

A typical Kustomize project has a base and one overlay per environment:

app/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── configmap.yaml
├── overlays/
│   ├── dev/
│   │   ├── kustomization.yaml
│   │   └── replica-patch.yaml
│   ├── staging/
│   │   ├── kustomization.yaml
│   │   └── replica-patch.yaml
│   └── production/
│       ├── kustomization.yaml
│       ├── replica-patch.yaml
│       └── hpa.yaml

The base contains the core manifests shared across all environments. Overlays add or modify resources for specific environments.

Building the Base

Start with standard Kubernetes manifests:

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  labels:
    app: web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: myregistry/web-app:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          envFrom:
            - configMapRef:
                name: web-app-config
# base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-app
spec:
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
# base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: web-app-config
data:
  LOG_LEVEL: "info"
  DB_HOST: "localhost"
  DB_PORT: "5432"

The base kustomization.yaml lists all resources:

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

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

commonLabels:
  managed-by: kustomize

Preview the output:

# Run this in your terminal:
# kubectl kustomize base/

Creating Overlays

Dev Overlay

# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

namePrefix: dev-
namespace: dev

patches:
  - path: replica-patch.yaml

configMapGenerator:
  - name: web-app-config
    behavior: merge
    literals:
      - LOG_LEVEL=debug
      - DB_HOST=dev-db.internal
# overlays/dev/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: web-app
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi

Production Overlay

# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base
  - hpa.yaml

namePrefix: prod-
namespace: production

patches:
  - path: replica-patch.yaml

images:
  - name: myregistry/web-app
    newTag: v2.1.0

configMapGenerator:
  - name: web-app-config
    behavior: merge
    literals:
      - LOG_LEVEL=warn
      - DB_HOST=prod-db.internal
      - DB_PORT=5432
# overlays/production/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: web-app
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi
# overlays/production/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: prod-web-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Deploy to each environment:

# Preview what will be applied:
# kubectl kustomize overlays/dev/
# kubectl kustomize overlays/production/

# Apply:
# kubectl apply -k overlays/dev/
# kubectl apply -k overlays/production/

Patch Strategies

Kustomize supports two patch formats.

Strategic Merge Patches

These merge with the existing resource. You only specify the fields you want to change:

# Add an annotation to the deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  annotations:
    monitoring.example.com/enabled: "true"

JSON Patches

For precise modifications like removing fields or changing array elements:

# overlays/production/kustomization.yaml
patches:
  - target:
      kind: Deployment
      name: web-app
    patch: |
      - op: add
        path: /spec/template/spec/containers/0/livenessProbe
        value:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 30
      - op: add
        path: /spec/template/spec/containers/0/readinessProbe
        value:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

Inline Patches

You can write patches inline in kustomization.yaml:

patches:
  - target:
      kind: Service
      name: web-app
    patch: |
      apiVersion: v1
      kind: Service
      metadata:
        name: web-app
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-type: nlb
      spec:
        type: LoadBalancer

ConfigMap and Secret Generators

Generators create ConfigMaps and Secrets with content-based hash suffixes. When the content changes, the name changes, triggering a rolling update:

# From literal values
configMapGenerator:
  - name: web-app-config
    literals:
      - LOG_LEVEL=info
      - APP_MODE=production

# From files
configMapGenerator:
  - name: nginx-config
    files:
      - nginx.conf
      - configs/upstream.conf

# From env file
configMapGenerator:
  - name: env-config
    envs:
      - .env.production

Secret generators work the same way:

secretGenerator:
  - name: db-credentials
    literals:
      - DB_USER=admin
      - DB_PASSWORD=supersecret
    type: Opaque

  - name: tls-cert
    files:
      - tls.crt
      - tls.key
    type: kubernetes.io/tls

The hash suffix (e.g., web-app-config-8m2dk4) means old ConfigMaps are not deleted immediately. Clean up stale ones with kubectl delete configmap -l managed-by=kustomize or let garbage collection handle them.

Transformers

commonLabels and commonAnnotations

commonLabels:
  team: platform
  environment: production

commonAnnotations:
  contact: platform-team@example.com

These are applied to all resources and their selectors.

namePrefix and nameSuffix

namePrefix: prod-
nameSuffix: -v2

The deployment named web-app becomes prod-web-app-v2. Kustomize automatically updates all references (service selectors, HPA targets, etc.).

Image Overrides

images:
  - name: myregistry/web-app
    newName: production-registry.example.com/web-app
    newTag: v2.1.0
  - name: nginx
    newTag: "1.25"
  - name: redis
    newName: custom-redis
    digest: sha256:abc123def456

This is cleaner than patching the deployment just to change an image tag.

Components for Shared Features

Components let you define reusable pieces of configuration that can be included in multiple overlays:

# components/monitoring/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component

patches:
  - target:
      kind: Deployment
    patch: |
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: not-used
        annotations:
          prometheus.io/scrape: "true"
          prometheus.io/port: "9090"
      spec:
        template:
          spec:
            containers:
              - name: metrics-sidecar
                image: prom/pushgateway:v1.6.0
                ports:
                  - containerPort: 9090

Include the component in overlays:

# overlays/production/kustomization.yaml
components:
  - ../../components/monitoring

Multi-Service Applications

For microservices, organize each service as its own base:

platform/
├── services/
│   ├── api/
│   │   ├── base/
│   │   │   ├── kustomization.yaml
│   │   │   ├── deployment.yaml
│   │   │   └── service.yaml
│   │   └── overlays/
│   │       ├── dev/
│   │       └── production/
│   ├── worker/
│   │   ├── base/
│   │   └── overlays/
│   └── frontend/
│       ├── base/
│       └── overlays/
└── environments/
    ├── dev/
    │   └── kustomization.yaml
    └── production/
        └── kustomization.yaml

The environment kustomization combines all services:

# environments/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../services/api/overlays/production
  - ../../services/worker/overlays/production
  - ../../services/frontend/overlays/production

namespace: production

CI/CD Integration

Build manifests in CI and apply them:

# .github/workflows/deploy.yml (simplified)
# steps:
#   - name: Build manifests
#     run: kubectl kustomize overlays/$ENVIRONMENT > manifests.yaml
#
#   - name: Validate
#     run: kubectl apply --dry-run=server -f manifests.yaml
#
#   - name: Deploy
#     run: kubectl apply -f manifests.yaml

You can also use Kustomize with GitOps tools like ArgoCD and Flux, which natively understand kustomization.yaml files.

Wrapping Up

Kustomize provides a template-free approach to Kubernetes configuration management that keeps your manifests readable and maintainable:

  • Bases hold shared configuration; overlays customize per environment.
  • Patches modify resources with strategic merge or JSON patch operations.
  • Generators create ConfigMaps and Secrets with automatic hash suffixes for safe rollouts.
  • Transformers apply labels, prefixes, and image overrides across all resources.
  • Components package reusable features for inclusion in multiple overlays.

Since Kustomize is built into kubectl (kubectl apply -k), there is nothing extra to install. Start by organizing your existing manifests into a base directory, create overlays for your environments, and gradually adopt generators and patches as your needs grow.