Kustomize Patches: Fine-Grained Control for Kubernetes Overlays

0


Kustomize already gives us many convenient ways to customize Kubernetes manifests without copying and modifying the original YAML files. We can add name prefixes and suffixes, assign namespaces, apply labels and annotations, update image tags, change replica counts, and generate ConfigMaps and Secrets.

Those features are great for broad, cross-cutting changes. But they are not always precise enough.

At some point, we need to change one specific object in the base, or even one specific field inside one object. For example, we might want to update the image of the main nginx Deployment, while leaving a second nginx-based reverse proxy Deployment untouched. This is where Kustomize patches become important.

Patches allow us to apply targeted changes on top of a base while keeping the base reusable and clean.

What problem do patches solve?

Imagine a base with two Deployments. Both use the nginx image:

base/
├── kustomization.yaml
├── nginx-deployment.yaml
└── reverse-proxy-deployment.yaml

The first Deployment is our main application:

# base/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"

The second Deployment is a reverse proxy:

# base/reverse-proxy-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reverse-proxy
  labels:
    app: reverse-proxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reverse-proxy
  template:
    metadata:
      labels:
        app: reverse-proxy
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"

The base includes both resources:

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

resources:
  - nginx-deployment.yaml
  - reverse-proxy-deployment.yaml

Now we create a development overlay:

overlays/
└── dev/
    └── kustomization.yaml

A first attempt might be to use the top-level images field:

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

resources:
  - ../../base

images:
  - name: nginx
    newTag: 1.27.1

This works, but it works too broadly. Kustomize will find every image named nginx and change the tag to 1.27.1. In our example, both Deployments are updated:

image: nginx:1.27.1

That is not what we want if only the main nginx Deployment should be changed. The images transformer is excellent when we intentionally want to update all matching image references. It is not the right tool when we need to update only one object.

For that, we use patches.

The modern patches field

In older Kustomize examples, you may still see these fields:

patchesStrategicMerge:
  - patch.yaml

patchesJson6902:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx
    path: patch.yaml

For modern Kustomize usage, prefer the unified top-level patches field instead:

patches:
  - path: patch.yaml

The patches field can handle both Strategic Merge patches and JSON 6902 patches. It can load patches from files, or it can define them inline in the kustomization.yaml file.

This gives us one consistent mechanism for targeted changes.

Strategic Merge patches

A Strategic Merge patch looks like a partial Kubernetes manifest. Instead of repeating the entire Deployment, we only write the fields we want to change.

Here is an inline patch that changes the image only for the Deployment named nginx:

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

resources:
  - ../../base

patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nginx
      spec:
        template:
          spec:
            containers:
              - name: nginx
                image: nginx:1.27.1

The important parts are:

FieldWhy it matters
apiVersionTells Kustomize which API schema the patch is based on.
kindTells Kustomize which resource kind to patch.
metadata.nameIdentifies the specific object to patch.
spec.template.spec.containersContains only the container fields we want to modify.

After running the overlay, the main Deployment gets the new image:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.1

The reverse proxy Deployment still uses the original image:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reverse-proxy
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0

That is the main advantage of a patch: it targets the resource we actually want to change.

How Strategic Merge handles containers

The containers field is a list. At first, it might look like the patch should replace the whole list, but Strategic Merge is smarter for built-in Kubernetes types.

For Pod containers, Kubernetes uses name as the merge key. That means Kustomize looks for an existing container with the same name.

If the container exists, the patch updates only the fields we provided:

containers:
  - name: nginx
    image: nginx:1.27.1

If the container does not exist, the patch appends a new container to the list.

For example, this patch updates the existing nginx container and adds a new busybox container:

patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nginx
      spec:
        template:
          spec:
            containers:
              - name: nginx
                image: nginx:1.27.1
              - name: busybox
                image: busybox:1.36.1
                command:
                  - sh
                  - -c
                  - "sleep 3600"

The resulting Pod template contains both containers:

containers:
  - name: nginx
    image: nginx:1.27.1
  - name: busybox
    image: busybox:1.36.1
    command:
      - sh
      - -c
      - "sleep 3600"

This behavior is very useful for environment-specific sidecars. For example, a development overlay could add a debugging sidecar, while the production overlay keeps the base unchanged.

Be careful, however: not every list behaves like containers. Some Kubernetes lists are merged by a key, while others are replaced. The behavior depends on the Kubernetes API field definition. A common example is tolerations, which is usually replaced rather than merged.

Merging nested maps such as resources

Strategic Merge also works well for nested maps.

Suppose the base defines both requests and limits:

resources:
  requests:
    cpu: "50m"
    memory: "64Mi"
  limits:
    cpu: "250m"
    memory: "128Mi"

Now the overlay wants to update only the memory request:

patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nginx
      spec:
        template:
          spec:
            containers:
              - name: nginx
                resources:
                  requests:
                    memory: "128Mi"

The final result keeps the other fields and updates only the value we specified:

resources:
  requests:
    cpu: "50m"
    memory: "128Mi"
  limits:
    cpu: "250m"
    memory: "128Mi"

If we also update the memory limit, only that nested value is changed:

patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nginx
      spec:
        template:
          spec:
            containers:
              - name: nginx
                resources:
                  requests:
                    memory: "128Mi"
                  limits:
                    memory: "256Mi"

Final result:

resources:
  requests:
    cpu: "50m"
    memory: "128Mi"
  limits:
    cpu: "250m"
    memory: "256Mi"

This is exactly why Strategic Merge patches are so practical. We can describe the final values we care about without rewriting the full object.

Inline patches versus patch files

Inline patches are useful for small examples and quick changes:

patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nginx
      spec:
        replicas: 2

But inline patches can become hard to maintain as an overlay grows. A real project might need separate changes for resources, images, environment variables, volumes, probes, security context, and annotations.

A more maintainable approach is to move each patch into its own file:

overlays/dev/
├── kustomization.yaml
├── update-resources.patch.yaml
├── use-latest-tag.patch.yaml
└── mount-db-init.patch.yaml

Then reference those files from kustomization.yaml:

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

resources:
  - ../../base

patches:
  - path: update-resources.patch.yaml
  - path: use-latest-tag.patch.yaml
  - path: mount-db-init.patch.yaml

Using the .patch.yaml suffix makes the intent clear. It also helps maintainers understand that the file is not a full Kubernetes resource, but a partial patch document.

Example: patching resource requests and limits from a file

Create a patch file:

# overlays/dev/update-resources.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              memory: "256Mi"

Reference it:

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

resources:
  - ../../base

patches:
  - path: update-resources.patch.yaml

Build the overlay:

kubectl kustomize overlays/dev

Or, if you use the standalone binary:

kustomize build overlays/dev

The Deployment is updated only where the patch says it should be updated.

Example: patching only the image tag

Create a dedicated patch:

# overlays/dev/use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:latest

Add it to the overlay:

patches:
  - path: update-resources.patch.yaml
  - path: use-latest-tag.patch.yaml

This is more precise than using the global images field when multiple resources use the same image name.

However, do not spread conflicting changes across multiple patch files. If two patches update the same field, the result depends on patch order.

Patch order matters

Kustomize applies patches in the order they are listed.

For example:

patches:
  - path: use-latest-tag.patch.yaml
  - path: use-stable-tag.patch.yaml

If both files update the same container image, the second patch wins.

# use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:latest
# use-stable-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:stable

With this order:

patches:
  - path: use-latest-tag.patch.yaml
  - path: use-stable-tag.patch.yaml

The final image is:

image: nginx:stable

With the reverse order:

patches:
  - path: use-stable-tag.patch.yaml
  - path: use-latest-tag.patch.yaml

The final image is:

image: nginx:latest

This can become confusing quickly. If two patches touch the same field, it is usually better to combine them into one patch so the final intended state is obvious.

Mounting a generated ConfigMap with a patch

Patches are also useful when an overlay needs to mount a generated ConfigMap or Secret into an existing Deployment.

Suppose the development overlay generates a ConfigMap from a database initialization script:

overlays/dev/
├── db-init.sql
├── kustomization.yaml
└── mount-db-init.patch.yaml

Example script:

-- overlays/dev/db-init.sql
CREATE TABLE IF NOT EXISTS example_items (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL
);

Generate a ConfigMap in the overlay:

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

resources:
  - ../../base

configMapGenerator:
  - name: db-init-config
    files:
      - db-init.sql

patches:
  - path: mount-db-init.patch.yaml

Now patch the existing nginx Deployment to mount this ConfigMap:

# overlays/dev/mount-db-init.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          volumeMounts:
            - name: db-config
              mountPath: /db-config
      volumes:
        - name: db-config
          configMap:
            name: db-init-config

When Kustomize builds the overlay, the generated ConfigMap may receive a hash suffix:

apiVersion: v1
kind: ConfigMap
metadata:
  name: db-init-config-9mt2f7768c

The Deployment reference is also updated:

volumes:
  - name: db-config
    configMap:
      name: db-init-config-9mt2f7768c

This is important. We reference the generator name, db-init-config, in the patch. Kustomize then rewrites the reference to the generated name.

When Strategic Merge is not enough

Strategic Merge patches are excellent for adding or changing fields. But they are not always the best tool for removing fields or performing very exact operations.

For example, suppose we want to remove the entire resources section from the first container in every Deployment:

resources:
  requests:
    cpu: "50m"
    memory: "64Mi"
  limits:
    cpu: "250m"
    memory: "128Mi"

A Strategic Merge patch describes desired values, but removal is more naturally expressed as an operation:

remove this exact path

For that, we can use JSON 6902 patches.

JSON 6902 patches

JSON 6902 patches use a list of operations. Each operation has an op and a path. Some operations also have a value or from field.

Common operations are:

OperationPurpose
addAdd a value at a path.
removeRemove a value at a path.
replaceReplace a value at a path.
moveMove a value from one path to another.
copyCopy a value from one path to another.
testAssert that a value matches before continuing.

In Kustomize, JSON 6902 patches are especially useful when we need exact path-based changes.

Removing resources from a container

Create a JSON 6902 patch file:

# overlays/dev/remove-resources.patch.yaml
- op: remove
  path: /spec/template/spec/containers/0/resources

Then reference it with a target:

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

resources:
  - ../../base

patches:
  - path: remove-resources.patch.yaml
    target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx

The target tells Kustomize which object should receive the JSON patch:

Target fieldExampleMeaning
groupappsAPI group. For apps/v1, the group is apps.
versionv1API version.
kindDeploymentResource kind.
namenginxResource name.

The path uses JSON Pointer syntax:

/spec/template/spec/containers/0/resources

This path means:

spec
└── template
    └── spec
        └── containers
            └── 0
                └── resources

After the patch is applied, the resources field is removed from the first container.

YAML or JSON patch files

JSON 6902 patches can be written as YAML:

- op: remove
  path: /spec/template/spec/containers/0/resources

Or as JSON:

[
  {
    "op": "remove",
    "path": "/spec/template/spec/containers/0/resources"
  }
]

Both represent the same patch document. YAML is usually easier to read in Kubernetes projects, but JSON can be useful if you want to stay close to the original RFC 6902 format.

Why JSON 6902 paths can be brittle

JSON 6902 patches are precise, but that precision comes with a trade-off.

This path targets the first container:

/spec/template/spec/containers/0/resources

If someone changes the container order in the base, the patch might remove resources from the wrong container.

For example, imagine the base changes from this:

containers:
  - name: nginx
    image: nginx:1.27.0
    resources:
      requests:
        cpu: "50m"
        memory: "64Mi"

To this:

containers:
  - name: busybox
    image: busybox:1.36.1
  - name: nginx
    image: nginx:1.27.0
    resources:
      requests:
        cpu: "50m"
        memory: "64Mi"

The JSON path /spec/template/spec/containers/0/resources now points to busybox, not nginx.

This is why Strategic Merge is usually better when you can identify list items by merge keys such as container name. JSON 6902 is best when you truly need exact path-level operations, especially removal or replacement.

Applying one JSON patch to multiple resources

The target field does not always need a resource name. We can target a broader group.

For example, this removes the first container’s resources field from every Deployment in the overlay:

patches:
  - path: remove-resources.patch.yaml
    target:
      group: apps
      version: v1
      kind: Deployment

This applies to all Deployments, but not to Pods, Services, ConfigMaps, or Secrets.

If the base has these resources:

base/
├── nginx-deployment.yaml
├── reverse-proxy-deployment.yaml
└── reverse-proxy-pod.yaml

And the target is:

target:
  group: apps
  version: v1
  kind: Deployment

Then the patch applies to:

nginx-deployment.yaml
reverse-proxy-deployment.yaml

But it does not apply to:

reverse-proxy-pod.yaml

Even if the standalone Pod has a similar resources field, it will not be touched because the target kind is Deployment.

Targeting with label selectors

Targets can also use label selectors.

For example, we can patch only Deployments with the label tier=edge:

patches:
  - path: remove-resources.patch.yaml
    target:
      group: apps
      version: v1
      kind: Deployment
      labelSelector: tier=edge

A matching Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reverse-proxy
  labels:
    app: reverse-proxy
    tier: edge

A non-matching Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
    tier: app

This makes selectors useful when several resources need the same modification, but only if the patch is compatible with every selected resource.

For example, this is risky:

target:
  labelSelector: environment=dev

If the selector matches Deployments, Services, and ConfigMaps, a patch path like /spec/template/spec/containers/0/resources will fail for objects that do not have that path.

A safer target is more specific:

target:
  group: apps
  version: v1
  kind: Deployment
  labelSelector: environment=dev

Targeting with annotation selectors

Annotation selectors work similarly:

patches:
  - path: remove-resources.patch.yaml
    target:
      group: apps
      version: v1
      kind: Deployment
      annotationSelector: patching.example.com/remove-resources=true

Example resource:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  annotations:
    patching.example.com/remove-resources: "true"

Annotation selectors are useful when you do not want to use labels for patching logic. Labels often have operational meaning for selectors, services, network policies, or monitoring. An annotation can be a cleaner way to mark resources for customization.

Strategic Merge versus JSON 6902

Both patch styles are useful, but they solve different problems.

Use casePreferWhy
Update a Deployment image by container nameStrategic MergeMore readable and less dependent on container order.
Add a sidecar containerStrategic MergeCan append a new container while preserving existing containers.
Update nested resource requests or limitsStrategic MergeMerges maps cleanly.
Remove a specific fieldJSON 6902remove expresses deletion clearly.
Replace an exact pathJSON 6902Precise path-based operation.
Patch several resources selected by labelEitherUse target.labelSelector, but ensure every match supports the patch.
Patch CRDsOften JSON 6902Do not assume built-in Strategic Merge behavior for custom resources.

A good default is:

  • Use Strategic Merge when you want to describe partial Kubernetes YAML.
  • Use JSON 6902 when you need exact operations such as removereplace, or path-specific add.

Using labels instead of deprecated commonLabels

You may still see older examples using commonLabels:

commonLabels:
  environment: dev

In modern Kustomize, prefer the labels field:

labels:
  - pairs:
      environment: dev
    includeSelectors: false
    includeTemplates: true

This matters because labels can affect selectors. Accidentally changing selectors can break Services, Deployments, NetworkPolicies, or other resources that depend on label matching.

The labels field gives you more control:

FieldMeaning
pairsLabels to add.
includeSelectorsWhether to add labels to selectors. Use carefully.
includeTemplatesWhether to add labels to Pod templates.

For patching examples, labels are also useful because they can be used as patch targets:

target:
  kind: Deployment
  labelSelector: environment=dev

A complete overlay example

Here is a complete development overlay that demonstrates several patterns together:

overlays/dev/
├── db-init.sql
├── kustomization.yaml
├── mount-db-init.patch.yaml
├── update-resources.patch.yaml
└── use-latest-tag.patch.yaml

The kustomization.yaml file:

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

resources:
  - ../../base

namePrefix: dev-

labels:
  - pairs:
      environment: dev
    includeSelectors: false
    includeTemplates: true

configMapGenerator:
  - name: db-init-config
    files:
      - db-init.sql

patches:
  - path: update-resources.patch.yaml
  - path: use-latest-tag.patch.yaml
  - path: mount-db-init.patch.yaml

The resource patch:

# update-resources.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              memory: "256Mi"

The image patch:

# use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:latest

The ConfigMap mount patch:

# mount-db-init.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
        - name: nginx
          volumeMounts:
            - name: db-config
              mountPath: /db-config
      volumes:
        - name: db-config
          configMap:
            name: db-init-config

Build it:

kubectl kustomize overlays/dev

Apply it:

kubectl apply -k overlays/dev

This overlay keeps the base intact while applying development-specific changes in small, readable patch files.

Practical best practices

Use patches intentionally. They are powerful, but they can also make overlays hard to understand if everything becomes a patch.

A good patching style looks like this:

  • Keep the base clean and reusable.
  • Use top-level Kustomize fields for broad changes.
  • Use patches for resource-specific changes.
  • Prefer small patch files that do one thing.
  • Use clear names such as update-resources.patch.yaml or mount-db-init.patch.yaml.
  • Avoid multiple patches changing the same field.
  • Prefer Strategic Merge for normal Kubernetes object changes.
  • Use JSON 6902 for exact operations such as removing fields.
  • Be careful with JSON 6902 array indexes like /containers/0.
  • Use kindnamelabelSelector, and annotationSelector to avoid patching unintended resources.
  • Run kubectl kustomize <overlay> before applying changes.
  • Review the final rendered YAML, not only the patch files.

Common mistakes

Using images when only one resource should change

The images field updates all matching image names. This is useful for global image updates, but not for precise object-level changes.

Use a patch if only one Deployment should change.

Forgetting to add a new resource to the base

If you create a new file such as reverse-proxy-deployment.yaml, remember to add it to the base kustomization.yaml:

resources:
  - nginx-deployment.yaml
  - reverse-proxy-deployment.yaml

Kustomize only processes resources that are part of the kustomization graph.

Assuming all lists merge by name

The containers list has a merge key, but not all lists behave that way. Some lists are replaced. Always check the rendered output.

Relying too much on JSON Patch array indexes

A path like this is fragile:

/spec/template/spec/containers/0/resources

It depends on container order. If the order changes, the patch can affect the wrong container.

Selecting too many resources

This target is broad:

target:
  labelSelector: environment=dev

This target is safer:

target:
  group: apps
  version: v1
  kind: Deployment
  labelSelector: environment=dev

The more specific target is less likely to match incompatible resources.

Conclusion

Kustomize patches are the tool we reach for when general transformations are not enough.

Top-level fields such as imagesreplicaslabelsnamePrefix, and configMapGenerator are useful for common customization patterns. But patches give us fine-grained control over specific resources and fields.

Strategic Merge patches are usually the best starting point because they look like normal Kubernetes YAML and can merge fields intelligently for built-in Kubernetes types. JSON 6902 patches are more surgical and are especially useful when we need to remove or replace exact paths.

The key is to keep patches small, explicit, and well-targeted. When used carefully, they make Kustomize overlays powerful without turning the base into duplicated YAML.

References

  • Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
  • Kubernetes: Update API Objects in Place Using kubectl patch - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/
  • Kubernetes: kubectl patch Reference - kubernetes[.]io/docs/reference/kubectl/generated/kubectl_patch/
  • Kustomize API Types: Kustomization Fields - pkg[.]go[.]dev/sigs.k8s.io/kustomize/api/types
  • Kustomize: Inline Patch Example - github[.]com/kubernetes-sigs/kustomize/blob/master/examples/inlinePatch.md
  • Kustomize: Patching Multiple Resources Example - github[.]com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md
  • IETF: RFC 6902 JSON Patch - datatracker[.]ietf[.]org/doc/html/rfc6902

Post a Comment

0 Comments

Post a Comment (0)

#buttons=(Ok, Go it!) #days=(20)

This site uses cookies from Google to deliver its services and analyze traffic. Your IP address and user-agent are shared with Google along with performance and security metrics to ensure quality of service, generate usage statistics, and to detect and address abuse. More Info
Ok, Go it!