The Quest Begins (The "Why")
Honestly, I used to treat AWS like a giant LEGO set where I’d snap pieces together by hand in the console. Click, click, click—create a VPC, add subnets, spin up an EC2, attach security groups, pray I didn’t miss a tag. One day I spun up a new environment for a feature branch, forgot to delete the old one, and woke up to a bill that looked like the phone number of a small country. That was my dragon.
I realized I needed a way to describe my infrastructure once, version it like code, and reproduce it reliably—no more midnight console marathons. The idea of Infrastructure as Code (IaC) sounded like a superhero cape, but I had to pick which tool to wear first.
The Revelation (The Insight)
After a few false starts (I tried writing bash scripts that called the AWS CLI—yeah, that was a mess), I stumbled onto two heavyweight contenders: Terraform and AWS CloudFormation. Both let you declare what you want, not how to get it. The magic clicked when I saw that a single file could spin up an entire VPC, a couple of subnets, an RDS instance, and a load balancer—all reproducible, reviewable, and ready for a pull request.
Terraform felt like learning a new language with a friendly community and a powerful plugin ecosystem. CloudFormation felt like staying inside the AWS family, using JSON/YAML that AWS already understands natively. The real insight? You don’t have to pick just one forever; you can start with whichever feels more natural and later compare notes.
Wielding the Power (Code & Examples)
The Struggle: Manual Click‑Ops
Here’s what a typical “create a web tier” flow looked like in the console (pseudo‑steps, because you can’t copy‑paste clicks):
- Open VPC dashboard → Create VPC → CIDR
10.0.0.0/16→ Nameprod-vpc - Create Subnet A →
10.0.1.0/24→ Availability Zoneus-east-1a - Create Subnet B →
10.0.2.0/24→ Availability Zoneus-east-1b - Create Internet Gateway → Attach to VPC
- Create Route Table → Add route
0.0.0.0/0→ IGW → Associate with subnets - Create Security Group → Allow
80/443inbound → Attach to future EC2 - Launch EC2 → AMI
amzn2-ami-hvm-2.0.0-x86_64-gp2→ Instance typet3.micro→ Subnet A → SG from step 6 - Create RDS Subnet Group → Use Subnet A & B
- Create RDS Instance → Engine
postgres→ Subnet group from step 8 → SG allowing5432from web tier SG
If you missed a step—or worse, typed the wrong CIDR—you’d spend hours untangling the mess.
The Victory: Terraform
Below is a Terraform version that does the same thing in ~30 lines. Save it as main.tf.
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# VPC
resource "aws_vpc" "prod" {
cidr_block = "10.0.0.0/16"
tags = { Name = "prod-vpc" }
}
# Internet Gateway
resource "aws_internet_gateway" "prod" {
vpc_id = aws_vpc.prod.id
tags = { Name = "prod-igw" }
}
# Subnets
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.prod.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
tags = { Name = "public-subnet-a" }
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.prod.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"
tags = { Name = "public-subnet-b" }
}
# Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.prod.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.prod.id
}
tags = { Name = "public-rt" }
}
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
# Security Group for web tier
resource "aws_security_group" "web_sg" {
name = "web-sg"
description = "Allow HTTP/HTTPS inbound"
vpc_id = aws_vpc.prod.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "web-sg" }
}
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public_a.id
vpc_security_group_ids = [aws_security_group.web_sg.id]
tags = { Name = "web-server" }
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# RDS Subnet Group
resource "aws_db_subnet_group" "rds_subnet" {
name = "rds-subnet-group"
subnet_ids = [aws_subnet.public_a.id, aws_subnet.public_b.id]
tags = { Name = "rds-subnet-group" }
}
# Security Group for RDS
resource "aws_security_group" "rds_sg" {
name = "rds-sg"
description = "Allow PostgreSQL from web tier"
vpc_id = aws_vpc.prod.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.web_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "rds-sg" }
}
# RDS Instance
resource "aws_db_instance" "postgres" {
identifier = "prod-postgres"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
name = "appdb"
username = "admin"
password = var.db_password # <-- put this in a tfvars file or secret manager
subnet_group_name = aws_db_subnet_group.rds_subnet.name
vpc_security_group_ids = [aws_security_group.rds_sg.id]
skip_final_snapshot = true
tags = { Name = "prod-postgres" }
}
What just happened?
- The file is declarative: I state what I want, Terraform figures out the order.
- Everything lives in version control—diff reviews, rollbacks, peer approvals.
- If I change the CIDR of a subnet, Terraform will plan the change and show me the impact before applying.
Common Terraform Traps
-
Hard‑coding secrets – Never put passwords straight in
.tffiles. Usetfvars, AWS Secrets Manager, or environment variables. I once committed a DB password to a public repo and had to rotate it in a panic—don’t be me. -
Forgetting to lock provider versions – Without a constraint, a provider update can break your config. The
required_providersblock above saves you from surprise upgrades.
The Victory: CloudFormation
If you prefer to stay inside the AWS ecosystem, here’s the same stack as a CloudFormation template (YAML). Save it as stack.yaml.
yaml
AWSTemplateFormatVersion: '2010-10'
Description: > VPC + 0'
Resources:
ProdVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
Tags:
- Key: Name
Value: prod-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: prod-igw
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref ProdVPC
InternetGatewayId: !Ref InternetGateway
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProdVPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: us-east-1a
Tags:
- Key: Name
Value: public-subnet-a
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProdVPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: us-east-1b
Tags:
- Key: Name
Value: public-subnet-b
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProdVPC
Tags:
- Key: Name
Value: public-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
SubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP/HTTPS inbound
VpcId: !Ref ProdVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: web-sg
WebInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !FindInMap [AWSRegionToAMI, !Ref "AWS::Region", AMI]
InstanceType: t3.micro
SubnetId: !Ref PublicSubnetA
SecurityGroupIds:
- !Ref WebSecurityGroup
Tags:
- Key: Name
Value: web-server
Mappings:
AWSRegionToAMI:
us-east-1:
AMI: ami-0abcdef1234567890 # Amazon Linux 2 (update as needed)
RDSSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnets for RDS
SubnetIds:
- !Ref PublicSubnetA
- !Ref Public
Top comments (0)