Skip to main content
RunBook Academy

KubernetesVII · Declarative Resource ManagementDeclarative resource management

kubectl patch — strategic merge, JSON patch, JSON merge patch

Intermediate⏱ ~18 minkubectl

What you'll learn

  • Use kubectl patch with strategic merge, JSON patch, and JSON merge patch
  • Identify when each format is the right choice
  • Combine patch with strategic merge patch directives (retainKeys, $patch: delete)
  • Recognise when patch is the right tool vs apply or edit

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

kubectl patch writes a partial object to the API server. It is the right tool when you want to change one field without disturbing the rest. This lesson covers the three patch formats kubectl supports, how each behaves on lists and maps, and when patch is the right tool versus apply or edit.

The three patch formats

# Substitute your own values before running:
KIND=deployment
NAME=web

kubectl patch "$KIND/$NAME" --type=strategic       # default
kubectl patch "$KIND/$NAME" --type=json            # RFC 6902
kubectl patch "$KIND/$NAME" --type=merge           # RFC 7396

If you omit --type, kubectl uses strategic merge patch. The other two are explicit.

Strategic merge patch — the default

Strategic merge patch is field-aware: it knows that a list of containers is merged element-by-element (keyed by name), and that a list of nodeSelectorTerms is replaced wholesale (no merge key).

# Add a label
kubectl patch deployment web -p '{"metadata":{"labels":{"env":"prod"}}}'

# Change the image
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:1.27.2"}]}}}}'

# Add a sidecar container
kubectl patch deployment web --type=strategic -p '
spec:
  template:
    spec:
      containers:
      - name: sidecar
        image: envoy:1.30
'

The behaviour:

  • Maps (objects) are merged recursively. A field in the patch overrides the same field on the live object; a field absent from the patch is left alone.
  • Lists with a merge key (e.g., containers keyed by name, volumes keyed by name) are merged element by element. A new element is added; an existing element with the same key is updated; an element absent from the patch is left alone.
  • Lists without a merge key (e.g., tolerations, nodeSelectorTerms) are replaced wholesale. A patch that includes the list replaces the entire list.

Strategic merge patch directives extend the syntax:

# Delete a list element by key (containers)
- op: remove
  path: /spec/template/spec/containers
  value:
    $patch: delete

# Replace a list element wholesale
- op: replace
  path: /spec/template/spec/containers
  value:
    - name: nginx
      image: nginx:1.27.2

# Retain unknown keys when replacing
spec:
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.27.2
        $retainKeys:
        - name

The $patch: delete directive tells strategic merge to remove the named list element. Without it, removing a container from a list would replace the entire list (and remove all other containers).

JSON patch — RFC 6902

JSON patch is a sequence of operations: add, remove, replace, move, copy, test. It is the most precise patch format; you name the path explicitly and say exactly what to do.

# Replace the image
kubectl patch deployment web --type=json -p '[
  {"op":"replace","path":"/spec/template/spec/containers/0/image","value":"nginx:1.27.2"}
]'

# Remove the second container
kubectl patch deployment web --type=json -p '[
  {"op":"remove","path":"/spec/template/spec/containers/1"}
]'

# Add a new label
kubectl patch deployment web --type=json -p '[
  {"op":"add","path":"/metadata/labels/env","value":"prod"}
]'

# Test-and-replace (atomic)
kubectl patch deployment web --type=json -p '[
  {"op":"test","path":"/spec/replicas","value":3},
  {"op":"replace","path":"/spec/replicas","value":5}
]'

The test operation is the killer feature: it asserts the current value matches the expected value, and the patch fails otherwise. This makes optimistic-concurrency patches safe — the patch either applies the change against the expected pre-state, or fails without modifying anything.

JSON patch is the right format when:

  • You need atomic test-and-modify semantics.
  • You are removing an element from a list without a merge key.
  • You are modifying a specific position in a list (e.g., containers[1]).
  • The patch logic is generated programmatically and needs explicit operations.

JSON patch is the wrong format when:

  • You want to merge a partial object declaratively (use strategic merge).
  • The list has a merge key and you want natural merging (strategic merge is cleaner).

JSON merge patch — RFC 7396

JSON merge patch is the simplest format: a partial object that is merged with the live state. Field present in patch overrides; field absent is left alone; field with value null is deleted.

# Add/update fields
kubectl patch deployment web --type=merge -p '{
  "spec":{"replicas":5}
}'

# Delete a field
kubectl patch deployment web --type=merge -p '{
  "spec":{"replicas":null}
}'

The behaviour:

  • A field with a non-null value in the patch overrides the live value.
  • A field with a value of null deletes the field from the live object.
  • A field absent from the patch is left alone.
  • Lists are replaced wholesale. JSON merge patch has no merge key; any list in the patch replaces the entire list on the live object.

JSON merge patch is the simplest format but the most limiting. Lists cannot be partially updated; the entire list must be included in the patch. For most Kubernetes use cases, strategic merge or JSON patch is the better choice.

Choosing the right patch type

Use caseFormat
Add/update fields, merge with live statestrategic
Remove a fieldstrategic with $patch: delete
Replace a list element by indexjson
Atomic test-and-modifyjson with test op
Remove a list element by keystrategic with $patch: delete
Replace the entire objectjson with replace
Simple update with no list manipulationmerge

Patch vs apply vs edit

Three commands write partial changes:

kubectl patch deployment web -p '{"spec":{"replicas":5}}'         # one field
kubectl edit deployment web                                       # interactive
kubectl apply -f deployment.yaml                                  # manifest-driven
  • patch is for one-off, targeted changes that should not touch the rest of the object. It does not update last-applied-configuration.
  • edit is for interactive changes where you want to see and edit the whole object.
  • apply is for manifest-driven changes where the manifest is the source of truth.

patch does not update last-applied-configuration. This means a subsequent apply of the manifest may overwrite the patch’s change. The pattern:

  1. Use patch for one-off triage.
  2. If the change is sustained, reflect it in the manifest.
  3. apply the manifest; the patch is preserved (because apply’s merge sees the patch as a manual change that wasn’t in last-applied).

Production patterns

Adding a label to a Deployment without a rollout:

kubectl patch deployment web --type=strategic -p '
metadata:
  labels:
    canary: "true"
'

Labels are part of metadata, not spec.template.metadata. Adding a label to the Deployment itself does not trigger a rollout. Adding a label to the Pod template does.

Triggering a rollout via a patch:

kubectl patch deployment web -p '
spec:
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/restartedAt: "2026-08-16T12:00:00Z"
'

The annotation change is a change to the Pod template’s metadata, which the Deployment controller detects as a template change and rolls out a new ReplicaSet.

Removing a container by name:

kubectl patch deployment web --type=strategic -p '
spec:
  template:
    spec:
      containers:
      - name: sidecar
        $patch: delete
'

The $patch: delete directive removes the named container without affecting the others. Without it, the patch would replace the entire containers list with an empty list, removing all containers.

Cross-course references

  • The Terraform course part XVII-Terraform-Drift covers state-vs-config drift; patch is the kubectl equivalent of a Terraform update operation.
  • The Ansible course part XXV-Ansible-CheckDiff covers surgical changes; patch is the same idea at the cluster level.
  • The Linux course part XXII-Linux-NetTroubleshoot covers surgical network changes; the patch discipline is the same.

Quiz

Knowledge check · 4 questions

  1. Q1. Which patch format is the default for kubectl patch when --type is not specified?

  2. Q2. `kubectl patch` updates the last-applied-configuration annotation, so a subsequent `kubectl apply` from the same manifest preserves the patch's change.

  3. Q3. An operator wants to remove a sidecar container from a 3-container Deployment using kubectl patch. They try `kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"sidecar","image":"envoy:1.30"}]}}}}'`. Diagnose what happens.

    Deployment has three containers: - name: nginx (main) - name: sidecar - name: metrics-exporter The operator wants to remove the sidecar. Patch attempted: ```bash kubectl patch deployment web -p ' {"spec":{"template":{"spec":{"containers":[{"name":"sidecar","image":"envoy:1.30"}]}}}} ' ```

  4. Q4. When is JSON patch the right format instead of strategic merge patch?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Use strategic merge patch for declarative merges. Strategic merge is the Kubernetes-native format; it understands field-level merge semantics.
  • Use JSON patch for test-and-modify. The test operation makes optimistic-concurrency patches safe.
  • Use $patch: delete to remove list elements by key. Without it, strategic merge patch replaces the whole list.
  • Reflect patches in the manifest. patch does not update last-applied-configuration; a subsequent apply will revert the patch.
  • Capture the object before every patch. A failed patch that left the object in an unexpected state is recoverable only if you have the pre-patch YAML.