Infrastructure as Code: Terraform on AWS
How I use Terraform to provision and manage scalable AWS infrastructure with reusable modules and remote state.
Ahmed
DevOps & Cloud Engineer
Managing cloud infrastructure by clicking around the AWS console works fine at the start. But the moment you need to reproduce an environment, audit a change, or roll back after an incident, you feel the pain immediately. Infrastructure as Code (IaC) with Terraform solves all three.
Why Terraform Over CloudFormation?
CloudFormation is AWS-native and well-integrated, but Terraform's provider ecosystem spans every major cloud and SaaS. Your team can manage AWS, Cloudflare DNS, GitHub repos, and Datadog monitors all in one unified workflow.
- HCL is more readable than JSON/YAML CloudFormation templates
- terraform plan shows a diff before applying — no surprises
- State management gives you a source of truth for what exists
- Modules let you build reusable infrastructure components
- Works with AWS, GCP, Azure, Kubernetes, GitHub, and 3,000+ providers
Project Structure That Scales
A flat single-file approach breaks down fast. Here's the structure I use across production projects:
infra/
├── environments/
│ ├── dev/
│ │ ├── main.tf # calls modules
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
└── modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── eks/
└── rds/Remote State with S3 + DynamoDB
Warning
Never use local state in a team environment. If two engineers run apply simultaneously without state locking, you will corrupt your infrastructure state.
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-lock" # state locking
}
}# One-time setup: create the S3 bucket and DynamoDB table
aws s3api create-bucket \
--bucket my-company-tfstate \
--region us-east-1
aws s3api put-bucket-versioning \
--bucket my-company-tfstate \
--versioning-configuration Status=Enabled
aws dynamodb create-table \
--table-name terraform-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTA Reusable VPC Module
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, { Name = "${var.name}-vpc" })
}
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr, 8, count.index)
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = { Name = "${var.name}-public-${var.azs[count.index]}" }
}The Workflow: Plan → Review → Apply
- 1terraform init — downloads providers and configures backend
- 2terraform fmt -recursive — formats all .tf files consistently
- 3terraform validate — catches syntax and semantic errors
- 4terraform plan -out=tfplan — generates a diff, saves to file
- 5Code review the plan output in your PR
- 6terraform apply tfplan — applies the exact reviewed plan
Tip
Use terraform plan -out=tfplan and then terraform apply tfplan so the apply executes exactly what was reviewed. A plain terraform apply re-plans, which can introduce drift.
“The highest form of infrastructure management is when your git history is the audit log of every change ever made to your cloud.