Terraform
The previous page argued that declarative IaC with a reconciler is the robust choice. Terraform is the tool that made that idea mainstream for provisioning cloud infrastructure. You write files describing the resources you want; Terraform figures out how to create, update, or destroy real resources to match. This page is the mechanical tour.
Providers: the plug for each platform
Section titled “Providers: the plug for each platform”Terraform itself knows nothing about AWS, GCP, Cloudflare, or GitHub. All of that lives in providers — plugins that translate Terraform’s generic “create/read/update/delete a resource” model into a specific platform’s API. You declare the providers you need; Terraform downloads them.
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
provider "aws" { region = "us-east-1"}Because the engine is provider-agnostic, the same Terraform workflow — plan, apply, the dependency
graph, state — works whether you’re provisioning EC2 instances, a Cloudflare DNS record, or a Datadog
monitor. One mental model, many platforms.
Resources and HCL
Section titled “Resources and HCL”The atom of Terraform is the resource — one piece of infrastructure (a VM, a subnet, a DNS record). You declare resources in HCL (HashiCorp Configuration Language), the declarative syntax you met in the last page: nouns and desired values, not steps.
resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro"
tags = { Name = "web-server" }}Read that as a fact about the world you want: there should be a t3.micro instance running this AMI,
tagged web-server. The two strings after resource are the type (aws_instance, defined by the
provider) and a local name (web, how you refer to it elsewhere in your code). Together they form
the address aws_instance.web.
The dependency graph
Section titled “The dependency graph”Real infrastructure has ordering constraints: a server must live inside a network that exists first; a DNS record must point at a server that exists first. Crucially, you don’t write that ordering. You express dependencies by referencing one resource’s attributes from another, and Terraform infers the order by building a dependency graph.
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16"}
resource "aws_subnet" "app" { vpc_id = aws_vpc.main.id # ← reference creates a dependency cidr_block = "10.0.1.0/24"}Because aws_subnet.app reads aws_vpc.main.id, Terraform knows the VPC must be created first. It also
knows that resources with no dependency between them can be created in parallel. This is the
declarative payoff in action: you state relationships, the tool derives the procedure — exactly the
“reasoning about steps” that the previous page said a reconciler
takes off your hands.
You write references Terraform builds the graph ────────────────────── ────────────────────────────── subnet needs vpc.id aws_vpc.main instance needs subnet.id │ record needs instance.ip aws_subnet.app │ aws_instance.web ──► aws_route53_record.www (create in this order; independent ones in parallel)plan and apply: look before you leap
Section titled “plan and apply: look before you leap”Terraform’s two most important commands embody the safety story of this whole Part.
terraform init # download providers, set up the working dirterraform plan # compute the diff between code and reality — changes NOTHINGterraform apply # execute that plan — creates/updates/destroys real resourcesplan is a dry run. It compares your code against the real world (via state — see
State & Drift) and prints exactly what it would do, with + for create, ~
for change, and - for destroy. Nothing happens yet. apply then executes that plan. This is the
reviewable diff from Why IaC, now concrete: you read the plan, confirm it does what you
expect, then let it touch production.
Variables and outputs: the seams
Section titled “Variables and outputs: the seams”Hardcoding values makes code un-reusable. Variables are typed inputs; outputs are values a configuration exposes after it runs.
variable "instance_type" { type = string default = "t3.micro"}
resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = var.instance_type # ← parameterized}
output "public_ip" { value = aws_instance.web.public_ip # ← exposed after apply}Variables are how the same code builds staging (instance_type = "t3.micro") and production
(instance_type = "m5.large") — the reproducibility-with-one-diff idea from Why IaC.
Outputs are how one configuration hands a value (a database endpoint, a server IP) to a human or to
another configuration.
Modules: reusable infrastructure
Section titled “Modules: reusable infrastructure”A module is a folder of .tf files you can call from elsewhere, with its own variables and outputs.
It’s the function of Terraform: define a “standard web service” once, then instantiate it many times.
module "api" { source = "./modules/web-service" instance_type = "m5.large" instance_count = 3}Modules turn copy-pasted infrastructure into a single reviewed, versioned definition. Fix a security default in the module and every caller inherits the fix — the same leverage Helm charts give Kubernetes manifests.
How does plan know what already exists in the real world? Through the state file — the subject of
the next page.
The architect’s lens
Section titled “The architect’s lens”Five questions to place Terraform rather than just run it:
- Why does it exist? To make declarative provisioning mainstream: you write HCL describing resources, and a provider-agnostic engine creates, updates, or destroys real infrastructure to match.
- What problem does it solve? Figuring out the order of API calls for interdependent infrastructure —
you state relationships by reference, Terraform builds the dependency graph and derives the order, and
planshows the exact diff before anything changes. - What are the trade-offs? Providers let one workflow span AWS/GCP/Cloudflare/Datadog, but you take on a
state file to manage, and a
~change can silently force a destroy-then-create replace — fine for a web server, catastrophic for a database. - When should I avoid it? Configuring software inside a running box (that’s Ansible’s job), or trivial one-off resources where the init/state/plan ceremony outweighs the benefit.
- What breaks if I remove it? Infrastructure reverts to console clicks or hand-ordered API scripts — no reviewable plan, no dependency graph, no deterministic engine, and no preview of the blast radius (recall the 2017 S3 typo).
Check your understanding
Section titled “Check your understanding”- Terraform’s engine knows nothing about AWS or GCP. What component supplies that knowledge, and why is that separation valuable?
- In
resource "aws_instance" "web", what are the two strings, and how do you reference this resource elsewhere? - You never write the order in which resources are created. How does Terraform determine it, and what in your code drives that?
- What is the difference between
terraform planandterraform apply, and why isplancentral to the safety argument of this Part? - Explain the roles of variables, outputs, and modules. Which one lets the same code build both staging and production?
Show answers
- Providers supply that knowledge — plugins that translate Terraform’s generic create/read/update/delete
model into a specific platform’s API. The separation is valuable because the engine stays
provider-agnostic, so one workflow (
plan,apply, graph, state) works across AWS, GCP, Cloudflare, Datadog, and more. - The first string is the type (
aws_instance, defined by the provider); the second is a local name (web, how you refer to it in your code). Together they form the addressaws_instance.web, which you use to reference it elsewhere. - Terraform builds a dependency graph and infers the order. What drives it is references: when one
resource reads another’s attribute (e.g.
aws_subnet.appusesaws_vpc.main.id), Terraform knows the VPC must come first; resources with no reference between them can be created in parallel. planis a dry run — it diffs your code against reality (via state) and prints what it would do (+/~/-) while changing nothing;applyexecutes that plan against real resources.planis central to the safety argument because it’s the reviewable diff you read and confirm before anything touches production.- Variables are typed inputs that parameterize the code; outputs expose values after a run (a server
IP, a DB endpoint) to a human or another configuration; modules are reusable folders of
.tfyou instantiate many times. Variables are what let the same code build staging (t3.micro) and prod (m5.large) with just a differing value.