OnCallReady

Lesson 15.40 · Kubernetes: Architecture & Workloads · 18 min read

Imperative vs declarative: create, apply, diff, patch, replace, edit

In plain words

There are two ways to set up a board game. You can say each move out loud: "put the red piece here, move the blue one there" (imperative). Or you can have a photo of how the board should look and arrange it to match; if someone knocks a piece, you just look at the photo again (declarative). The photo is easy to share, check and keep.

kubectl supports both. create, scale, set image and edit are imperative: quick for the exam and emergencies. apply -f from files in git is declarative: it converges and is idempotent (created, configured, unchanged), using a three-way merge with the last-applied-configuration annotation. diff previews changes (exit 1 means differences). patch comes in strategic, merge and JSON dialects, and replace --force deletes and recreates for immutable fields.

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

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:

Why it helps

Production runs on declarative apply from git, through reviewed CI jobs, and knowing the three-way merge explains real conflicts: a manifest with replicas: 3 fighting an autoscaler on every apply, or a manual kubectl edit in prod silently reverted by the next deploy. kubectl diff is your terraform plan for Kubernetes. The patch dialects matter when scripting: a JSON merge patch replaces lists wholesale and can delete your sidecar. replace loses fields other controllers set. In the exam, imperative commands and edit are how you work fast; at work, you write it back to git afterwards.

Commands in this lesson

cd sed echo

FAQ

What is the difference between kubectl create and kubectl apply?

create makes an object and fails with AlreadyExists if it's there: a one-shot imperative command. apply converges: it creates if missing, updates what changed, and prints unchanged if nothing did, so it's safe to run repeatedly in pipelines. It records the applied manifest in an annotation to compute future changes.

Why does kubectl apply not remove a field someone else set?

The three-way merge compares the last-applied configuration, the live object and the new file. Fields in the new file are set; fields in last-applied but not in the new file are removed; fields only in live, set by someone else, are left alone. That's why apply coexists with controllers: leave replicas out of a manifest managed by an HPA and apply won't touch it.

What do kubectl diff's exit codes mean?

0 means no differences, 1 means there are differences, and greater than 1 means an error. The server computes what apply would produce and diffs it against the live object. Use it in CI and before every production apply; remember a script with set -e will stop on exit 1 unless you handle it.

Which patch type should I use?

Strategic merge (the default) understands Kubernetes lists keyed by name, so patching containers merges by container name. JSON merge patch (--type=merge) replaces lists entirely, so a patch naming one container removes the others. JSON patch (--type=json) addresses exact paths with add, remove and replace operations, precise for removing a field like a nodeSelector.

Is kubectl edit OK to use in production?

For emergencies, yes, but it's unreviewed and invisible to git, and the next deploy from git may revert it. Treat it like a portal hotfix in Terraform: make the change, then immediately write it back into the repository through a PR. In the exam, edit is a fast and legitimate tool.

In an interview Mid

What is the difference between kubectl create, apply, patch and replace?

Also asked: What is the difference between imperative and declarative management in Kubernetes? · Why can kubectl apply fight an autoscaler? · What does kubectl diff show, and what does its exit code mean?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.