DEVOPS-05-003-P · Primer — Idempotency in Provisioning
The idea in one sentence
An operation is idempotent when running it many times leaves the system in the same state as running it once, so you can safely re-apply your infrastructure definition without piling up duplicate or broken results.
Why it exists
Real automation runs in an unreliable world. Networks drop, a terraform apply gets interrupted halfway, a CI job retries after a timeout, or two engineers apply the same config an hour apart. If each run blindly executed steps — "create a server," "add a firewall rule" — a second run would create a second server and a duplicate rule. Chaos.
The reason declarative Infrastructure as Code (IaC) feels safe is that it is built on idempotency. You describe the desired end state ("one server, one firewall rule with these ports"), and the tool figures out the minimal difference between what exists and what you asked for. Run it once: it creates them. Run it again with no changes: it does nothing. That property is exactly what lets a retry, a scheduled drift-correction, or a nervous re-run be harmless.
This isn't a new invention — the same idea underpins the web. RFC 9110 (HTTP Semantics) defines a method as idempotent when "the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request," which is why PUT and DELETE are safe to retry but POST (create-a-new-thing) is not. IaC borrows this discipline for whole cloud environments.
How it works
The mechanism is read, compare, converge — not blind execution:
- Declare desired state. You write what you want, not how to get there:
state: present,count = 1, "these three ports open." - Read current state. The tool inspects reality — Terraform reads its state file and the cloud API; Ansible queries the target host to see what's actually installed or running.
- Compute the diff. It compares desired vs. current.
terraform planshows you this diff before touching anything. - Converge. It applies only the difference. Nothing to change? It reports
changed: falseand exits. This is a convergent operation: repeated runs steer the system toward the same fixed point and then stop.
That's why the second run is the real test. In Ansible, the convention is to run a playbook twice — a healthy, idempotent task shows changed: false on the second pass. In Terraform, a second apply on unchanged config yields "No changes."
Note that idempotency is about effect on state, not about identical responses. RFC 9110 is explicit here: deleting a resource by ID may return 200 OK the first time and 404 Not Found the next — the state (resource is gone) is identical, so DELETE is still idempotent. The server may also log every request separately without breaking idempotency.
The one gotcha
"I ran it, so it happened exactly once" is a lie your retries tell you. In distributed systems you almost always get at-least-once delivery, not exactly-once. A create request can succeed on the cloud side, but the acknowledgement gets lost, so your tool retries — and you've now made the same call twice.
Idempotency is the shield: if the operation is idempotent, at-least-once delivery is fine, because the duplicate does no extra harm. The danger zone is non-idempotent operations sneaking into declarative flows. The classic offender is Ansible's command/shell (and Terraform local-exec/remote-exec provisioners): the tool can't understand an arbitrary command, so it re-runs it every time and reports changed on every pass. Running useradd bob twice fails the second time; appending a line to a file twice duplicates the line.
The fix is to make the effect idempotent even when the call repeats: prefer a purpose-built module that checks state first; or guard the command with creates/removes and changed_when; or, for APIs you build, attach an idempotency key so the server recognizes and de-duplicates a retried request. Never assume "exactly once" — design so that "at least once" is safe.
See it — read, compare, converge on every run
flowchart TD
A["Same definition<br/>(desired state)"] --> B["Read current state<br/>from cloud / host"]
B --> C{"Diff:<br/>desired vs current?"}
C -->|"No difference"| D["Do nothing<br/>(changed: false)"]
C -->|"Difference found"| E["Apply only the delta"]
E --> F["System now matches<br/>desired state"]
D --> F
F -->|"Re-run is safe"| A
The one-line takeaway
Idempotent provisioning means "describe the end state, then converge to it" — so a re-run is a no-op, and at-least-once retries can never corrupt your infrastructure.
Where you'll meet it
You will lean on this every time you re-run terraform apply after fixing a typo, let a CI pipeline retry a flaky deploy, schedule Ansible to correct configuration drift nightly, or wire an idempotency key into a create endpoint so a client's retry doesn't double-charge. Any time a "safe re-run" saves you from a 2 a.m. cleanup, idempotency is why.
Named concepts: idempotency, convergent operations, safe re-runs, at-least-once vs exactly-once effects
