Every CKAD task that says modify the existing Deployment so that… is a race between you and a 2-hour clock. You can open the object in an editor, scroll, change a line, save and hope the API server accepts it. Or you can send exactly the change you want in a single command and move on. That second option is kubectl patch, and it is one of the most under-practised commands on the CKAD exam.
The trouble is that kubectl patch is really three commands wearing one flag. The default strategic merge patch merges some lists and replaces others, following rules buried in the API type definitions. A JSON merge patch always replaces lists and is the only one that works on custom resources. A JSON Patch is an ordered list of operations addressed by index. Pick the wrong one and your patch silently deletes a container, wipes every toleration on a Pod, or fails with an error you have never seen.
This guide explains the three patch types from a practitioner’s perspective, lists which Pod fields merge by key and which get replaced, walks through the strategic-merge directives, covers --subresource, and ends with a decision table for patch vs set vs edit vs apply vs replace and a set of exam-day one-liners. The broader topic of how apply, create and replace manage objects, including the three-way merge and Server-Side Apply, is covered in our kubectl object management guide; this article assumes that background and goes one layer deeper into patch itself.
Why Patch Matters on the CKAD
The CKAD is a performance-based exam with roughly 15 to 20 tasks in two hours. Many of them are edit an existing object tasks: change an image, add an environment variable, add a label, flip a CronJob to suspended, change a Service type, scale something. For each of those, kubectl patch is a one-liner that needs no file and no editor session.
Three properties make it worth learning properly:
- It is partial. You send only the fields you want to change. Everything else on the live object stays as it is, including fields written by controllers.
- It is atomic per request. The API server applies the patch against the current object, runs validation and admission, and either accepts the whole change or rejects it with a message.
- It reaches subresources. With
--subresourceyou can changescale,statusorresizedirectly, which no editor flow does cleanly.
The cost is that you have to know what the patch means. That is the rest of this article.
The Three Patch Types
The --type flag selects the patch format. The default is strategic.
--type | Standard | How lists are handled | null on a field | Works on custom resources | Typical use |
|---|---|---|---|---|---|
strategic (default) | Kubernetes-specific | Merged by key for fields that declare a merge key; replaced otherwise | Deletes the field | No | Add or change a container, env var, port or volume by name |
merge | JSON Merge Patch, RFC 7386 | Always replaced with the list you send | Deletes the field | Yes | CRDs, removing finalizers, simple scalar changes |
json | JSON Patch, RFC 6902 | Addressed by index with explicit add, remove, replace operations | Not applicable (use remove) | Yes | Remove a list element, insert at a position, rename a key, guarded changes with test |
The one sentence to memorise: strategic merge knows the Kubernetes schema; the other two do not. That single fact explains every difference in the table.
Strategic Merge Patch: Merging by Key
A strategic merge patch looks like a fragment of the object. Maps merge recursively, so a patch containing only metadata.labels.tier: web adds or updates that one label and leaves every other label alone. The interesting behaviour is in lists.
Every list field in a built-in API type carries a patchStrategy. If the strategy is merge with a patchMergeKey, elements in your patch are matched to live elements by that key and merged. If the field has no strategy, the default is replace, and the list you send becomes the whole list.
Fields that merge by key
| Field | Merge key | What this means for your patch |
|---|---|---|
spec.containers, spec.initContainers, spec.ephemeralContainers | name | You can change one container’s image by sending only its name and image |
containers[].env | name | Add or update a single env var without listing the others |
containers[].ports | containerPort | Add a port without repeating existing ones |
containers[].volumeMounts | mountPath | Add a mount by path |
spec.volumes | name | Add a volume by name |
spec.imagePullSecrets | name | Add a pull secret |
spec.hostAliases | ip | Add an entry per IP |
spec.topologySpreadConstraints | topologyKey | Change one constraint |
Service spec.ports | port | Add a Service port |
metadata.finalizers | set of strings (merge) | Values are added as a set; remove with $deleteFromPrimitiveList |
Fields that are replaced wholesale
| Field | Behaviour | The trap |
|---|---|---|
spec.tolerations | Replaced | Patching in one toleration removes every existing toleration |
containers[].args, containers[].command | Replaced | Send the full argument list |
containers[].envFrom | Replaced | Send all envFrom sources |
spec.strategy on a Deployment | retainKeys | Switching type needs the $retainKeys directive (below) |
Maps such as metadata.labels, metadata.annotations and spec.nodeSelector are merged key by key, so adding one label never removes another.
A worked example
Start with a two-replica Deployment:
kubectl create deployment web --image=nginx:1.27 --replicas=2
Add a sidecar container by name. Because containers merges on name, the nginx container survives:
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"log","image":"busybox:1.36","command":["sh","-c","sleep 3600"]}]}}}}'
kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[*].name}'
# web log
Now add a toleration. Because tolerations is replace, the list you send is the list you get:
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"tolerations":[{"key":"disktype","operator":"Equal","value":"ssd","effect":"NoSchedule"}]}}}}'
If the Pod template already had a dedicated=test-team toleration, it is gone. On the exam, read the live tolerations first with kubectl get deploy web -o yaml and include all of them in the patch.
How to find out which rule a field uses
kubectl explain describes fields but does not print the patch strategy. The information lives in the OpenAPI schema as x-kubernetes-patch-merge-key and x-kubernetes-patch-strategy:
kubectl get --raw /openapi/v2 | jq '.definitions["io.k8s.api.core.v1.PodSpec"].properties.containers'
{
"items": { "$ref": "#/definitions/io.k8s.api.core.v1.Container" },
"type": "array",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
}
You will not have jq on the exam, so the table above is what to carry in your head. The rule of thumb covers most cases: lists of named things (containers, volumes, env, ports) merge; lists of unnamed things (tolerations, args, envFrom) replace.
Strategic Merge Directives
Because a strategic merge patch is schema-aware, it supports a handful of directives that JSON merge patch cannot express. They all start with $.
$patch: delete — remove one element from a keyed list
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"log","$patch":"delete"}]}}}}'
This removes only the log container. Without the directive, a JSON merge patch would need the full remaining list, and a JSON Patch would need the element’s index.
$patch: replace — replace a list or map instead of merging
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"$patch":"replace"},{"name":"web","image":"nginx:1.27"}]}}}}'
The first element says replace the whole list; the elements after it are the new list. This is how you deliberately drop every other container in a keyed list.
$deleteFromPrimitiveList/<field> — remove values from a set-like list
metadata.finalizers is a list of strings with the merge strategy, so it behaves as a set. To remove one value:
kubectl patch pvc data -p '{"metadata":{"$deleteFromPrimitiveList/finalizers":["kubernetes.io/pvc-protection"]}}'
Most people instead clear the whole list with a merge patch ({"metadata":{"finalizers":null}}), which is fine when there is only one finalizer. The mechanics of why an object gets stuck in Terminating are in our finalizers and garbage collection guide.
$setElementOrder/<field> — control ordering
Keyed lists keep their live order after a merge. If order matters, for example for initContainers that run sequentially, $setElementOrder/initContainers takes a list of merge-key values in the order you want. You will rarely need this on the exam, but it explains why a merged list sometimes does not appear in the order you wrote it.
$retainKeys — switch between mutually exclusive fields
Deployment spec.strategy is a union: rollingUpdate may only be present when type is RollingUpdate. A naive patch fails:
kubectl patch deployment web -p '{"spec":{"strategy":{"type":"Recreate"}}}'
# The Deployment "web" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate'
The field’s strategy is retainKeys, so tell the server which keys to keep and it clears the rest:
kubectl patch deployment web -p '{"spec":{"strategy":{"$retainKeys":["type"],"type":"Recreate"}}}'
Alternatively send {"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}, which uses the null deletion rule instead. Both work; the second is easier to remember. Deployment strategies themselves are covered in the rolling updates guide.
JSON Merge Patch: Simple and Blunt
--type merge follows RFC 7386. The rules fit in two lines:
- Maps are merged recursively, and a
nullvalue deletes that key. - Any list in the patch replaces the live list entirely.
That bluntness is the point. There is no schema to consult, which is why it is the format that works on custom resources:
kubectl patch deployment web --type merge -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:1.27"}]}}}}'
That command looks identical to the strategic version, but with merge it throws away the log sidecar because the containers list was replaced with a one-element list. Each new Pod comes up 1/1 instead of 2/2.
Where merge patch shines is scalars, maps and deletions:
# Flip a CronJob to suspended
kubectl patch cronjob backup --type merge -p '{"spec":{"suspend":true}}'
# Remove a label (null deletes the key)
kubectl patch deployment web --type merge -p '{"metadata":{"labels":{"tier":null}}}'
# Clear all finalizers on a stuck namespace-scoped object
kubectl patch pvc data --type merge -p '{"metadata":{"finalizers":null}}'
# Patch a custom resource (strategic merge would be rejected)
kubectl patch mycrd example --type merge -p '{"spec":{"replicas":3}}'
Try a strategic patch on a CRD and you get strategic merge patch format is not supported. When you see a resource from an Operator in a CKAD task, reach for --type merge first. Custom resources themselves are introduced in our CKAD CRD and Operator guide.
JSON Patch: Ordered Operations by Index
--type json follows RFC 6902. The patch is a JSON array of operations, each with an op, a path and usually a value:
op | What it does |
|---|---|
add | Insert a value at a path. On a list, /- appends; /0 inserts at the front. On a map, adds or replaces the key |
remove | Delete the value at a path |
replace | Replace an existing value (fails if the path does not exist) |
move | Move a value from one path to another |
copy | Copy a value from one path to another |
test | Assert that a path has a value; the whole patch fails if not |
Paths use JSON Pointer syntax: each segment is separated by /, list elements are addressed by index, and the two special characters are escaped as ~0 for ~ and ~1 for /. That last rule matters for Kubernetes because label and annotation keys often contain a slash.
# Replace the first container's image by index
kubectl patch deployment web --type json -p '[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"nginx:1.28"}]'
# Append an env var to the first container (creates the env list if missing)
kubectl patch deployment web --type json -p '[{"op":"add","path":"/spec/template/spec/containers/0/env/-","value":{"name":"LOG_LEVEL","value":"debug"}}]'
# Remove the second toleration without touching the first
kubectl patch deployment web --type json -p '[{"op":"remove","path":"/spec/template/spec/tolerations/1"}]'
# Add a label whose key contains a slash
kubectl patch node node01 --type json -p '[{"op":"add","path":"/metadata/labels/node-role.kubernetes.io~1worker","value":""}]'
# Only change the image if it is currently nginx:1.27
kubectl patch deployment web --type json -p '[{"op":"test","path":"/spec/template/spec/containers/0/image","value":"nginx:1.27"},{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"nginx:1.28"}]'
Two cautions. First, add to /env/- fails if the container has no env field at all; in that case add the whole list: {"op":"add","path":"/spec/template/spec/containers/0/env","value":[{...}]}. Second, indexes are positional, so check them before you patch:
kubectl get deployment web -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{"\n"}{end}'
JSON Patch is the only format that can remove a single element from an unkeyed list, such as one toleration or one argument, without re-sending the rest. That makes it worth knowing even if strategic merge is your default.
Ways to Deliver the Patch
The patch body can be JSON or YAML, inline or from a file. These four are equivalent:
kubectl patch deployment web -p '{"spec":{"replicas":3}}'
kubectl patch deployment web -p $'spec:\n replicas: 3'
kubectl patch deployment web --patch-file replicas.yaml
kubectl patch deployment web --patch-file replicas.json
The $'...' form lets Bash interpret \n, which is the quickest way to write YAML inline. On the exam, inline JSON is usually fastest for one-field changes and --patch-file is better for anything with more than two levels of nesting, because you can write the fragment in YAML and avoid quoting mistakes.
Useful flags:
| Flag | Purpose |
|---|---|
--type | strategic (default), merge or json |
-p, --patch | Inline patch body |
--patch-file | Patch from a file, JSON or YAML |
--dry-run=client -o yaml | Show the object kubectl would send without contacting the server; note it does not validate against the schema |
--dry-run=server | Send the patch, run validation and admission, persist nothing |
-f FILE --local | Apply the patch to a local manifest instead of the live object, handy for editing a YAML you are about to apply |
--field-manager | Field-manager name recorded in managedFields; defaults to kubectl-patch |
--subresource | Target status, scale or resize instead of the main resource |
--dry-run=server is the one to remember. It is the cheapest way to confirm a patch is valid before you commit to it under time pressure.
Patching Subresources: scale, status and resize
Some changes are not accepted on the main resource endpoint. Replicas on a Deployment can be patched directly, but the scale subresource is what kubectl scale and the HorizontalPodAutoscaler actually use, and patching it is a clean way to prove you understand subresources:
kubectl patch deployment web --subresource scale --type merge -p '{"spec":{"replicas":4}}'
# scale.autoscaling/web patched
The --subresource flag works with get, patch, edit, apply and replace, and is stable in kubectl v1.33 and later. It accepts status, scale and, from kubectl v1.32 onward, resize. If the resource does not expose the subresource you asked for, the API server returns 404 Not Found.
Two subresource cases show up in modern CKAD tasks:
resizeis how you change a running Pod’s CPU or memory in place. The main Pod endpoint still rejects changes toresources, sokubectl patch pod NAME --subresource resizeis mandatory. The full mechanics, includingresizePolicyand thePodResizePendingcondition, are in our in-place Pod resize guide.statuslets you write a status condition on a custom resource that has the status subresource enabled, which is useful when debugging an Operator that is not reconciling. Normal users cannot patch the status of built-in types because the kubelet and controllers own those fields.
What a Patch Does to a Deployment
A patch that touches spec.template creates a new ReplicaSet and starts a rollout, exactly like kubectl set image or kubectl edit would. A patch that touches anything outside the template, such as replicas, paused, minReadySeconds or revisionHistoryLimit, does not create a new revision.
That distinction leads to one of the most useful exam patterns: pause, make several template changes with patches, then resume so the Deployment rolls once instead of three times:
kubectl patch deployment web -p '{"spec":{"paused":true}}'
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:1.28"}]}}}}'
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","resources":{"requests":{"cpu":"100m"}}}]}}}}'
kubectl patch deployment web -p '{"spec":{"paused":false}}'
kubectl rollout status deployment web
kubectl rollout pause and resume do the same thing; the patch form simply shows what those commands send.
patch vs set vs edit vs apply vs replace
The CKAD gives you five ways to change an object. Choose by what you know and what you want to protect.
| Command | Sends | Best for | Watch out for |
|---|---|---|---|
kubectl set image/env/resources/serviceaccount/selector/subject | A strategic merge patch built for you | The most common single-field changes; fastest to type | Only covers the fields it has subcommands for |
kubectl patch | Exactly the patch you write | Any field, subresources, CRDs (merge), list surgery (json) | You must know the list semantics |
kubectl edit | A patch computed from your editor diff | Several related changes when you want to see the whole object | Slow; validation errors send you back into the editor |
kubectl apply -f | A three-way merge patch against last-applied-configuration (or Server-Side Apply) | Declarative workflows where the file is the source of truth | Fields you removed from the file are deleted; mixing with imperative edits causes drift |
kubectl replace -f | The whole object (PUT) | When you have a complete, current manifest; --force deletes and recreates | Fails on resourceVersion conflicts; --force causes downtime |
A practical ordering for exam tasks:
- If a
kubectl setsubcommand exists for the change, use it. - Otherwise
kubectl patchwith the default strategic type, unless the target is a CRD (--type merge) or you need to remove one element of an unkeyed list (--type json). - Use
kubectl editwhen you need to make several changes and verify context, or when a question explicitly asks you to edit. - Use
applywhen the task gives you a file to maintain, andreplace --forceonly for immutable-field changes on objects you are allowed to recreate.
Why not always edit? Because the editor loop is where exam minutes disappear. A validation error from kubectl edit drops you into a temporary file with a comment block, and it is easy to lose the change. A validation error from kubectl patch is one line on your terminal, and you re-run with the fix.
Exam-Day One-Liners
Each of these is a complete answer to a task you could plausibly see. All use the default namespace; add -n as required.
# Cordon a node without kubectl cordon
kubectl patch node node01 -p '{"spec":{"unschedulable":true}}'
# Change a Service from ClusterIP to NodePort
kubectl patch svc web --type merge -p '{"spec":{"type":"NodePort"}}'
# Add a label to a Deployment and to its Pod template
kubectl patch deployment web -p '{"metadata":{"labels":{"env":"prod"}},"spec":{"template":{"metadata":{"labels":{"env":"prod"}}}}}'
# Add an env var from a Secret key to the "web" container
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","env":[{"name":"DB_PASS","valueFrom":{"secretKeyRef":{"name":"db","key":"password"}}}]}]}}}}'
# Add a readiness probe to the "web" container
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","readinessProbe":{"httpGet":{"path":"/healthz","port":80},"periodSeconds":5}}]}}}}'
# Mount an existing ConfigMap as a volume
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"volumes":[{"name":"cfg","configMap":{"name":"app-config"}}],"containers":[{"name":"web","volumeMounts":[{"name":"cfg","mountPath":"/etc/app"}]}]}}}}'
# Suspend and resume a CronJob
kubectl patch cronjob backup -p '{"spec":{"suspend":true}}'
kubectl patch cronjob backup -p '{"spec":{"suspend":false}}'
# Scale through the scale subresource
kubectl patch deployment web --subresource scale --type merge -p '{"spec":{"replicas":5}}'
# Remove a container by name (strategic directive)
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"containers":[{"name":"log","$patch":"delete"}]}}}}'
# Remove the first toleration only (JSON Patch by index)
kubectl patch deployment web --type json -p '[{"op":"remove","path":"/spec/template/spec/tolerations/0"}]'
# Switch a Deployment to Recreate
kubectl patch deployment web -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
# Set a ServiceAccount on the Pod template
kubectl patch deployment web -p '{"spec":{"template":{"spec":{"serviceAccountName":"app-sa"}}}}'
Each of the Pod-template patches above triggers a rolling update. Follow with kubectl rollout status so you know the task is actually finished before you move on. For label-only tasks, kubectl label is even shorter; see the labels and selectors guide for that family of commands.
Common Mistakes
- Patching tolerations, args or envFrom with strategic merge and expecting a merge. Those lists replace. Read the live list first and send the complete new list, or use JSON Patch to insert.
- Using
--type mergeon a Pod template with several containers. Thecontainerslist is replaced, and every container you did not mention is deleted. Use strategic merge for keyed lists. - Using the default strategic type on a custom resource. It fails with
strategic merge patch format is not supported. Add--type merge. - Forgetting the
namekey in a container patch. Strategic merge needs the merge key to match an element; without it, the server cannot tell which container you mean. - Guessing list indexes in a JSON Patch. Print the list with
jsonpathfirst. Index0is the first element, and-means append. - Trusting
--dry-run=client. It only shows what kubectl would send; it does not validate the patch against the schema. Use--dry-run=serverto catch errors. - Patching a Pod’s
resourceson the main endpoint. Running Pods reject that; use--subresource resize. - Patching a Deployment’s Pod with
kubectl patch pod. The ReplicaSet recreates it from the template. Patch the Deployment. - Mixing
patchwithapply-managed objects without updating the file. The nextapplycan revert your patch because the file no longer matches. Either patch the file too or accept thatapplyowns the object.
Practice Until the Syntax Is Automatic
Reading about patch types is cheap; typing a nested JSON patch correctly on the first try with the clock running is the actual skill. Build a throwaway Deployment with two containers, two tolerations and three env vars, and run each one-liner from the list above against it. Then break it deliberately: send a merge patch where a strategic one was needed and watch the sidecar vanish, patch a CRD without --type merge and read the error, and append to an env list that does not exist yet. Doing this once makes the failure modes recognisable on exam day.
Sailor.sh’s Certified Kubernetes Application Developer (CKAD) Mock Exam Bundle presents modify the existing object tasks in a live browser terminal with the same time pressure and phrasing as the real exam, which is the fastest way to find out whether your patch reflexes hold up. Pair it with the CKAD kubectl cheat sheet for the surrounding commands and our free CKAD practice guide if you are setting up a local cluster to rehearse on.
Frequently Asked Questions
What is the difference between strategic merge patch, JSON merge patch and JSON Patch in kubectl?
Strategic merge patch is Kubernetes-specific and uses the API schema to merge lists by key (for example containers by name). JSON merge patch (RFC 7386) merges maps but replaces any list entirely, and null deletes a field. JSON Patch (RFC 6902) is an ordered array of add, remove, replace, move, copy and test operations addressed by JSON Pointer paths with list indexes. Select them with --type strategic (default), --type merge and --type json.
Why does kubectl patch fail on a custom resource?
Strategic merge patch is not supported for custom resources because the API server has no patch-strategy metadata for them. Use --type merge or --type json on anything served by a CRD.
How do I remove a finalizer with kubectl patch?
Either clear the list with a merge patch, kubectl patch RESOURCE NAME --type merge -p '{"metadata":{"finalizers":null}}', or remove a single value with the strategic directive {"metadata":{"$deleteFromPrimitiveList/finalizers":["example.com/my-finalizer"]}}. Only do this once you understand why the finalizer is still there.
Does kubectl patch on a Deployment trigger a rolling update?
Only if the patch changes spec.template. Changes to replicas, paused, minReadySeconds or revisionHistoryLimit do not create a new ReplicaSet. You can patch paused: true, make several template changes, then patch paused: false to roll once.
How do I add an item to a list with kubectl patch without replacing the list?
If the list has a merge key (containers, env, ports, volumes, volumeMounts), a strategic merge patch containing just the new element merges it in. If the list has no merge key (tolerations, args), use JSON Patch with {"op":"add","path":"/…/tolerations/-","value":{…}} to append, or send the full list with strategic or merge patch.
What does --subresource do in kubectl patch?
It sends the patch to a subresource endpoint instead of the main resource: scale to change replicas the way the HPA does, status to write status on objects that allow it, and resize to change a running Pod’s CPU and memory in place. The flag is stable from kubectl v1.33 and returns 404 Not Found if the resource does not support the subresource.
Is kubectl patch faster than kubectl edit on the CKAD?
For single-field or single-element changes, yes: it needs no editor session and errors come back as one line. For several related changes in a large object, kubectl edit or a --patch-file can be clearer. Use kubectl set when a subcommand exists for the change, because it is shorter than either.
Conclusion
kubectl patch is three formats behind one flag. Strategic merge is schema-aware: keyed lists such as containers and env merge, unkeyed lists such as tolerations replace, and directives like $patch: delete and $retainKeys handle the awkward cases. JSON merge patch replaces every list and is the only format that works on custom resources. JSON Patch addresses elements by index and is the right tool for removing or inserting into unkeyed lists. Add --subresource for scale, status and resize, use --dry-run=server to validate under pressure, and keep kubectl set for the changes it covers. Practise the one-liners until the quoting is muscle memory and modify the existing object becomes the easiest kind of task on the paper.