Infrastructure as code — study guide
The concept's fragments, read in order.
Infrastructure you can commit
Infrastructure as code describes your servers, networks, and services in files, and hands those files to a tool that builds the real thing for you. The alternative is the console: clicking through a web dashboard to create a virtual machine, wiring up a network by hand, remembering which checkbox you toggled at two in the morning six months ago. Infrastructure as code replaces that with a written definition — machine-readable configuration files instead of a graphical interface and human memory.
The payoff is reproducibility. A hand-configured box is a one-off: nobody can say for certain how it was built, so nobody can rebuild it exactly, and everyone is a little afraid to touch it. A definition file is the opposite. It can be versioned, reused, and shared, and applying it a second time produces the same setup as the first, on a fresh machine that has never seen a mouse.
The rest of this concept is what that shift buys you: describing the end state instead of the steps, letting the tool compare and converge reality onto your declaration, safe re-runs, the review workflow your infrastructure inherits from application code, how the pieces get declared, and what happens when someone changes the real world behind the file's back.
Say what you want, not every step
There are two ways to tell a machine to build something, and the difference is the heart of infrastructure as code. The imperative way is a script: an explicit, ordered list of steps you write out yourself — create this, then attach that, then configure the other — where you own every command and its sequence. The declarative way inverts it. You describe the desired end state, the finished arrangement you want to exist, and the tool works out the steps needed to reach it, like ordering a dish by name - you state the result you want and the kitchen decides every cooking step.
Declarative is the approach mainstream infrastructure-as-code tools take. You state that you want a network, a virtual machine attached to it, and a firewall rule, and the tool figures out the order to create them and how to get there from wherever things currently stand. You are not writing the procedure; you are writing the destination.
That inversion is what makes the rest of infrastructure as code possible. A description of the end state is something a tool can compare against reality, over and over, without you re-deriving the steps each time — which is exactly what the next idea, reconciliation, depends on.
Compare, plan, converge
A declarative definition is a statement of desired state: this is what the infrastructure should look like. To make that real, the tool runs a loop with three moves. First it compares the desired state in your files against the actual state of the running infrastructure. Then it produces a plan — the diff between the two, the precise list of what must be created, changed, or destroyed to close the gap. Then, on apply, it carries out that plan so the actual world converges onto the declaration, like a thermostat - you set the target and it keeps acting to close the gap between the current and the desired temperature.
The plan is the safety rail. Because it is generated before anything changes, you get to read exactly what the tool intends to do before you let it act, and an apply that produces an empty plan tells you reality already matches — nothing to do. Comparing first and acting second is what separates this from a blind script that just runs its commands and hopes.
The same loop works from any starting point. Whether the infrastructure is empty, half-built, or already close to correct, the tool computes the diff from where things are to where the declaration says they should be, and moves only what needs moving.
Safe to run again
Idempotence is the property that applying the same configuration repeatedly yields the same end state. Run it once and the tool builds what the declaration describes; run the identical configuration again and, if reality already matches, it does nothing at all. The second apply is not a second build — it is a check that finds nothing to change.
The classic example is a declaration that says a particular piece of software should be installed at a particular version. The first apply installs it. The second apply looks, sees the version is already present, and moves on without touching it. The instruction is not "install this" but "this should be installed," and a thing that is already true needs no action.
This is what makes an apply safe to re-run, and it falls straight out of comparing desired state to actual state before acting. You never have to wonder whether running the tool one more time will pile a duplicate on top of what exists; matching reality is a no-op, so the honest answer to "is it safe to apply again?" is always yes.
Infrastructure gets the same workflow as code
Once the infrastructure is files, it lives wherever your other files live: in version control, under the same commit-and-review workflow used for application code. Every change to the infrastructure becomes a commit with a message and an author, so the repository holds a full, ordered history of how the environment got to be the way it is — who changed what, when, and why.
That history hands you three things a console never could. Review: a change to production infrastructure can be proposed, read, and approved by another person before it is applied, the same way a code change is. History: you can see exactly what the declaration said at any past point. Rollback: if a change goes wrong, returning to a known-good state is reverting to an earlier version of the file and applying it, not a frantic reconstruction from memory.
None of this is a new tool to learn if you already track application code this way. The infrastructure simply joins it. The change that used to be an untracked click in a dashboard is now a reviewable commit sitting beside the code it supports.
Declaring the pieces
The things you declare are resources: individual units of infrastructure, each a typed piece the tool knows how to manage. A virtual machine on your hypervisor is a resource. So is a network it attaches to, and so is a container image built with podman build. Each resource has a type and a set of settings, and your declaration is really a collection of these pieces and how they connect.
Providers are what make a given kind of resource manageable. A provider is the plugin that knows how to talk to a particular system — a cloud, a hypervisor, a container tool — and how to create, read, update, and delete the resource types that system offers. When your declaration asks for a virtual machine, the relevant provider is the thing that actually knows the API calls to bring one into existence and to check whether it already exists.
So a definition file is layered: resources describe what you want, providers supply the how for each type. You write the what, in the tool's declarative language, and lean on the provider to translate it into the specific actions a real network or a real podman-built image requires.
When reality wanders off
Drift is the gap that opens when the running infrastructure no longer matches the declaration in your files. It happens when someone changes the real world out of band — an engineer opens the console during an incident and edits a resource by hand, or bumps a setting directly through a command line, without touching the code. The declaration still says one thing; the actual infrastructure now says another, and the two have quietly diverged.
An infrastructure-as-code tool handles drift with the same compare-first loop it uses for everything. It queries what actually exists, sets that against what the files declare, and surfaces the difference. Because the declaration is the source of truth, the next apply plans to undo the manual change and reconcile the real world back to what the code says.
This is the reason the discipline is to stop hand-editing resources the tool manages. Every out-of-band change is a lie waiting to be caught and reverted, and the moment you route changes back through the file, the gap closes and stays closed.
Rebuild it from the file
The whole point lands when something has to be rebuilt. A hand-configured server is a hand-tuned box nobody dares touch: if it dies, or you need a second one, you are reconstructing months of forgotten clicks from memory and hoping you got them all. A server described in code is the opposite. Its files are a complete recipe, so you can stand up an identical environment from scratch — on new hardware, in a fresh account, as many copies as you want — like rebuilding a structure identically from its blueprint, rather than losing a one-off that can never be reproduced.
For a homelab or a small server environment this is the difference between a fragile pet and something you actually control. The virtual machines on your hypervisor, their networks, the images they run, all live in version-controlled files, so recovering from a wiped disk or a bad experiment is applying the declaration again rather than starting over. The environment stops being precious because it is reproducible.
That reproducibility is also the foundation the next layer builds on: the declarative, desired-state idea generalizes from describing one environment to keeping a whole fleet of running services in their desired state, which is where orchestration picks up. It is the same instinct — say what should be true, and let a tool keep it true — scaled past a single set of files.