DEV Community

Timevolt
Timevolt

Posted on

Infrastructure as Code: The Terraform and CloudFormation Strikes Back

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):

  1. Open VPC dashboard → Create VPC → CIDR 10.0.0.0/16 → Name prod-vpc
  2. Create Subnet A → 10.0.1.0/24 → Availability Zone us-east-1a
  3. Create Subnet B → 10.0.2.0/24 → Availability Zone us-east-1b
  4. Create Internet Gateway → Attach to VPC
  5. Create Route Table → Add route 0.0.0.0/0 → IGW → Associate with subnets
  6. Create Security Group → Allow 80/443 inbound → Attach to future EC2
  7. Launch EC2 → AMI amzn2-ami-hvm-2.0.0-x86_64-gp2 → Instance type t3.micro → Subnet A → SG from step 6
  8. Create RDS Subnet Group → Use Subnet A & B
  9. Create RDS Instance → Engine postgres → Subnet group from step 8 → SG allowing 5432 from 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" }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Hard‑coding secrets – Never put passwords straight in .tf files. Use tfvars, 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.
  2. Forgetting to lock provider versions – Without a constraint, a provider update can break your config. The required_providers block 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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)