Declarative vs Imperative
Summary
- Imperative commands tell Kubernetes what to do right now; declarative configuration tells it what should exist and lets controllers make it so.
- Insight:
kubectl applytreats your YAML as the source of truth and computes the diff needed to reach it, which is what enables GitOps, review, and repeatability. - Key Concepts: desired state, reconciliation, the last-applied-configuration annotation, server-side apply, and field ownership.
- When to Use: declarative for anything you keep, version, or deploy repeatedly; imperative for quick investigation, one-off debugging, and generating starter YAML.
- Limitations/Trade-offs: declarative has a learning curve and a diff model to understand; imperative is fast but leaves no auditable record.
- Related Topics: API objects, reconciliation loops, and kubectl fundamentals.
Foundations
Imperative means you issue a command that performs an action immediately. kubectl create deployment web --image=nginx creates a Deployment right now.
Declarative means you describe the end state in a file and hand it to the cluster. kubectl apply -f web.yaml says "make the cluster look like this."
The difference sounds academic, but it changes everything about how you operate a cluster.
An imperative command is a verb. It runs once and forgets. Run it twice and the second call errors because the object already exists.
A declarative file is a noun. It is a description you can apply again and again, and the result is always the same object in the same state. This property is idempotence.
Because the file is just data, you can commit it to Git, review it in a pull request, and diff two versions. Your infrastructure becomes code with history.
Mechanics & Interactions
When you run kubectl apply, kubectl does not blindly overwrite the object. It computes a diff between what you submitted and what exists, then patches only the differences.
Classic client-side apply stored your last submission in a kubectl.kubernetes.io/last-applied-configuration annotation and used it as the base for a three-way merge (last-applied, live, and your new file).
Modern clusters use server-side apply. The API server tracks which "manager" owns each field, so multiple actors can safely own different parts of the same object without stomping each other.
Consider editing a Deployment three ways.
# Imperative: acts now, no saved record
kubectl scale deploy/web --replicas=5
# Imperative edit: opens the live object in an editor
kubectl edit deploy/web
# Declarative: change the file, then apply the whole intent
kubectl apply -f web.yamlThe first two change the live object but drift from any file you keep. Your Git copy still says 3 replicas while the cluster runs 5.
The third keeps the file as truth. Next apply, kubectl sees the file and reconciles the object to match it, healing drift instead of causing it.
This matters because controllers never stop. If you scale imperatively but a GitOps tool re-applies your 3-replica file, it will scale you back down. The declarative source wins.
Advanced Considerations & Applications
Declarative configuration is the foundation of GitOps. Tools like Argo CD and Flux watch a Git repository and continuously apply its manifests, so the repo is the desired state and the cluster reconciles toward it.
This gives you an audit trail, easy rollback (revert the commit), and drift detection out of the box. None of that is possible if humans run imperative commands.
Server-side apply also solves multi-writer problems. An HPA can own spec.replicas while your manifest owns the image and probes, and neither overwrites the other's field.
To make this work, omit fields you do not manage. If your YAML hardcodes replicas while an HPA also manages it, you create a fight; drop replicas from the file and let the HPA own it.
Imperative commands still earn their place. They are excellent for exploration, and for generating YAML you then commit.
kubectl create deployment web --image=nginx \
--dry-run=client -o yaml > web.yamlThat produces a starter manifest without touching the cluster, blending the speed of imperative with the durability of declarative. Use imperative to explore and scaffold, declarative to deploy and keep.
Common Misconceptions
"apply and create do the same thing." No. create fails if the object exists; apply creates or updates and is idempotent.
"Declarative is slower." Only to type the first time. It is far faster over the life of a system because it is repeatable and reviewable.
"kubectl edit is declarative because I see YAML." It edits the live object directly and leaves no file, so it causes the same drift as any imperative change.
"apply always fully overwrites the object." It merges. With server-side apply, it only changes fields your manager owns, respecting other owners.
"I can mix free-form imperative scaling with GitOps." Not safely. A reconciler will revert your manual change on its next sync.
FAQs
When should I ever use imperative commands?
For debugging, one-off tasks, and generating YAML with --dry-run=client -o yaml. Keep them out of anything you deploy repeatedly.
What is the difference between apply and replace?
apply does a merge patch and preserves fields it does not manage. replace overwrites the whole object and can drop fields you did not include.
Why did my apply not remove a field I deleted?
With server-side apply, a field is only removed when the manager that owns it stops setting it. Check field ownership with kubectl get -o yaml and the managedFields section.
Does apply cause downtime? Not by itself. It records intent; the relevant controller performs any rollout gradually using your probes and update strategy.
How does this relate to Helm and Kustomize? Both generate declarative manifests that are ultimately applied. They are templating and composition layers on top of the same desired-state model.
Related
- What Kubernetes Is and Why It Exists
- kubectl Essentials
- API Objects & Reconciliation
- Kubernetes Basics
- Kubernetes Fundamentals Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).