Why this matters
What you need to know already: manifests and $do generators (15.3), the reconciliation loop (15.9), Deployments and rollouts (15.16), Terraform's plan and apply (12.24), diff -u output (the -/+ lines of a PR diff), JSON (7.11).
Someone ran kubectl scale in an emergency, someone else edited the file in git, and a script patched the same Deployment. Which one wins on the next deploy, and what silently disappears? kubectl has several ways to change an object, and they differ exactly on those questions.
Two ways of saying the same thing
imperative k create deployment web --image=nginx:1.27
k scale deploy web --replicas=3
k set image deploy/web nginx=nginx:1.28
declarative k apply -f web.yaml (the file says what should exist)
Imperative = commands that each say what to do (create, scale, set image). They are fast and great for the exam, experiments and emergencies. Declarative = a file that says what should exist. It is what production runs on: the YAML lives in git, is reviewed, and apply makes the cluster match it - the same model as Terraform (12.1).
Later (Ch 26): GitOps tools keep running this apply from git for you, all the time.
create fails if it exists; apply converges
A manifest to play with - generated, not typed (and the old web out of the way; --ignore-not-found = no error if it does not exist):
$ cd ~/oncall-lab/labs/3-k8s && k delete deploy web --ignore-not-found
$ k create deployment web --image=nginx:1.27 --replicas=2 --dry-run=client -o yaml > web.yaml
$ k create -f web.yaml
deployment.apps/web created
$ k create -f web.yaml
Error from server (AlreadyExists): error when creating "web.yaml": deployments.apps "web" already exists
$ k apply -f web.yaml
Warning: resource deployments/web is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
deployment.apps/web configured
$ k apply -f web.yaml
deployment.apps/web unchanged
k create -f FILE creates the objects in a file and fails if they exist; k apply -f FILE creates or updates them. The warning says the object was made without apply's bookkeeping annotation (next section) and that kubectl fixed it. apply prints created, configured or unchanged - idempotent (0.35), so safe to run on every deploy.
How apply decides what to change: the 3-way merge
apply stores the manifest you applied in an annotation, kubectl.kubernetes.io/last-applied-configuration. On the next apply it compares three things:
last-applied what you applied last time
live what is in the cluster now (including other people's changes)
new file what you are applying now
- a field in the new file -> set to that value
- a field in last-applied but not in the new file -> removed (you deleted it)
- a field in live but in neither -> left alone (someone else owns it)
That third rule is why apply coexists with controllers and humans. If your manifest has no replicas: and an autoscaler or k scale set it to 5, apply leaves it at 5. If your manifest does say replicas: 3, every apply resets it to 3 - fighting the autoscaler. (Leave replicas out of autoscaled manifests; the autoscaler is Ch 17.)
k apply view-last-applied deploy/web prints the stored annotation:
$ k apply view-last-applied deploy/web
apiVersion: apps/v1
kind: Deployment
...
diff before you apply
Change the file, then ask what apply would do. k diff -f FILE prints the difference between the live object and what apply would make of it, in diff -u format (- lines go away, + lines arrive):
$ sed -i 's/replicas: 2/replicas: 3/' web.yaml
$ k diff -f web.yaml
diff -u -N /tmp/LIVE-2718281828/apps.v1.Deployment.default.web /tmp/MERGED-3141592653/apps.v1.Deployment.default.web
--- /tmp/LIVE-2718281828/apps.v1.Deployment.default.web
+++ /tmp/MERGED-3141592653/apps.v1.Deployment.default.web
@@ -6,7 +6,7 @@
creationTimestamp: "2026-09-22T20:00:07Z"
- generation: 1
+ generation: 2
labels:
@@ -15,7 +15,7 @@
spec:
progressDeadlineSeconds: 600
- replicas: 2
+ replicas: 3
revisionHistoryLimit: 10
$ echo $?
1
The server computes what apply would produce (a server-side dry run, so defaults and the generation bump show up too) and runs diff -u of it against live. Exit code 1 = there are differences, 0 = none, >1 = error. It is the terraform plan of kubectl - run it in CI and in your head before every production apply. (echo $? prints the exit code of the previous command, 1.7.)
patch: three dialects
k patch OBJECT -p 'JSON' changes just the fields you send; --type picks how the JSON is interpreted:
k patch deploy web -p '{"spec":{"replicas":4}}' # strategic merge (default)
k patch deploy web --type=merge -p '{"metadata":{"labels":{"x":"y"}}}' # JSON merge patch
k patch deploy web --type=json -p '[{"op":"replace","path":"/spec/replicas","value":4}]'
The difference shows on lists. A strategic merge patch knows that containers is keyed by name, so it merges:
$ k patch deploy web -p '{"spec":{"template":{"spec":{"containers":[{"name":"sidecar","image":"busybox:1.36"}]}}}}'
deployment.apps/web patched
$ k get deploy web -o jsonpath='{.spec.template.spec.containers[*].name}'
nginx sidecar
A JSON merge patch (RFC 7386) replaces lists wholesale:
$ k patch deploy web --type=merge -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:1.28"}]}}}}'
$ k get deploy web -o jsonpath='{.spec.template.spec.containers[*].name}'
nginx
The sidecar is gone. A JSON patch (RFC 6902) addresses exact paths by index: add, remove, replace - the precise tool for "remove this nodeSelector" or "change the port of the first probe":
k patch deploy web --type=json -p '[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'
replace and edit
k replace -f web.yaml sends the whole object (a PUT): anything not in the file is lost, including fields other controllers set. k replace --force -f deletes and recreates - the way to change fields that are immutable, like a Pod's containers or a Job's template. It is a delete: the old pods go.
k edit deploy web opens the live object in $KUBE_EDITOR/$EDITOR (nano or vim) and applies what you save. Quick, unreviewed, and invisible to git - the next apply from git may revert it. Fine for the exam and for emergencies; write it back into the repo after.
Which to use
exam / quick experiments imperative + $do generators, edit, patch
production apply -f from git (via a reviewed CI job)
immutable field change replace --force (or delete + apply) - accepting a restart
one targeted field in a script patch --type=json
What you can now do:
- Explain create vs apply and the 3-way merge (what apply removes, what it leaves alone).
- Preview a change with
kubectl diffand read its exit code. - Pick the right patch type, and know when only
replace --forceworks.