DEV Community

Timevolt
Timevolt

Posted on

Terraform vs CloudFormation: The Infrastructure Awakens

The Quest Begins (The "Why")

I still remember the first time I tried to spin up a small web app on AWS by clicking through the console. I created a VPC, added a subnet, launched an EC2 instance, attached a security group, and then… I realized I’d forgotten to tag the instance for cost allocation. I went back, clicked a few more times, and somehow ended up with two identical security groups because I’d copied the wrong ID. After an hour of clicking, I had a working environment—but zero confidence I could recreate it tomorrow.

That feeling of “did I just build a house of cards?” is what pushed me toward Infrastructure as Code (IaC). I wanted a repeatable, version‑controlled way to describe my infrastructure so I could stop playing whack‑a‑mouse with the AWS console and start focusing on actually shipping features.

The Revelation (The Insight)

The big “aha!” moment came when I realized IaC isn’t just about writing scripts; it’s about declaring the desired state of your world and letting a tool figure out how to get there. Think of it like giving a GPS a destination instead of shouting turn‑by‑turn directions at a driver.

Two heavyweight contenders in the AWS ecosystem are Terraform and CloudFormation. Both let you write code that provisions resources, but they differ in flavor:

  • CloudFormation is AWS‑native, uses JSON or YAML, and lives entirely inside the AWS ecosystem.
  • Terraform is cloud‑agnostic, uses its own HCL syntax, and can manage AWS, GCP, Azure, Kubernetes, and even SaaS services with the same language.

The magic? Once your infrastructure lives in a text file, you can treat it like any other codebase: review it in pull requests, run tests, tag releases, and roll back with a git revert. It’s like turning your infra into a trusty sidekick that never forgets a detail.

Wielding the Power (Code & Examples)

Let’s see the difference in practice. Imagine we need a simple web tier: a VPC, a public subnet, an Internet Gateway, a route table, and an EC2 instance with a security group that allows HTTP traffic.

The Struggle (Manual / Ad‑hoc CLI)

If I were to do this with the AWS CLI, I’d end up with a brittle Bash script that looks something like this (I’ve omitted error handling for brevity—yeah, that was a trap!):

# Create VPC
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)

# Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID

# Subnet
SUBNET_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --query 'Subnet.SubnetId' --output text)

# Route Table
RT_ID=$(aws ec2 create-route-table --vpc-id $VPC_ID --query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $RT_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID
aws ec2 associate-route-table --subnet-id $SUBNET_ID --route-table-id $RT_ID

# Security Group
SG_ID=$(aws ec2 create-security-group --group-name web-sg --description "Allow HTTP" --vpc-id $VPC_ID --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0

# EC2 Instance
aws ec2 run-instances \
    --image-id ami-0abcdef1234567890 \
    --count 1 \
    --instance-type t3.micro \
    --subnet-id $SUBNET_ID \
    --security-group-ids $SG_ID \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-server}]'
Enter fullscreen mode Exit fullscreen mode

Trap #1 – Hard‑coded IDs: If you run this script twice without cleaning up, you’ll get duplicate resources or errors because the IDs aren’t stored anywhere reusable.

Trap #2 – No drift detection: If someone manually changes the security group in the console, the script has no way to notice or revert it.

The Victory (Terraform)

Now the same thing in Terraform (main.tf). Notice how we describe what we want, not how to get it:

provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = { Name = "infra-awakens-vpc" }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "infra-awakens-igw" }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true
  tags = { Name = "infra-awakens-public-subnet" }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "infra-awakens-rt" }
}

resource "aws_route" "public_internet" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.gw.id
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP inbound"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "All outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "infra-awakens-web-sg" }
}

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  tags = {
    Name = "web-server"
  }
}
Enter fullscreen mode Exit fullscreen mode

To apply it:

terraform init
terraform plan   # <- see what will change, no surprises
terraform apply  # <- creates everything, or updates if already there
Enter fullscreen mode Exit fullscreen mode

Why this feels like a win:

  • State file (terraform.tfstate) tracks exactly what exists, so running the same code again is a no‑op unless something changed.
  • Variables & modules let you parameterize CIDR blocks, instance types, or reuse the VPC across environments—no more copy‑pasting IDs.
  • Plan step shows you a diff before you touch anything, catching drift or mistakes early.

The Victory (CloudFormation)

If you prefer staying purely inside AWS, the equivalent CloudFormation template (YAML) looks like this:


yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Simple web tier VPC + EC2

Parameters:
  InstanceType:
    Type: String
    Default: t3.micro
    AllowedValues:
      - t2.micro
      - t3.micro
      - t3.small
    Description: EC2 instance type

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: infra-awakens-vpc

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: infra-awakens-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: us-east-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: infra-awakens-public-subnet

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: infra-awakens-rt

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP inbound
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: infra-awakens-web-sg

  WebInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0abcdef1234567890
      InstanceType: !Ref InstanceType
      SubnetId: !Ref PublicSub
Enter fullscreen mode Exit fullscreen mode

Top comments (0)