Declarative vs Imperative
Why IaC settled that you should put infrastructure in code. This page settles the deeper question: how should that code be written? There are two fundamentally different styles, and the choice between them shapes everything about how robust your infrastructure is.
Two ways to ask for the same thing
Section titled “Two ways to ask for the same thing”Imagine you want exactly three web servers running. You can express that as a recipe of steps, or as a statement of the desired result.
IMPERATIVE (how) DECLARATIVE (what) ───────────────────────────── ───────────────────────────── 1. check how many servers exist desired_state: 2. if fewer than 3, create some web_servers = 3 3. if more than 3, delete some 4. configure each new one "Make it so. I don't care how." 5. handle the half-created one…Imperative code is a sequence of commands: do this, then this, then this. A bash script that runs
create-instance three times is imperative. You are responsible for the logic — counting what exists,
deciding what to add or remove, and handling every edge case.
Declarative code describes the desired end-state: “there should be three web servers.” You don’t write the steps. A tool figures out the difference between what exists and what you asked for, then does whatever is necessary to close the gap.
Why imperative scripts rot
Section titled “Why imperative scripts rot”An imperative provisioning script is brittle for a reason that compounds over time: it has to be correct for every possible starting state. Run it on an empty account and it creates three servers — fine. But run the same script when two servers already exist, and a naive “create three” makes five. Run it after a server was manually deleted, and you must detect the gap. Run it and have it die halfway, and now you’re in a partial state no branch of the script anticipated.
You end up writing ever more defensive logic — check-before-create, idempotency guards, cleanup handlers — until the script is mostly bookkeeping about current reality and barely about your intent. That bookkeeping is exactly the error-prone, manual reasoning IaC was supposed to remove. The script is the toil, just relocated into code.
The declarative answer: state your intent, let a reconciler converge
Section titled “The declarative answer: state your intent, let a reconciler converge”Declarative tools solve this by separating two concerns:
- You declare the desired state (the what).
- The tool computes the difference from current reality and applies only the changes needed (the how).
That second component — the thing that reads desired state, observes actual state, computes the delta,
and acts to close it — is a reconciler. And you have met it before. This is precisely the
Kubernetes reconciliation loop: you write a Deployment
saying replicas: 3, and a controller continuously works to make three Pods exist, creating or deleting
as needed. You never script “start a pod.” You state the count.
┌──────────────────────────────────────────────┐ │ RECONCILER (the loop) │ │ │ desired ─┼──► observe actual ──► compute diff ──► act ──┐ state │ ▲ │ │ (code) │ └────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘ reality now matches desiredThis is why the book keeps returning to the control loop — it’s the same robust pattern under Kubernetes, GitOps, and Terraform alike.
Why declarative is more robust
Section titled “Why declarative is more robust”The reconciler model is more robust precisely because of the property that made imperative scripts rot: it is idempotent and convergent.
- Idempotent — running it once and running it ten times produce the same result. The first run creates the three servers; every run after that observes “already three, nothing to do.” There’s no “run it twice and get six.”
- Convergent — it always drives toward the declared state from whatever the current state happens to be. Empty account, partial account, drifted account — the reconciler computes the right delta and closes it. You never have to enumerate the starting conditions, because the tool measures reality instead of assuming it.
| Property | Imperative | Declarative |
|---|---|---|
| You specify | The steps | The end-state |
| Edge cases | Your job | The tool’s job |
| Run twice | Risky (may duplicate) | Safe (idempotent) |
| Recovers from drift | Only if you coded it | Built in (convergent) |
| Reviewing the code shows | A procedure | The intended world |
That last row matters for review: a declarative file is a description of the infrastructure you want, so reading it tells you the truth about your intent. An imperative script only tells you a procedure — you have to mentally execute it to know what it builds.
Not everything is purely declarative — configuration management tools like Ansible sit closer to imperative, and even Terraform has occasional imperative escape hatches. But the default and the goal is declarative: say what you want, let the reconciler converge. The next page puts a concrete tool behind the idea: Terraform.
The architect’s lens
Section titled “The architect’s lens”Five questions for the declarative-vs-imperative choice itself:
- Why does it exist? Because an imperative provisioning script has to be correct for every possible starting state, and that defensive bookkeeping (check-before-create, half-built handling) becomes the very toil IaC was meant to remove.
- What problem does it solve? Brittleness from assuming reality: you declare the end-state and a reconciler measures actual state, computes the delta, and closes it — the same loop running under Kubernetes, GitOps, and Terraform.
- What are the trade-offs? Idempotence and convergence make it robust, but you give up fine-grained control of how; not everything maps to a clean desired state, which is why Ansible and the occasional Terraform escape hatch stay closer to imperative.
- When should I avoid it? Genuinely one-shot, order-specific operations — a data migration, a manual cutover — where there’s no standing desired state to converge toward.
- What breaks if I remove it? You return to scripts that duplicate resources when run twice, can’t recover from drift unless you coded for it, and read as a procedure you must mentally execute rather than a description of the world you want.
Check your understanding
Section titled “Check your understanding”- Express “I want three web servers” both imperatively and declaratively. What is the core difference in what you are responsible for?
- Why does an imperative provisioning script become more brittle over time? Give a concrete starting state that breaks a naive “create three servers” script.
- What is a reconciler, and where in this book have you already seen one in action? Describe the loop.
- Define idempotent and convergent, and explain how each property makes declarative IaC more robust than an imperative script.
- Using the book’s thread, name the specific manual reasoning that declarative IaC removes, and why that makes production safer.
Show answers
- Imperative: a recipe of steps — check how many servers exist, create or delete to reach three, handle
each edge case. Declarative: a statement of the result —
web_servers = 3, “make it so.” The core difference is responsibility: imperatively you own counting state and handling edge cases; declaratively the tool does. - An imperative script must be correct for every possible starting state, and that set keeps growing. A concrete breaker: run a naive “create three servers” script when two already exist and you get five — it assumed an empty account instead of measuring reality.
- A reconciler reads desired state, observes actual state, computes the delta, and acts to close it — on
a loop. You’ve already seen it in the Kubernetes reconciliation loop:
you declare
replicas: 3and a controller continuously creates or deletes Pods until exactly three exist. - Idempotent — running it once or ten times yields the same result (no “run twice, get six”). Convergent — it drives toward the declared state from whatever the current state is, empty or drifted. Together they make declarative IaC robust because the tool measures reality each run instead of assuming a starting state.
- It removes the reasoning about current state — the check-before-create, edge-case handling, and “what if it’s half-built” logic. Production is safer because the system self-corrects toward your declared intent every run, rather than depending on a script being correct for a starting condition nobody verified.