Declarative vs. imperative
Declarative vs. imperative
Three ways to manage objects. Use one per object - mixing them is where drift comes from.
| Technique | Example | You say | Good for |
|---|---|---|---|
| Imperative commands | kubectl run, create deploy, scale | do this | exploring, one-offs |
| Imperative object config | kubectl create -f, replace -f | create exactly this file | rarely |
| Declarative object config | kubectl apply -f | make it look like this | everything you keep |
kubectl create deploy web --image nginx:1.27 # fails if it exists
kubectl apply -f web.yaml # creates or updates - idempotent
The workflow
graph LR
G["generate<br/>--dry-run=client -o yaml"] --> E[edit YAML]
E --> D["kubectl diff -f"]
D --> A["kubectl apply -f"]
A --> V[version in git]
kubectl create deploy web --image nginx:1.27 --dry-run=client -o yaml > web.yaml
kubectl diff -f web.yaml # what would change?
kubectl apply -f web.yaml --dry-run=server # validated by the API, not stored
kubectl apply -f web.yaml
How apply knows what to remove
apply compares three things: your file, the live object and the last
applied configuration. Client-side apply stores the last one in the
annotation kubectl.kubernetes.io/last-applied-configuration.
Server-side apply (kubectl apply --server-side) moves that bookkeeping to
the API server (metadata.managedFields) and reports conflicts when two
managers own the same field. Helm, Argo CD and controllers use it.
Warning
kubectl edit or kubectl scale on an object you manage with apply creates
drift: the next apply from git silently reverts it.