Terraform Explained¶
6-minute read.
The one-line answer¶
Terraform is a tool that lets you define cloud infrastructure in text files (instead of clicking around a console), then provisions it for you. It's "infrastructure as code."
Why this exists¶
Cloud consoles are great for exploring. They are terrible for building production systems. Specifically:
- Not reproducible - "click these 47 things in this order" is not a deployable artifact
- No history - who changed the security group rule? Why?
- No environments - clicking everything 3x for dev/staging/prod is error-prone
- No code review - changes happen invisibly
Infrastructure as code (IaC) treats your infrastructure the same as your application: it's text in git, reviewed in pull requests, deployed by automation, with full history.
Terraform is the most popular IaC tool. It works across nearly every cloud and SaaS API.
How it works¶
flowchart LR
CODE[".tf files in git<br/>(your desired state)"] --> INIT[terraform init]
INIT --> PLAN[terraform plan]
REAL[(Real cloud<br/>resources)] -. read current state .-> PLAN
STATE[(terraform.tfstate<br/>in S3 / GCS / Azure)] -. read mappings .-> PLAN
PLAN --> DIFF[Diff:<br/>create / update / destroy]
DIFF --> APPLY[terraform apply]
APPLY --> REAL2[(Cloud resources<br/>updated)]
APPLY --> STATE2[(State file<br/>updated)] You write .tf files like:
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-prod"
tags = {
Project = "my-app"
Environment = "prod"
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration {
status = "Enabled"
}
}
Then:
terraform init # download providers
terraform plan # show what will change
terraform apply # actually do it
plan is the killer feature: it diffs your code against reality and shows exactly what will be created, modified, or destroyed before you commit.
The state file¶
Terraform tracks reality in a JSON file called terraform.tfstate. This is the bridge between "what you wrote" and "what exists."
The state file: - Maps your code (aws_s3_bucket.logs) to real resources (S3 ARN) - Tracks attributes that aren't in your code (auto-generated IDs, etc.) - Lets plan know "this exists already, no need to recreate"
State is sacred. Never edit it by hand. Lose it and Terraform doesn't know what it created.
In production, store state remotely:
- AWS - S3 bucket + DynamoDB table for locking
- Azure - Blob Storage container
- GCP - GCS bucket
- Terraform Cloud / HCP Terraform - hosted state
The lock prevents two engineers from running apply simultaneously and corrupting state.
A small concrete example¶
A complete VPC + EC2 instance:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "main" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
subnet_id = aws_subnet.main.id
tags = { Name = "web" }
}
output "instance_ip" {
value = aws_instance.web.private_ip
}
terraform apply creates a VPC, subnet, and EC2 instance, in the right order (VPC first, since the subnet depends on it). Terraform builds a dependency graph automatically.
Variables and modules¶
Hardcoding values is bad. Use variables:
variable "environment" {
type = string
default = "dev"
}
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-${var.environment}"
}
Reusable infrastructure goes in modules:
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = "prod"
}
Modules can be local directories, git repos, or pulled from the public Terraform Registry.
Workspaces / environments¶
Two common patterns for dev/staging/prod:
- One codebase, multiple state files - separate
.tfvarsfiles and state buckets per environment. - Branching the codebase - each env has its own directory tree (more verbose but clearer).
Don't use the same state for prod and dev. Don't.
Common pitfalls¶
- Drift - someone clicked something in the console. Now reality doesn't match state. Run
terraform planregularly to detect. - Manual edits to state - eventually destroys you. Use
terraform importto bring existing resources under management. - Storing secrets in
.tffiles - they end up in git. Use a secret manager orsensitivevariables sourced from elsewhere. - Massive monolithic state - one apply takes 10 minutes, blast radius is huge. Split into smaller stacks.
- No locking - two
applys at once = corrupted state. Always use a remote backend with locking.
Terraform vs the alternatives¶
- CloudFormation (AWS-only) - native, JSON/YAML, slower iteration than Terraform.
- Pulumi - Terraform-like but in real programming languages (TypeScript, Python, Go). Same state model.
- CDK (AWS Cloud Development Kit) - write CloudFormation in TypeScript/Python. AWS-native.
- Ansible - configuration management. Sometimes used for infra; not its sweet spot.
- Terragrunt - thin wrapper over Terraform that helps with DRY across many environments and stacks.
For multi-cloud or vendor-neutral: Terraform. For AWS-only with a developer-first team: CDK is great. For very large orgs with many stacks: Terragrunt or Terraform Cloud workspaces.
Workflow in practice¶
- Write/change
.tffiles terraform fmt(auto-format)terraform validate(syntax check)terraform plan -out=tfplan(review diff)- Code review (git PR)
terraform apply tfplan(after merge, often via CI)
Production: apply runs in CI (GitHub Actions, Atlantis, Terraform Cloud) so a human can't accidentally apply unreviewed changes.
What to look at next¶
- CI/CD explained - apply runs in CI in production
- Glossary: Terraform, IaC, State, Module
- CLI cheat sheet: Terraform
- HashiCorp certifications - Terraform Associate is the gateway cert