Kustomize

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every earlier page of this section wrote plain YAML by hand. That works for one environment, but the moment the same Bookshelf stack has to run in a dev and a prod cluster — with different replica counts, image tags, hostnames and resource limits — copy-pasting manifests becomes a maintenance problem. Kustomize solves it without a template language: you keep a valid, plain-YAML base and describe each environment as an overlay that patches it. This page is written against Kubernetes 1.37 and Kustomize 5.8 (the version embedded in kubectl 1.37 may lag the standalone release, so check kubectl version — see the section index for the full version baseline). Its templating-based counterpart is Helm, and delivering either one continuously is the subject of GitOps and CI/CD. Kustomize operates on the same objects described in Objects and the API, and is applied with the commands from kubectl Essentials.

Two ways to run it

Kustomize exists in two forms, and mixing them up is a common source of "it works on my machine":

  • Built into kubectl. kubectl kustomize <dir> renders the result to standard output, and kubectl apply -k <dir> renders and applies it in one step. No extra install is needed, but the embedded Kustomize version is whatever that kubectl was built with, so the newest fields may be missing.

  • Standalone kustomize. kustomize build <dir> renders the same output using the latest release. It is what CI and GitOps tools typically invoke, so a manifest that builds in the standalone binary but not in kubectl -k is usually a version gap, not a bug.

# Render to stdout, nothing is sent to the cluster
kubectl kustomize k8s/overlays/dev

# Render and apply; -k means "the argument is a directory with a kustomization.yaml"
kubectl apply -k k8s/overlays/dev

# Standalone binary, same directory
kustomize build k8s/overlays/dev | kubectl apply -f -

# Compare live state with what would be applied
kubectl diff -k k8s/overlays/dev

Always run kubectl kustomize (or kubectl diff -k) before apply -k: the rendered output is the single source of truth for what will actually reach the API server.

The kustomization.yaml fields

A directory becomes a Kustomize unit when it contains a file named kustomization.yaml. The fields you will use most:

Field What it does

resources

Files, directories or remote bases to include. An overlay lists its base directory here.

namespace

Sets metadata.namespace on every namespaced resource that does not already pin another one.

namePrefix / nameSuffix

Prepends or appends a string to every resource name, and rewrites references to those names (for example a Deployment pointing at a renamed ConfigMap).

labels

Adds labels to every resource; with includeSelectors: true they are also added to selectors. Changing selectors on an existing Deployment is not allowed, so use it from the first deployment onwards or leave it false.

images

Rewrites name, newName, newTag or digest for a container image wherever it appears, without editing the manifests.

replicas

Overrides spec.replicas for a named workload.

configMapGenerator / secretGenerator

Builds a ConfigMap or Secret from literals, files or env files, and appends a content hash to its name.

patches

Strategic-merge or JSON 6902 modifications of selected resources.

components

Reusable, optional bundles of resources and patches included by an overlay.

Generators and hash suffixes

A generator creates a ConfigMap or Secret named, for example, bookshelf-config-7h2m9k4t5d. The suffix is a hash of the content, and Kustomize rewrites every reference to the generated name. The consequence is the important part: when the content changes, the name changes, so the Deployment’s Pod template changes, and Kubernetes performs a rolling update. This solves the classic problem covered in ConfigMaps — editing a ConfigMap does not restart the Pods that consume it. Set options.disableNameSuffixHash: true only when something outside the cluster must reference a fixed name.

A secretGenerator is a convenience for producing the object, not a way to protect it: literals and files you feed it still live in the repository in clear text. For real secret handling, see Secrets and the sealed-secret options in GitOps and CI/CD.

Strategic-merge versus JSON 6902 patches

patches accepts two styles:

  • A strategic-merge patch is a partial manifest that is merged into the target; list items such as containers are matched by their merge key (name). It reads like ordinary YAML and is the right choice for most edits.

  • A JSON 6902 patch is an explicit list of op / path / value operations (add, replace, remove). It is the tool for edits a merge cannot express, such as removing a field or addressing a list item by index.

Both are attached with a target selector (group, version, kind, name, label selector), so one patch file can be applied to several resources.

The Bookshelf layout

The recommended structure keeps one base and one thin overlay per environment. This is the layout used when the Bookshelf application is delivered in GitOps and CI/CD, with rollouts following Deployment Strategies:

k8s/
├── base/
│   ├── kustomization.yaml
│   ├── namespace.yaml
│   ├── catalog-api.yaml          # Deployment + Service
│   ├── reviews-api.yaml          # Deployment + Service
│   ├── web.yaml                  # Deployment + Service
│   └── postgres.yaml             # StatefulSet + headless Service
├── components/
│   └── metrics/
│       └── kustomization.yaml    # kind: Component
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── replicas-patch.yaml
    └── prod/
        ├── kustomization.yaml
        ├── resources-patch.yaml
        └── hpa.yaml

The base lists its files and common settings:

# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bookshelf
resources:
  - namespace.yaml
  - catalog-api.yaml
  - reviews-api.yaml
  - web.yaml
  - postgres.yaml
labels:
  - pairs:
      app.kubernetes.io/part-of: bookshelf
configMapGenerator:
  - name: bookshelf-config
    literals:
      - LOG_LEVEL=info

The dev overlay stays close to the base and lowers the footprint:

# k8s/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
images:
  - name: ghcr.io/example/bookshelf-catalog-api
    newTag: 1.1.0-dev
replicas:
  - name: catalog-api
    count: 1
patches:
  - path: replicas-patch.yaml

The prod overlay adds an HPA, a production image tag, resource limits and an optional component:

# k8s/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
  - hpa.yaml
components:
  - ../../components/metrics
images:
  - name: ghcr.io/example/bookshelf-catalog-api
    newTag: 1.0.0
replicas:
  - name: catalog-api
    count: 3
configMapGenerator:
  - name: bookshelf-config
    behavior: merge
    literals:
      - LOG_LEVEL=warn
patches:
  - path: resources-patch.yaml
  - target:
      kind: Deployment
      name: web
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/imagePullPolicy
        value: IfNotPresent

The strategic-merge patch referenced above only names what it changes:

# k8s/overlays/prod/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
spec:
  template:
    spec:
      containers:
        - name: catalog-api
          resources:
            requests:
              cpu: 250m
              memory: 384Mi
            limits:
              memory: 512Mi

Preview and apply each environment:

kubectl kustomize k8s/overlays/prod | head -40
kubectl apply -k k8s/overlays/dev
kubectl get deploy -n bookshelf
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
catalog-api   1/1     1            1           12s
reviews-api   1/1     1            1           12s
web           1/1     1            1           12s

A Component (kind: Component, apiVersion: kustomize.config.k8s.io/v1alpha1) packages an optional feature — here, scrape annotations and a ServiceMonitor — so an overlay opts in with one line instead of copying patches between overlays. Resource sizing itself is explained in Resource Management, and the hpa.yaml in Autoscaling.

Kustomize or Helm?

Neither replaces the other; they answer different needs, and many teams use both (Helm for third-party software, Kustomize for their own applications, or Kustomize layered over a rendered chart).

Question Kustomize Helm

Approach

Patch plain YAML; no template language.

Render Go templates from values into YAML.

Reuse by third parties

Possible through remote bases, but there is no packaging or versioning story.

Charts are versioned, published to repositories and OCI registries, and discoverable on Artifact Hub.

Per-environment differences

Overlays; ideal when environments differ by a few patches.

A values file per environment; ideal when the differences are many and structured.

Release tracking and rollback

None of its own; relies on Git history and the cluster’s Deployment revisions.

Releases with numbered revisions, helm rollback, hooks and tests.

Learning curve and debugging

Low; the rendered output is the input plus patches.

Higher; template logic can hide what will be generated (use helm template to see it).

Built into kubectl

Yes (apply -k).

No; separate binary.

A practical rule: if you own the manifests and need two to five environments, start with Kustomize; if you distribute the application to other teams or need parameterised installs, package it as a chart.