DEV Community

Timevolt
Timevolt

Posted on

Terraform vs CloudFormation: The Matrix of IaC

The Quest Begins (The "Why")

Look, I’ve been there. You’re spinning up a new environment for a side‑project, and you find yourself clicking through the AWS console, copying‑pasting CLI commands into a terminal, and praying that nothing gets missed. One minute you’re proud of a shiny new VPC, the next you realize you forgot to tag a subnet, or worse, you left a security group wide open because you “thought you’d fix it later.”

It felt like trying to solve a Rubik’s cube while blindfolded—every turn seemed to make a mess somewhere else. I kept thinking, “There’s got to be a better way to describe what I want and have the cloud just… do it.” That “aha!” moment came when a teammate showed me a single file that could spin up an entire stack, and if I changed one line, the whole thing updated itself. Suddenly, the chaos turned into a repeatable spell.

The Revelation (The Insight)

The treasure I uncovered is Infrastructure as Code (IaC)—the idea that your infrastructure should live in version‑controlled, human‑readable files just like your application code. Two heavyweights dominate the AWS world: Terraform (cloud‑agnostic, declarative, state‑driven) and CloudFormation (AWS‑native, JSON/YAML, tightly integrated).

What blew my mind was how both let you treat servers, networks, and policies as data. Instead of imperative steps (“create this, then that, then wait for it to be ready”), you declare the desired end state and the tool figures out how to get there. State management (Terraform’s .tfstate or CloudFormation’s stack) ensures you don’t drift—if someone manually changes a resource, the next plan will flag it or revert it, depending on how you set things up.

The magic isn’t just in the files; it’s in the workflow. You can review changes with a pull request, run a plan to see exactly what will happen, and apply with confidence. No more “oops, I forgot to delete that old load balancer” surprises.

Wielding the Power (Code & Examples)

The Struggle: Manual‑ish CloudFormation

Here’s a typical CloudFormation template I used when I first started—lots of repetition, hard‑coded IDs, and a confusing mix of parameters and mappings:

AWSTemplateFormatVersion: '2010-09-09'
Description: Simple VPC with public and private subnets

Parameters:
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
  PublicSubnet1Cidr:
    Type: String
    Default: 10.0.1.0/24
  PrivateSubnet1Cidr:
    Type: String
    Default: 10.0.2.0/24

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: MyVPC

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: MyIGW

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

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !Ref PublicSubnet1Cidr
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: PublicSubnet1

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !Ref PrivateSubnet1Cidr
      MapPublicIpOnLaunch: false
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: PrivateSubnet1

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
        - Key: Name
          Value: PublicRT

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

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable
Enter fullscreen mode Exit fullscreen mode

It works, but every time I wanted to add another AZ or tweak a tag, I had to copy‑paste blocks, keep track of IDs, and worry about missing a comma. The template grew legs and started to feel like a beast.

The Victory: Terraform – Clean, Modular, and Exciting

Now watch how the same idea looks in Terraform. I broke it into reusable modules, used variables, and let the state file handle the heavy lifting:

# variables.tf
variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "List of public subnet CIDRs"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.10.0/24"]
}

variable "private_subnet_cidrs" {
  description = "List of private subnet CIDRs"
  type        = list(string)
  default     = ["10.0.2.0/24", "10.0.11.0/24"]
}

# main.tf
provider "aws" {
  region = var.aws_region
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  tags = {
    Name = "my-vpc"
  }
}

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

# Create subnets dynamically
resource "aws_subnet" "public" {
  for_each = toset(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value
  map_public_ip_on_launch = true
  availability_zone       = element(data.aws_availability_zones.available.names, index(toset(var.public_subnet_cidrs), each.value))
  tags = {
    Name = "public-subnet-${each.value}"
  }
}

resource "aws_subnet" "private" {
  for_each = toset(var.private_subnet_cidrs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value
  map_public_ip_on_launch = false
  availability_zone       = element(data.aws_availability_zones.available.names, index(toset(var.private_subnet_cidrs), each.value))
  tags = {
    Name = "private-subnet-${each.value}"
  }
}

# Route table for public subnets
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "public-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" {
  for_each = aws_subnet.public
  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

# Data source for AZs (helps us spread subnets)
data "aws_availability_zones" "available" {}
Enter fullscreen mode Exit fullscreen mode

Why this feels like a win:

  • Declarative & concise – I state what I want, not how to build it step‑by‑step.
  • Loops (for_each) – Adding a new subnet is as simple as appending a CIDR to a list; Terraform figures out the rest.
  • State‑driven – If someone manually changes a tag, terraform plan will show a drift and terraform apply can reconcile it.
  • Modules & reusability – I can extract the subnet block into a module and reuse it across projects with a single line.

Traps to Avoid (The “Monsters” on the Path)

  1. Hardcoding IDs in CloudFormation – I once copied a subnet ID from one stack into another, assuming it would stay the same. When the original stack was deleted, the dependent stack failed catastrophically. Lesson: Use !Ref or !GetAtt to reference resources dynamically, never copy‑paste raw IDs.

  2. Forgetting to Lock Terraform State – Early on I ran terraform apply concurrently with a teammate on the same backend. The state file got corrupted, and we spent hours untangling resources. Lesson: Always configure a remote backend with state locking (e.g., S3 + DynamoDB) before you start collaborating.

Why This New Power Matters

Now that I’ve tamed the IaC beast, I can spin up a full‑featured environment—VPC, subnets, security groups, RDS, even an EKS cluster—in under five minutes. My pull requests become the single source of truth: reviewers see exactly what will change, and CI pipelines run plan/apply automatically.

The best part? The same Terraform code works (with minor tweaks) on GCP or Azure if I ever need to multi‑cloud. CloudFormation, while AWS‑only, shines when you want deep integration with services like AWS Config or Service Catalog—just pick the tool that fits your quest.

You’re no longer a manual click‑warrior; you’re an architect who treats infrastructure like code, with all the benefits of version control, peer review, and automated testing. It’s empowering, it’s reproducible, and honestly, it feels a little like finally mastering a combo in a fighting game—smooth, satisfying, and ready for the next boss.

Your Turn – Start Your Own Quest

Grab a fresh AWS account (or a sandbox), pick either Terraform or CloudFormation, and try to provision a simple VPC with two public and two private subnets. Write the code, run a plan or changeset, and watch the resources appear.

When you get it working, tweak something—add a NAT gateway, change a tag, or attach an EC2 instance—and see how the tool reacts. Share your experience, your gotchas, and the moment you felt that “I’ve leveled up” rush.

What’s your first IaC spell going to be? 🚀

Top comments (0)