Overview

Kustomize is a Kubernetes-native configuration management tool built into kubectl. It works by layering patches and overlays on top of a base of plain Kubernetes YAML — no templating language, no value files, just plain manifests and transformations.

base/                        ← plain Kubernetes manifests, no env-specific values
  kustomization.yaml
  deployment.yaml
  service.yaml
  configmap.yaml

overlays/
  staging/
    kustomization.yaml       ← patches for staging
    replica-patch.yaml
  production/
    kustomization.yaml       ← patches for production
    replica-patch.yaml
    hpa-patch.yaml
    ingress.yaml             ← production-only resource

Kustomize transforms are deterministic and diffable — git diff always shows exactly what changed, unlike Helm's Go template rendering.

Base Structure

base/kustomization.yaml

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

resources:
- deployment.yaml
- service.yaml
- serviceaccount.yaml
- networkpolicy.yaml

# Common labels added to ALL resources
commonLabels:
  app.kubernetes.io/name: payments-api
  app.kubernetes.io/managed-by: kustomize

# Common annotations added to ALL resources
commonAnnotations:
  team: platform-payments

# Image transformer — update image tags without editing deployment.yaml
images:
- name: payments-api
  newName: ghcr.io/acme/payments-api
  newTag: latest             # overridden per overlay

base/deployment.yaml — Plain YAML, No Templating

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api         # no {{ }} — just plain YAML
spec:
  replicas: 1                # base default, overridden by overlay
  selector:
    matchLabels:
      app: payments-api
  template:
    metadata:
      labels:
        app: payments-api
    spec:
      containers:
      - name: payments-api
        image: payments-api:latest     # replaced by images transformer
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080

Overlay Structure

overlays/staging/kustomization.yaml

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

resources:
- ../../base              # inherit everything from base

namePrefix: staging-      # prepend "staging-" to all resource names (optional)
namespace: staging        # set namespace on all resources

# Override image tag for staging
images:
- name: payments-api
  newName: ghcr.io/acme/payments-api
  newTag: sha-abc123      # set by CI after build

# Patches
patches:
- path: replica-patch.yaml
- path: configmap-patch.yaml

# ConfigMap generated from literal values
configMapGenerator:
- name: payments-config
  literals:
  - LOG_LEVEL=debug
  - DATABASE_HOST=postgres-staging.internal
  - ENVIRONMENT=staging
  options:
    disableNameSuffixHash: true

overlays/staging/replica-patch.yaml

# Strategic merge patch — only the fields listed are changed
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
spec:
  replicas: 1              # staging: 1 replica

overlays/production/kustomization.yaml

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

resources:
- ../../base
- ingress.yaml             # production-only resource
- hpa.yaml
- pdb.yaml

namespace: production

images:
- name: payments-api
  newName: ghcr.io/acme/payments-api
  newTag: sha-def456

patches:
- path: replica-patch.yaml
- path: resources-patch.yaml
- path: affinity-patch.yaml

configMapGenerator:
- name: payments-config
  literals:
  - LOG_LEVEL=info
  - DATABASE_HOST=postgres-prod.internal
  - ENVIRONMENT=production
  options:
    disableNameSuffixHash: true

overlays/production/replica-patch.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
spec:
  replicas: 3              # production: 3 replicas

Patch Types

Strategic Merge Patch (most common)

Merges at the Kubernetes API level — lists are merged by key, not replaced. Good for most cases.

# patches/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
spec:
  template:
    spec:
      containers:
      - name: payments-api      # must match container name to merge
        resources:
          requests:
            cpu: 500m
            memory: 512Mi
          limits:
            cpu: 2000m
            memory: 1Gi

JSON 6902 Patch (surgical precision)

Operates on specific JSON path. Required when you need to add to a list, remove a key, or do operations not supported by strategic merge.

# kustomization.yaml
patches:
- target:
    kind: Deployment
    name: payments-api
  patch: |-
    - op: replace
      path: /spec/template/spec/containers/0/image
      value: ghcr.io/acme/payments-api:sha-newsha

    - op: add
      path: /spec/template/spec/containers/0/env/-
      value:
        name: NEW_ENV_VAR
        value: "new-value"

    - op: remove
      path: /spec/template/spec/containers/0/env/2

Patch from File with Target Selector

# kustomization.yaml — apply one patch to multiple resources
patches:
- path: add-prometheus-annotations.yaml
  target:
    kind: Deployment        # applies to ALL Deployments in this overlay
# add-prometheus-annotations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: irrelevant         # ignored when target selector is used
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    prometheus.io/path: "/metrics"

Generators

ConfigMap Generator

# kustomization.yaml
configMapGenerator:
- name: app-config
  files:
  - config/app.properties      # file content becomes a key
  - nginx.conf=config/nginx.conf   # custom key name
  literals:
  - LOG_LEVEL=info
  envs:
  - .env.staging               # key=value file
  options:
    disableNameSuffixHash: true   # prevent Kustomize from appending hash suffix

Secret Generator

# kustomization.yaml
secretGenerator:
- name: db-credentials
  literals:
  - username=payments-user
  - password=changeme        # use ESO in production — this is for local dev only
  type: Opaque
  options:
    disableNameSuffixHash: true

Components — Reusable Kustomize Modules

Components are reusable Kustomize configurations that can be included in multiple overlays. Useful for cross-cutting concerns like enabling Istio sidecar injection, adding monitoring annotations, or enabling debug mode.

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

patches:
- patch: |-
    - op: add
      path: /spec/template/metadata/labels/sidecar.istio.io~1inject
      value: "true"
  target:
    kind: Deployment
# components/prometheus-scrape/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component

patches:
- patch: |-
    - op: add
      path: /metadata/annotations/prometheus.io~1scrape
      value: "true"
    - op: add
      path: /metadata/annotations/prometheus.io~1port
      value: "8080"
  target:
    kind: Service
# overlays/production/kustomization.yaml — include components
components:
- ../../components/istio-injection
- ../../components/prometheus-scrape

Image Tag Updates in CI

# Update image tag in a specific overlay using kustomize edit
cd overlays/staging
kustomize edit set image payments-api=ghcr.io/acme/payments-api:sha-${GIT_SHA}

# Verify the change
kustomize build overlays/staging | grep "image:"

# Commit and push (triggers Argo CD sync)
git add kustomization.yaml
git commit -m "chore(payments-api): bump staging to sha-${GIT_SHA}"
git push

GitHub Actions: Update and PR

- name: Update kustomize image tag
  run: |
    cd k8s-config/services/payments-api/staging
    kustomize edit set image \
      payments-api=ghcr.io/acme/payments-api:sha-${{ github.sha }}

- name: Create Pull Request
  uses: peter-evans/create-pull-request@v6
  with:
    commit-message: "chore(staging): bump payments-api to sha-${{ github.sha }}"
    branch: bump/payments-api-staging-${{ github.sha }}

Kustomize Build and Diff

# Render manifests for an overlay (dry-run, no apply)
kustomize build overlays/production

# Apply directly
kustomize build overlays/production | kubectl apply -f -

# kubectl kustomize (built-in, same as kustomize build)
kubectl kustomize overlays/production

# Apply via kubectl
kubectl apply -k overlays/production

# Diff against live cluster
kustomize build overlays/production | kubectl diff -f -

Kustomize vs Helm — When to Use Each

CriterionKustomizeHelm
Config formatPlain YAML + patchesGo templates + values.yaml
VersioningGit history is the versionHelm release history + chart semver
Sharing/packagingLimited (no OCI distribution)First-class (OCI, Helm repos)
Rollbackgit reverthelm rollback
ConditionalsJSON patch or separate overlay{{- if .Values.foo }}
Learning curveLow (know YAML, know Kustomize)Higher (Go template syntax)
Third-party chartsPoor (no dependencies)Excellent (bitnami, prometheus-community)
Best forIn-house apps, simple env overlaysThird-party software, complex parameterisation

Common pattern: use Helm for third-party infrastructure (cert-manager, Prometheus, NGINX ingress) and Kustomize for in-house application deployments.

Argo CD with Kustomize

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments-api
  namespace: argocd
spec:
  source:
    repoURL: https://github.com/acme/k8s-config
    targetRevision: main
    path: services/payments-api/overlays/production
    kustomize:
      images:
      - ghcr.io/acme/payments-api:sha-abc123
      # Optional: pass additional patches at Argo CD level
      patches:
      - target:
          kind: Deployment
          name: payments-api
        patch: |-
          - op: replace
            path: /spec/replicas
            value: 5
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true