Chapter 1: Foundations
Topic 01

What Infrastructure as Code Is

ConceptIaC

Infrastructure as Code is the practice of defining your servers, networks, databases, and permissions in text files that you version, review, and apply — instead of clicking through a cloud console. The files are the source of truth; a tool reads them and makes the cloud match. Your infrastructure stops being a thing you remember how you built and becomes a thing you can read, diff, and rebuild.

The problem it solves is that console-built infrastructure is unreproducible. Nobody can say exactly how production was assembled, staging quietly diverges from it, and rebuilding after an outage turns into archaeology. Write the same infrastructure as code and those questions have answers: the code is the record, the git history is the changelog, and a second identical environment is one apply away.

The Cost of ClickOps

Building infrastructure by hand in a web console — "ClickOps" — feels fast for the first resource and compounds into debt for the hundredth. Each manual change is invisible: there is no record of who made it, when, or why. Two engineers solving the same problem build it two different ways. The knowledge of how the system fits together lives in people's heads and leaves when they do.

The sharpest version of this pain is environment drift. You build production by clicking, then build staging the same way a month later from memory. The two are subtly different, so a change that works in staging breaks in production, and the difference is undocumented because there was never a document. The console gives you no diff, no review, and no undo beyond your own memory.

Declarative, Not Imperative

There are two ways to tell a computer to build infrastructure. The imperative way is a script of steps: create this bucket, then this database, then wire them together — and you own every step, including checking whether each thing already exists. The declarative way is a description of the end state: here is the bucket, the database, and the wiring I want to exist. You describe the destination; the tool computes the route.

That difference is not stylistic. An imperative script that creates a bucket fails the second time you run it, because the bucket already exists — so you end up writing "does this exist yet?" logic around every action. A declarative tool compares your description to what is actually there and does only what is needed to close the gap. The snippet below is the entire instruction to ensure a bucket exists; there is no "create" verb and no "already exists" handling, because you declared a state, not an action.

Declarative — a desired-state resource
resource "aws_s3_bucket" "logs" {
  bucket = "my-app-logs-7f3a"   # globally-unique name — declared, not "created"
}

Run the equivalent imperative command twice and the second run errors because the bucket is already there; you would have to catch that yourself. Apply the declarative resource twice and the second run is a no-op, because the desired state already matches reality. That property has a name, and it is the next idea.

Idempotency and Desired State

Idempotency means applying the same configuration twice yields the same result — the first apply creates what is missing, and the second changes nothing because nothing needs changing. The tool holds a model of desired state and continuously reconciles reality toward it, rather than blindly re-running actions. This is why a declarative apply is safe to run repeatedly and an imperative script is not.

Desired state is also what makes change reviewable. Because the configuration describes the end result, a change to it is a diff — add three lines to add a subnet, change one to resize an instance. That diff goes through the same pull-request review as application code, which means infrastructure changes get the same scrutiny, history, and rollback that code has had for decades.

Provisioning, Configuration, and Orchestration

"Infrastructure as Code" is an umbrella over three different layers, and confusing them is the most common newcomer mistake. Provisioning creates the resources — the virtual machine, the network, the database. Configuration management sets up what runs on them — installs packages, edits config files, manages services. Orchestration schedules long-running workloads across machines, as Kubernetes does for containers.

Terraform is a provisioning tool. It is excellent at creating and tracking the existence and shape of cloud resources, and it deliberately does not configure the operating system inside a server — that is the configuration layer's job, handled by tools like Ansible. Keeping these layers distinct, rather than forcing one tool to do all three, is what keeps an infrastructure pipeline clean. The next topic places Terraform precisely; the one after maps the whole landscape.

Three layers of Infrastructure as Code
Provisioning
Creates the resources — VM, network, database. Terraform lives here.
Configuration
Sets up what runs on them — packages, config files, services. Ansible, Chef, cloud-init.
Orchestration
Schedules long-running workloads across machines. Kubernetes, Nomad.
Declarative vs Imperative

Declarative — you commit the desired end state (Terraform, CloudFormation) and the tool computes the diff and converges on it. Re-running is safe, and the configuration doubles as documentation of what exists. Prefer it for anything that outlives a one-off task.

Imperative — you script each step (a shell loop over the AWS CLI). Fast for a single throwaway action, but you must handle "does this already exist?" yourself, and re-running is dangerous. It rots as a way to manage standing infrastructure.

Common Mistakes
  • Building production in the console "just this once" and intending to codify it later — the code never gets written and the resource becomes an undocumented snowflake nobody dares touch.
  • Treating IaC as documentation that can lag reality — once you make a change by hand, the code lies, and the next apply may revert your manual fix.
  • Expecting a provisioning tool to install packages and configure the OS — provisioning a server is not configuring it; that is a separate layer's job.
  • Scripting raw cloud API calls to "do IaC" without any state tracking — you rebuild idempotency badly instead of using a tool that already solved it.
  • Mixing provisioning, configuration, and orchestration into one tool because it seems simpler, then fighting the tool everywhere it was never meant to go.
Best Practices
  • Codify infrastructure from the first resource, before anything reaches production, so there is never a manual baseline to reverse-engineer.
  • Make every change through code and never through the console on managed resources, so declared state and real state stay in sync.
  • Put all infrastructure code under version control and review it with the same pull-request process as application code.
  • Separate the provisioning layer (Terraform) from the configuration layer (Ansible, cloud-init) deliberately rather than blurring them.
  • Treat a declarative configuration as the source of truth, and reach for the console only to read, never to change.
Comparable tools CloudFormation · Pulumi declarative provisioning, like Terraform Ansible · Chef · Puppet configuration management, a different layer AWS CLI · boto3 scripts imperative provisioning, no state tracking

Knowledge Check

Why is a declarative configuration safe to apply repeatedly when an imperative script is not?

  • It compares desired state to reality and does only what is needed, so a second apply is a no-op
  • It runs faster because the steps are compiled ahead of time
  • It skips any resource that already exists by name without ever checking whether its configuration still matches
  • It only ever creates resources and never modifies them

A team builds production by hand in the console, then builds staging the same way a month later. What is the predictable result?

  • The environments drift apart in undocumented ways, so a change that works in one breaks in the other
  • The two environments stay byte-for-byte identical because the console enforces consistency across every manual build
  • Staging automatically inherits production's configuration
  • Nothing, as long as the same engineer builds both

Which task is the job of the configuration-management layer, not the provisioning layer Terraform occupies?

  • Installing packages and editing config files inside a running server
  • Creating the virtual machine, its network, and the security groups that surround it
  • Allocating a database instance
  • Provisioning a storage bucket

What does "idempotent" mean for a Terraform apply?

  • Applying the same configuration again produces no change once reality already matches it
  • Every apply recreates all resources from scratch for consistency
  • The configuration can only be applied once, after which it is locked and becomes permanently read-only
  • Each apply must be run twice to take effect

You got correct