Use it without installing anything

Kustomize is built into kubectl since 1.14:

kubectl version --client
# v1.32.x — comes with kustomize

# Apply a kustomization
kubectl apply -k ./overlays/prod/

# Just see the rendered YAML without applying
kubectl kustomize ./overlays/prod/

The standalone kustomize binary tracks newer features faster than the kubectl-embedded version. Install if you need the latest:

curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
kustomize version

The base directory

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

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

# Optional: common labels applied to every resource
commonLabels:
  app.kubernetes.io/name: myapp
  app.kubernetes.io/managed-by: kustomize

resources: lists the raw YAML files. kubectl kustomize ./base outputs all of them concatenated, with the common labels applied to each.

An overlay

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

namespace: prod

resources:
  - ../../base

namePrefix: prod-
commonAnnotations:
  environment: production
  team: platform

# Inline patches (strategic merge)
patches:
  - target:
      kind: Deployment
      name: myapp
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 5
      - op: add
        path: /spec/template/spec/containers/0/resources
        value:
          limits: { memory: 1Gi, cpu: 500m }
          requests: { memory: 512Mi, cpu: 250m }

# Or file-based patches
# patches:
#   - path: replicas-patch.yaml
#     target: { kind: Deployment, name: myapp }

# Image overrides
images:
  - name: myorg/myapp
    newTag: v1.2.3
    # Or newName: ghcr.io/myorg/myapp-prod

# Generate a ConfigMap from a literal value or file
configMapGenerator:
  - name: app-config
    behavior: merge          # merge with the one from base; or "create" or "replace"
    literals:
      - LOG_LEVEL=info
      - SENTRY_ENV=prod
    files:
      - config/feature-flags.json

secretGenerator:
  - name: api-keys
    files:
      - secrets/stripe.txt

kubectl apply -k ./overlays/prod/ renders + applies.

The directory layout that works

k8s/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── configmap.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── replicas-1.yaml
    ├── staging/
    │   ├── kustomization.yaml
    │   └── replicas-2.yaml
    └── prod/
        ├── kustomization.yaml
        ├── replicas-5.yaml
        └── resources-limits.yaml

Common diffs across overlays usually boil down to: namespace, replicas, image tag, resource limits, env var values, ingress hostname. All cleanly express-able as patches without the base seeing anything environment-specific.

ConfigMap / Secret generators

Kustomize's hashed ConfigMap / Secret generation is the standout feature: when you change a value, the generated name gets a new content-hash suffix. Deployments referencing the ConfigMap by name see a "new" ConfigMap on apply, triggering a rolling update. No manual restart needed when configuration changes.

# In a Deployment that uses the configmap
spec:
  template:
    spec:
      containers:
        - name: app
          envFrom:
            - configMapRef:
                name: app-config          # kustomize rewrites this to app-config-abc1234

# Disable hashing (if you want a stable name; loses the auto-rollout benefit)
configMapGenerator:
  - name: app-config
    literals: ["LOG_LEVEL=info"]
    options:
      disableNameSuffixHash: true

Strategic-merge vs JSON-6902 patches

  • Strategic merge (default for file-based patches) — structural patches; "merge this fragment into the matching resource." Knows about Kubernetes' merge semantics (a list of containers merges by name).
  • JSON patch (RFC 6902, the op: replace style above) — explicit path operations. More precise; required when you need to replace an entire array or modify by index.

Cross-cutting transformations

# In kustomization.yaml

# Add a label to every resource
labels:
  - includeSelectors: true
    pairs:
      tier: backend

# Add annotations
commonAnnotations:
  deployment.kubernetes.io/revision-history: "5"

# Set a namespace for every resource
namespace: my-namespace

# Add a name prefix / suffix
namePrefix: dev-
nameSuffix: -v2

# Replace all references to one image with another (across containers, init-containers, etc.)
images:
  - name: nginx
    newName: ghcr.io/myorg/nginx
    newTag: alpine

Components: reusable cross-cutting features

Beyond base + overlay, Kustomize components are reusable bundles of patches that can be mixed in. Example: a "with-redis-sidecar" component that any overlay can opt-in to:

# overlays/prod/kustomization.yaml
resources:
  - ../../base
components:
  - ../../components/with-redis-sidecar
  - ../../components/with-prometheus-monitor

Components are great for orthogonal features that aren't environment-specific (debug instrumentation, sidecars, monitor resources).

Remote bases

resources:
  - github.com/myorg/k8s-charts/base?ref=v1.2.3

Cache + version-pinned reference to a remote base. Useful for shared chart-of-charts patterns across teams.

Validate before applying

# Preview the rendered YAML
kubectl kustomize ./overlays/prod/

# Dry-run + diff against the live cluster
kubectl apply -k ./overlays/prod/ --dry-run=server -o yaml

# Diff against current
kubectl diff -k ./overlays/prod/

Kustomize vs Helm

  • Helm — templates with {{ }}; chart-shaped distribution; broad community-chart ecosystem. The right answer when you're consuming third-party software (cert-manager, ingress-nginx, prometheus-stack) or distributing your app to others.
  • Kustomize — pure YAML + patches; no templating; built into kubectl. The right answer for your own apps with a small number of environments to manage.

The most common production pattern: Helm for installing third-party software, Kustomize for managing your own apps' per-env diffs. Argo CD (see that tutorial) handles both formats natively from the same Git repo.

Kustomize + Helm together

Kustomize can render Helm charts and then patch the result:

# kustomization.yaml
helmCharts:
  - name: redis
    repo: oci://registry-1.docker.io/bitnamicharts
    version: 21.0.0
    releaseName: cache
    namespace: myapp
    valuesInline:
      auth:
        password: changeme

patches:
  - target:
      kind: StatefulSet
      name: cache-redis-master
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/resources/limits/memory
        value: 2Gi

Render the chart, apply patches on top, output as a single stream. Get Helm's chart ecosystem + Kustomize's overlay flexibility.

Worth knowing

  • The commonLabels field is now deprecated in favor of labels: with includeSelectors. commonLabels rewrote selector labels too (changing them broke immutable selectors); the new labels: field is safer.
  • The order of resources: matters for ordering of generated output but not for cluster apply (kubectl resolves dependencies).
  • For very-large kustomizations, the standalone kustomize binary is faster than the kubectl-embedded one.