Skip to content

Kustomize

Kustomize customizes Kubernetes manifests without templates. Instead of sprinkling {{ .Values.replicas }} through your YAML, you keep complete, valid base manifests and declare patches that transform them per environment. It is built into kubectl (kubectl apply -k), which also makes it CKA exam material.

The philosophy difference from Helm matters more than the syntax: a Helm chart is a program that generates YAML; a Kustomize overlay is a diff against YAML that already exists. Diffs are easier to review and reason about; programs are more powerful for packaging and distribution. Most platforms end up using both.

The base/overlay model

app/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml        # complete, valid manifests
│   └── service.yaml
└── overlays/
    ├── staging/
    │   ├── kustomization.yaml # references ../../base + patches
    │   └── replica-patch.yaml
    └── production/
        ├── kustomization.yaml
        ├── replica-patch.yaml
        └── resources-patch.yaml
flowchart LR
    BASE[base/\ncomplete manifests] --> S[overlay: staging\n+ 1 replica, debug logging]
    BASE --> P[overlay: production\n+ 10 replicas, resources,\nprod image tag]
    S --> OUT1[rendered staging YAML]
    P --> OUT2[rendered production YAML]

The base is deployable on its own. Overlays never copy it -- they reference and transform it, so a fix to the base propagates to every environment on the next apply.

Base kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
labels:
  - pairs:
      app.kubernetes.io/name: web
    includeSelectors: true

Production overlay

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: production
namePrefix: prod-
images:
  - name: ghcr.io/example/web
    newTag: v2.4.1            # pin the image per environment, no template needed
replicas:
  - name: web
    count: 10
patches:
  - path: resources-patch.yaml

Built-in transformers (namespace, namePrefix, images, replicas, labels) cover the majority of per-environment changes without writing a patch file at all.

Patches: two flavors

Strategic merge patch -- a partial manifest; anything you include is merged over the base:

# resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  template:
    spec:
      containers:
        - name: web            # matched by name, not position
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              memory: 1Gi

JSON6902 patch -- surgical operations at exact paths, for cases merge semantics can't express (removing a field, editing a list element by index):

patches:
  - target:
      kind: Deployment
      name: web
    patch: |-
      - op: remove
        path: /spec/template/spec/containers/0/livenessProbe
      - op: replace
        path: /spec/strategy/rollingUpdate/maxUnavailable
        value: 0

Rule of thumb: strategic merge for "set these fields," JSON6902 for "remove this" or list-index surgery.

Generators: the killer feature

configMapGenerator and secretGenerator create ConfigMaps/Secrets with a content-hash suffix, and rewrite every reference to match:

configMapGenerator:
  - name: app-config
    files:
      - app.properties
    literals:
      - LOG_LEVEL=info

This produces app-config-7b2f8c9k4d, and the Deployment that mounts app-config is automatically updated to reference the hashed name. Change app.properties, and the hash changes -- which changes the pod template -- which triggers a normal rolling update.

This is the clean solution to the problem described in ConfigMaps and Secrets: editing a ConfigMap in place never restarts pods, and subPath mounts never update at all. Generated, hash-named config makes every config change a versioned, rollback-able rollout instead of a silent in-place mutation. If you adopt one Kustomize feature, adopt this one.

(Use generatorOptions: {disableNameSuffixHash: true} only when something outside Kustomize references the ConfigMap by fixed name.)

Using it

Kustomize is built into kubectl:

kubectl kustomize overlays/production          # render, review
kubectl apply -k overlays/production           # render + apply
kubectl diff -k overlays/production            # what would change
kubectl delete -k overlays/production

The standalone kustomize binary tracks newer features (and is what Argo CD and Flux embed); kubectl's built-in version lags it slightly. For CI, render with kustomize build and pipe through policy checks before applying.

Composing with Helm and GitOps

  • Argo CD and Flux speak Kustomize natively -- point an Application at an overlay directory and you have per-environment GitOps with zero templating.
  • The common hybrid: Helm for third-party software (you consume someone's chart), Kustomize for your own apps (you own the YAML). Kustomize can even post-process rendered charts via helmCharts: when you need to patch a chart the maintainer won't parameterize.
  • Because overlays are plain YAML in Git, git diff on a PR shows exactly what changes in production -- the review experience is Kustomize's quiet superpower.

Kustomize vs Helm

Kustomize Helm
Model patch existing YAML render templates
Learning curve low medium (Go templates, Sprig)
Environment variants overlays values files
Packaging/distribution none (it's just Git) charts, registries, versioning
Rollback tooling via Git / GitOps helm rollback release history
Config-change rollouts hash-suffixed generators checksum annotations (manual pattern)
Built into kubectl yes (-k) no

Choose Kustomize when you own the manifests and want reviewable, template-free environment variants. Choose Helm when you're distributing software to others or consuming third-party packages. They compose rather than compete.

Common mistakes

  • Copying the base into each overlay -- the entire point is single-source transformation; copies rot immediately.
  • Deep overlay chains (overlay-on-overlay-on-overlay): after two levels, nobody can predict the output. Keep it to base + one overlay per environment and verify with kubectl kustomize.
  • Patching what a transformer already handles -- use images: and replicas: fields instead of hand-written patches for those.
  • Disabling the hash suffix out of habit -- you're throwing away automatic config rollouts.

Certification notes

  • The current CKA curriculum explicitly includes Kustomize (alongside Helm) under cluster and workload configuration. Know kubectl apply -k, the kustomization.yaml structure, and how overlays reference bases.
  • kubectl kustomize <dir> (render without applying) is the fast way to verify your answer in the exam.