Infrastructure as code means defining servers, networks, databases and permissions in files that are versioned, reviewed and applied by a tool, rather than created by clicking in a console.
The benefit most often cited is automation. That is the least interesting one.
What you actually get
Reviewability. A change to a firewall rule arrives as a pull request someone else reads. Console changes are invisible until something breaks.
An audit trail that is real. Version history shows who changed what and when, with the reasoning in the commit message and the review. Cloud audit logs tell you an API call happened; they rarely tell you why.
Reproducibility. A second environment is a parameter change rather than a week of clicking. This is what makes realistic staging environments and disaster recovery practical.
Drift detection. The tool can tell you when reality no longer matches the definition, which is how you discover the manual change someone made during an incident and forgot.
Knowledge that outlives people. The configuration is the documentation, and it cannot be out of date in the way a wiki page can.
The costs nobody mentions
It is slower at first. Creating a resource in the console takes two minutes. Writing, reviewing and applying the code takes an hour. The payback comes on the fifth change, not the first.
State management is genuinely fiddly. Declarative tools keep a state file mapping definitions to real resources. Corrupted state, concurrent applies and resources deleted outside the tool all cause real problems, and recovering from them requires understanding the model rather than just the syntax.
A new category of outage. A bad apply can delete a production database. Plans that look innocuous sometimes propose destroy-and-recreate for an in-place change.
Abstraction creep. Teams build module hierarchies four layers deep, and nobody can work out what a change will actually do. Flat and slightly repetitive beats clever and opaque.
Declarative versus imperative
Declarative tools describe the desired end state and work out the steps. This is the dominant model and the right default: it handles drift, supports planning, and makes the definition readable as a description of the system.
Imperative approaches — scripts that create things in sequence — are easier to reason about for one-off tasks and much worse at maintaining state over time. They have a place in bootstrapping and in gluing steps together, not in managing an estate.
Adopting it on an existing estate
Few teams start from nothing. A workable sequence:
- New resources only, from today. Everything created from now on is defined in code. This stops the problem growing without requiring a migration project.
- Import the critical path. Networking, identity and the production database — the things whose accidental change would hurt most.
- Build a new environment entirely from code. Usually staging. This proves the definitions are complete, because anything missing simply will not exist.
- Import the rest opportunistically, when a resource needs changing anyway.
- Lock the console. Remove write permissions for humans in production, with a documented break-glass path for emergencies. Until this happens, drift continues.
Expect this to take a quarter or two alongside other work. A big-bang import of an entire estate rarely completes.
Practices that keep it sane
Separate state per environment. One state file for everything means a staging change can break production.
Plan in CI, apply deliberately. Every pull request shows the plan output so reviewers see what will change. Apply happens on merge, or with an explicit approval for production.
Pin versions. Tool and provider versions pinned, upgraded intentionally. Provider upgrades change behaviour in ways that surprise you during an urgent change.
Keep modules shallow. Two levels at most. The purpose is reuse of genuinely common patterns, not a framework.
Never put secrets in the code. Reference a secret manager. State files frequently contain resolved values and should be treated as sensitive regardless.
Where it fits with everything else
Infrastructure as code is a prerequisite for several things that are hard without it. Reproducible environments make disaster recovery testable rather than theoretical. Automated provisioning is what makes running many services feasible. And enforced tagging through code is what makes cost allocation possible at all.
It is also one of the easiest wins during a migration, when everything is being created anyway. Retrofitting later costs several times as much.
Common mistakes
Managing resources in code that something else also manages — a Kubernetes operator, an auto-scaler, a managed service’s own controls — produces a fight where each system reverts the other. Decide the owner for each resource and mark the rest as ignored.
Applying from laptops. Everyone has different tool versions, different credentials and no audit trail. Apply from CI.
Treating the state file casually. Back it up, version it, lock it during apply, and restrict access to it. It is as sensitive as any credential store.
Choosing a tool
The main choice is between a general-purpose declarative tool, a cloud provider’s native tool, and defining infrastructure in a general-purpose programming language.
General-purpose declarative tools work across providers, have the largest ecosystem and the deepest pool of people who know them. The configuration language is limited by design, which is frustrating occasionally and protective often.
Provider-native tools integrate tightly with one cloud, support new services on day one, and keep state managed for you. The trade is that the definitions do not travel.
Programming-language approaches give you loops, conditionals, types and real testing. They also give you the ability to write infrastructure code complex enough that nobody can predict what it does, which is a genuine risk in a shared codebase.
For most teams the general-purpose declarative tool is the safe default, chiefly because hiring and documentation are easiest. Pick one and standardise; the worst outcome is three tools managing adjacent parts of one estate.
Testing infrastructure code
Infrastructure code can be tested, and few teams do it. Four levels, in increasing cost.
- Static analysis. Linting and security scanning of the definitions themselves — catches open security groups, unencrypted storage, overly broad permissions, before anything is created.
- Plan review in CI. The diff of what would change, visible on every pull request. The cheapest and most valuable control available.
- Policy as code. Automated rules that reject a plan violating organisational standards, such as a publicly accessible database.
- Ephemeral environment tests. Actually create the infrastructure in a throwaway account, assert it works, destroy it. Slow and the only way to catch certain classes of problem.
The first two cost almost nothing to set up and prevent most of the incidents teams attribute to infrastructure as code being risky.
Frequently asked questions
Is infrastructure as code worth it for a small team?
Yes, and arguably more so, because a small team cannot afford the time lost rebuilding environments by hand or diagnosing drift. Start with new resources rather than a migration.
Can I use it with multiple cloud providers?
Yes. Most tools support multiple providers through plugins, though the resource definitions remain provider-specific. It reduces the cost of operating two platforms without making workloads portable.
What about configuration inside servers?
That is configuration management, a related but separate concern. The modern pattern is to build immutable images and replace instances rather than configuring running servers.
How do I handle manual emergency changes?
Allow them with a documented break-glass procedure, then reconcile the code within a defined window. Pretending they never happen is how drift accumulates invisibly.
