Leer en español · Lire en français · Auf Deutsch lesen
Section 10.4 of 10.8 · Advanced · 24 min read
Infrastructure as code and immutable infrastructure
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
Clicking through a cloud console isn't reviewed, isn't recorded and can't be repeated. In this lesson you'll see how infrastructure becomes code: declared, planned, reviewed, applied, and kept honest.
Imagine furnishing a hotel with two hundred identical rooms. You could tell each decorator what to do, and hope they remember. Or you could write one precise specification for a room, keep it in a binder, and have every change approved in writing. Then any room can be checked against it, a damaged room can be rebuilt exactly, and a new wing fitted out in days. Infrastructure as code is that specification for the computers, networks and databases a system runs on.
Infrastructure as code, or IaC, means defining networks, machines, databases and permissions in files kept in version control. An imperative script lists the steps: create a network, then a subnet, then a database. Run it twice, and it tries to create everything twice. A declarative tool describes the end state instead, and works out the steps by comparing that description with what exists. So applying it twice changes nothing the second time. That property, idempotence, is what makes infrastructure code safe to run again and again, in automation. Terraform became the most widely used multi-cloud tool. In 2023 HashiCorp moved it from an open-source licence to the Business Source License, and the community forked it as OpenTofu. The lesson: a tool's licence and governance are part of its risk.
Here's how a tool such as Terraform or OpenTofu works. The code, kept in Git, says what should exist. The state file maps each resource in the code to the real object the tool created. It must be remote, locked during changes, versioned, and treated as a secret, because it often holds passwords in plain text. The plan compares the code, the state and the real infrastructure, read from the provider's API. It shows what would change before anything happens, and reviewers read it in the pull request. Only after review is it applied. Apply changes the real infrastructure and records the result in the state. But if someone changes something by hand in the console, the real infrastructure no longer matches the code. That's drift, and the next plan will try to undo it.
Here's drift in practice. During a Friday night incident, an engineer opens the production orders database's firewall to a contractor's IP address, through the console. Nobody updates the code. On Saturday, the nightly drift job runs a plan for every production state. It reports one change, for that firewall, and posts it to the platform team. The plan shows the extra address, and the cloud audit log shows who added it, when, and for which incident. The access was temporary and the incident is over, so the code is right and reality is wrong. Applying the plan removes the rule. Had the change been needed for good, it would go into the code through a reviewed pull request. Afterwards, console write access in production becomes an emergency role that needs a reason and raises an alert. The rule of thumb: any difference is codified or reverted by the next working day.
Reusable modules hide provider details behind a small interface, such as name, size and high availability. Staging and production use the same versioned modules with different inputs, never copied code, which drifts apart within months. Immutable infrastructure means never changing a running server. Every change, even an operating system patch, is a new image that replaces the old instances. There's no drift, and rollback is redeploying the previous image. And policy as code turns rules, such as no public storage buckets and encrypted databases, into automatic checks on every plan, explained in the pull request.
GitOps applies the reconciliation loop of Section ten point two to whole environments. A developer pushes code, and the pipeline tests it, builds an image and pushes it to a registry. The pipeline then commits the new image digest to a configuration repository, which holds the desired state. Changes to it are reviewed like any code. Inside the cluster, an agent such as Argo CD or Flux pulls that repository, applies it, and keeps correcting drift. A manual edit lasts only until the next sync. Because the agent pulls, the pipeline needs no credentials for production. Git becomes the audit log, and rolling back is a reviewed revert.
The full section compares the tools, shows how to test infrastructure code, and ends with a checklist for production. Next, in Section ten point five, we follow a change from commit to production. See you there.
What you need to know
- Infrastructure as code defines infrastructure in version-controlled files applied by tools, making environments reproducible, reviewable, auditable and quick to create.
- Declarative tools describe the desired end state and compute the steps, which makes repeated runs idempotent; imperative scripts list steps and are hard to rerun safely.
- HashiCorp moved Terraform to the Business Source License in August 2023; the OpenTofu fork followed under the Linux Foundation (stable in January 2024, a CNCF sandbox project since 2025); CloudFormation, Bicep, Pulumi and Crossplane are the main alternatives.
- State maps code to real resources; it must be remote, locked, versioned, access-controlled and split to limit blast radius, and drift must be detected by scheduled plans and then codified or reverted.
- Versioned modules with small interfaces, reused across environments with different inputs, prevent copy-and-paste environments from diverging.
- Immutable infrastructure replaces servers and containers from new images instead of changing them in place; configuration management remains useful for long-lived estates and for building images.
- GitOps keeps desired state in Git and lets in-cluster agents (Argo CD, Flux) pull and continuously reconcile it; policy as code and layered tests make changes safe.
What infrastructure as code is
Infrastructure as code (IaC) means defining networks, virtual machines, clusters, databases, queues, DNS records, permissions and every other piece of infrastructure in text files kept in version control, and letting tools create and change the real thing from those files. Instead of an engineer clicking through a cloud console, a change is a commit: reviewed in a pull request, tested in a pipeline, applied automatically and recorded for ever.
The idea is older than the public cloud. Mark Burgess's CFEngine (1993) managed Unix machines from declarative policies; Puppet (2005), Chef (2009) and Ansible (2012) brought configuration management to the mainstream. The cloud made it essential, because infrastructure itself became available through APIs: AWS CloudFormation (2011) and HashiCorp's Terraform (2014) describe whole environments, not just the software on servers. Kief Morris's book Infrastructure as Code, now in its third edition (2025), is the standard account of the practice and its principles.
The benefits are those of software engineering applied to infrastructure. Environments become reproducible: staging can be built from the same definitions as production, and a region can be rebuilt after a disaster from code rather than from memory. Changes become reviewable and auditable, which regulators and security teams value. Knowledge stops living in one engineer's head. And speed rises, because creating an environment for a new food-delivery service becomes a pull request rather than a ticket that waits two weeks.
Declarative versus imperative
An imperative approach lists the steps: create a network, then a subnet, then a database, then open this port. Running the script twice tries to create everything twice. A declarative approach describes the end state, the desired state of Section 10.2: a network with these subnets, a database of this size, a firewall allowing this port. The tool compares the description with what exists and works out the steps, so applying it twice changes nothing the second time. That property, idempotence, is what makes infrastructure code safe to run repeatedly and in automation.
Most mature tools are declarative at their core, but they differ in the language used to write the declaration and in who keeps track of what exists. The table summarises the main families.
Approaches to infrastructure code
| Approach | You write | Examples | Strengths | Weaknesses |
|---|---|---|---|---|
| Imperative scripts | Sequences of CLI or SDK calls | Shell scripts calling cloud CLIs, SDK programs | Familiar, flexible, quick for one-off tasks | Not idempotent without great care; no record of what exists; hard to change safely |
| Declarative, domain-specific language | Resources and their relationships in a configuration language | Terraform and OpenTofu (HCL), AWS CloudFormation (YAML or JSON), Azure Bicep, Kubernetes manifests | Readable, reviewable plans; idempotent; large provider ecosystems | Loops, conditions and abstraction can be awkward; a language to learn |
| Declarative model, general-purpose language | Code in TypeScript, Python, Go, C# or Java that builds a declarative model | Pulumi, AWS Cloud Development Kit (CDK) | Real abstractions, testing tools and editors; familiar to developers | Easy to write clever, hard-to-review infrastructure; the plan is less visible in the code |
| Continuous reconciliation | Kubernetes-style resources describing cloud infrastructure | Crossplane, cloud providers' Kubernetes controllers | Same reconciliation loop and tooling as applications; drift corrected continuously | Needs a Kubernetes control plane; debugging spans two systems |
| Configuration management | Desired configuration of software on existing servers | Ansible, Chef, Puppet | Manages long-lived machines, network devices and on-premises estates | Servers accumulate history and drift; slower and less predictable than replacing them |
The tools and the OpenTofu fork
Terraform became the most widely used multi-cloud tool because one language and workflow covers thousands of providers: AWS, Azure, Google Cloud, Cloudflare, Kubernetes, GitHub, monitoring services and more. On 10 August 2023 HashiCorp moved Terraform from the open-source Mozilla Public License 2.0 to the Business Source License 1.1, which permits internal and commercial use but forbids offering it in products that compete with HashiCorp's own. Version 1.5.7 was the last release under the old licence.
The community's response was a fork. A manifesto calling for a return to open source appeared within days; in September 2023 the Linux Foundation accepted the fork as OpenTofu; its first stable release, 1.6.0, followed in January 2024; and in 2025 it was accepted into the CNCF as a sandbox project. IBM completed its acquisition of HashiCorp in February 2025. The two tools remain largely compatible in language and providers but are diverging in features: OpenTofu added client-side encryption of state, for example, while Terraform's commercial platform added its own capabilities. For an architect, the episode is a reminder that a tool's licence and governance are part of its risk profile, and that open standards and a credible alternative keep that risk manageable.
Among the single-cloud tools, AWS CloudFormation (2011) and Microsoft's Bicep (introduced in 2020, compiling to Azure Resource Manager templates) are run by the provider, which keeps track of what exists, so there is no state file to manage, at the price of being tied to one cloud. Pulumi (2018) and the AWS CDK let developers write infrastructure in general-purpose languages; Crossplane extends Kubernetes to manage cloud resources by reconciliation. The right choice depends on which clouds you use, your team's skills, the maturity of the providers you need, licence policy and what the rest of the organisation already runs; consistency across teams is usually worth more than the marginal merits of any one tool.
terraform {
required_version = ">= 1.6"
backend "gcs" { # remote state; this backend locks natively
bucket = "fd-infra-state"
prefix = "orders/prod"
}
}
variable "environment" { type = string }
module "orders_db" {
source = "git::https://git.example.com/modules/postgres.git?ref=v3.2.0"
name = "orders-${var.environment}"
size = var.environment == "prod" ? "large" : "small"
high_avail = var.environment == "prod" # standby in a second zone
backup_days = 14
}
output "orders_db_host" {
value = module.orders_db.host
}Running tofu plan (or terraform plan) shows what would change before anything happens. The plan is the most important artefact in the workflow: reviewers read it in the pull request, alongside the code, and it is what gets applied.
# module.orders_db.google_sql_database_instance.this will be updated in-place
~ resource "google_sql_database_instance" "this" {
~ settings {
~ tier = "db-custom-2-8192" -> "db-custom-4-16384"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.State, locking and drift
Declarative tools need to know which real resources belong to which definitions. Terraform and OpenTofu keep this in state: a file mapping each resource in the code to the identifier of the real object, with its last known attributes. Planning compares three things: the code (what should exist), the state (what the tool created) and the real infrastructure, read from the provider's API (what actually exists).
Plan, apply, state and drift
State needs care because it is critical and sensitive. It must be stored remotely (in object storage or a managed service), not on a laptop, so that the whole team and the pipeline share one version. It must be locked during changes, so that two applies cannot interleave and corrupt it. It must be backed up and versioned, because a lost state file leaves the tool unaware of resources it created. And it is a secret: state often contains database passwords and keys in plain text, so access to it must be restricted, and encrypted at rest (OpenTofu can also encrypt it on the client side).
Drift is any difference between the code and reality, usually from manual changes made in a hurry, from another tool touching the same resource, or from a partial failure during apply. Drift is dangerous because the next apply may silently revert a change someone depended on, or fail in production on a difference nobody knew existed. Teams detect it by running a plan on a schedule (for example nightly) and alerting on any proposed change, and prevent it by restricting console write access to a recorded break-glass procedure.
State also defines blast radius. One giant state for a whole company means every plan is slow, every apply risks everything, and one lock blocks every team. Splitting state by component and environment (network, shared cluster, each service's resources; production separate from staging) keeps plans fast and failures contained, at the price of passing outputs such as network identifiers between states. Refactoring is supported: moved blocks rename resources without destroying them, and import brings existing hand-made resources under management.
Worked example
Worked example: a drifted firewall rule
During a Friday-night incident, an engineer opens the production orders database's firewall to a contractor's IP address through the cloud console, so that the contractor can help diagnose a slow query. Nobody updates the code.
| Detection | The nightly drift job runs tofu plan for every production state. On Saturday it reports 1 to change for the orders database firewall and posts the plan to the platform team's channel. |
|---|---|
| Diagnosis | The plan shows an extra allowed source address. The cloud audit log identifies who added it, when and from which incident. |
| Decision | The access was temporary and the incident is closed, so the code is right and reality is wrong. Applying the plan removes the rule. Had the change been needed permanently, the fix would be to add it to the code through a reviewed pull request instead. |
| Prevention | Console write access in production is replaced by a break-glass role that requires a reason and alerts on use. A policy rule (see below) rejects any database firewall rule open to the whole internet. |
| What it shows | Drift is not only a tidiness problem: an undocumented hole in a firewall is a security exposure, and without detection the next unrelated apply would have removed it without anyone knowing why the contractor lost access. |
The rule of thumb: reality may differ from the code for minutes during an emergency, but the difference must be either codified or reverted by the next working day.
Modules and environments
A module is a reusable unit of infrastructure code with inputs and outputs: a PostgreSQL database with backups and monitoring, a service's network and load balancer, a standard Kubernetes namespace with its quotas and policies. A good module applies the information hiding of Chapter 3: a small interface (name, size, high availability or not) in front of the many provider-specific details a service team should not have to know. Modules are versioned and released like libraries, and environments refer to a specific version, as the ref=v3.2.0 in the example does, so that a module change reaches production only when someone deliberately upgrades it.
Environments should be built from the same modules with different inputs, not from copied code. A common layout is one directory (or repository) per environment and component, each with its own state, calling shared, versioned modules; a change is promoted by updating the module version or inputs in staging, then in production. Copy-and-paste environments diverge within months, until staging no longer predicts production. Workspaces, which give one configuration several states, suit short-lived copies such as per-branch test environments better than long-lived production and staging.
Beware of over-abstraction. A module with forty boolean inputs that tries to serve every team is as hard to change as the code it replaced. Prefer a few opinionated modules that encode the organisation's standards, which is exactly what a platform team offers as a golden path in Section 10.7.
Immutable infrastructure versus configuration management
Configuration management tools keep long-lived servers in a described state: Ansible connects over SSH and pushes changes without an agent; Chef and Puppet run agents that pull their configuration and converge on it periodically. They work, but every server accumulates history: packages installed during an incident, files edited by hand, updates that half-applied. Martin Fowler called the result a snowflake server, unique and impossible to reproduce, and proposed the phoenix server, regularly burnt down and rebuilt from scratch, as the cure.
Immutable infrastructure, a term popularised by Chad Fowler in 2013, takes the cure to its conclusion: never change a running server. Every change, whether an application release or an operating system patch, is made by building a new image (a container image, or a machine image with a tool such as Packer), testing it and replacing the old instances, typically by a rolling or blue-green deployment. Containers on Kubernetes are immutable by default. The benefits are no drift, identical instances, trivially repeatable environments and rollback by redeploying the previous image; the costs are an image pipeline, the need for disposable, stateless processes (Section 10.3) and slower turnaround for a single urgent change.
Configuration management has not disappeared. It remains the right tool for estates that cannot be replaced wholesale: on-premises servers, network equipment, databases on dedicated hardware, and the base images themselves, which are often built by running Ansible inside the image build. The architectural rule is to push mutability as far down and as early as possible: configure while building images, then treat what runs as read-only.
GitOps
GitOps applies the reconciliation loop of Section 10.2 to whole environments. The term was coined in 2017 by Alexis Richardson of Weaveworks, the company behind the Flux tool. The OpenGitOps project, a CNCF working group, defines four principles: the desired state is declarative; it is versioned and immutable, with a complete history; it is pulled automatically by software agents; and it is continuously reconciled, with agents observing the actual state and applying the desired one. The two leading agents, Argo CD and Flux, are both graduated CNCF projects; Flux continued as a community project after Weaveworks closed in 2024.
A GitOps delivery flow
The pull model has a security advantage over a pipeline that pushes changes into the cluster: the CI system needs no credentials for production, because the agent inside the cluster fetches what it should run. Git becomes the audit log and the interface for change, including for operations: scaling a service or rolling back is a reviewed commit or a revert. Drift is corrected automatically, so a manual kubectl edit lasts only until the next sync.
GitOps brings its own design questions. Secrets must not sit in Git in plain text; teams encrypt them (SOPS, Sealed Secrets) or store references that the External Secrets Operator resolves from a secrets manager. Promotion between environments needs a convention, such as one directory per environment with pull requests that copy a new image digest from staging to production. Automatic drift correction must be paused deliberately during some emergencies, and the repository structure must reflect ownership so that teams can change their own services without touching others'. GitOps is most natural for Kubernetes, but the same principles apply to cloud infrastructure through Crossplane or through pipelines that plan and apply infrastructure code from merged commits.
Policy as code
Policy as code expresses rules about infrastructure as machine-checkable code instead of a wiki page: storage buckets must not be public, databases must be encrypted and backed up, every resource must carry an owner and cost-centre tag, only approved regions may be used, containers must not run as root, production images must come from the company registry. Checks run at several points: on the plan in the pull request (Open Policy Agent with Conftest, Checkov, HashiCorp Sentinel in its commercial products), at admission into a Kubernetes cluster (OPA Gatekeeper, Kyverno) and continuously against running resources (cloud providers' policy services).
Policy as code turns governance from a review meeting into a fast, consistent guardrail that explains itself in the pull request. It works best when policies are few, clearly justified, versioned and tested like any code, when they start in warning mode before they block, and when there is a recorded exception process. Chapter 11 discusses how such automated checks fit into architecture governance (governance in agile organisations).
Testing infrastructure code, from fast to slow
- Static checks on every commitFormatting,
validate, linting (for example TFLint) and security scanners catch syntax errors, deprecated arguments and obvious misconfigurations in seconds. - Policy checks on the planRender the plan as JSON and evaluate it against the organisation's policies; reviewers see violations in the pull request.
- Module tests
terraform testandtofu testrun assertions against a module, with mocked providers for fast unit-style tests or real ones for integration tests. - Integration tests in ephemeral environmentsApply the module or environment to a throwaway account, check that it works (a database accepts connections, a service answers), then destroy it. Tools such as Terratest script this; it is slow and costs money, so reserve it for shared modules.
- Plan review and staged rolloutApply to staging first, then production, from the same versions; a human reads every production plan, especially destroys and replacements.
- Drift detection and restore drillsScheduled plans catch drift; periodically rebuilding an environment from code proves that the code, not memory, is the source of truth.
Checklist: infrastructure code ready for production
Use it when reviewing a team's infrastructure repository or a new platform module.
- Everything is in codeNo production resource exists only because someone clicked; existing hand-made resources have been imported.
- Remote, locked, encrypted stateShared backend with locking, versioning and restricted access; state split by component and environment to limit blast radius.
- Reviewed plansEvery change is a pull request showing the plan; destroys and replacements of stateful resources require explicit approval.
- Versioned modulesEnvironments pin module versions; staging and production use the same modules with different inputs, not copies.
- Policies enforced automaticallyEncryption, network exposure, tagging and region rules are checked on every plan and at cluster admission.
- Secrets kept out of code and GitCredentials come from a secrets manager or workload identity; state access is treated as secret access.
- Tests in the pipelineStatic checks and policy checks on every change; module tests for shared modules; integration tests for critical ones.
- Drift detected and handledScheduled plans alert on differences; console write access is a recorded break-glass procedure.
- Immutable where possibleServers and containers are replaced from images rather than patched in place; configuration happens at image build time.
- Recovery rehearsedAn environment has been rebuilt from code within the agreed recovery time, including data restore (see Section 10.6).
Check your understanding
Why is idempotence important for infrastructure code?
Because automation reruns code often. A declarative, idempotent tool changes nothing when reality already matches the code, so rerunning it is safe; a non-idempotent script may try to create everything again.
Name three things a plan compares, and what each represents.
The code (what should exist), the state (what the tool created and tracks) and the real infrastructure read from the provider's API (what actually exists).
Why should state not be kept as one file for a whole organisation?
One state means slow plans, a single lock that blocks every team and a blast radius covering everything; splitting by component and environment keeps changes fast and failures contained.
What security advantage does the GitOps pull model have over a pipeline that pushes to the cluster?
The CI system needs no production credentials, because an agent inside the cluster pulls the desired state from Git and applies it.
When is configuration management still a good choice?
For long-lived machines that cannot simply be replaced, such as on-premises servers and network devices, and for configuring base images during the image build.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “How should we split our Terraform or OpenTofu state for the food-delivery platform, and how do we stop people making changes in the console?”