DEV Community

Timevolt
Timevolt

Posted on

Infrastructure as Code: Terraform and CloudFormation – The Fellowship of the Cloud

The Quest Begins (The "Why")

Honestly, I was tired of playing “guess the configuration” every time I spun up a new environment. One day I clicked “Apply” in the AWS console, watched a VPC pop up, then realized three weeks later that the same VPC existed in two accounts with slightly different CIDR blocks. It felt like trying to herd cats while blindfolded. I knew there had to be a better way—something that would let me define my infrastructure once, version‑control it, and apply it everywhere with confidence. That’s when the idea of Infrastructure as Code (IaC) stopped being a buzzword and started looking like the holy grail I’d been chasing.

The Revelation (The Insight)

The magic moment came when I realized IaC isn’t just about writing scripts; it’s about treating your infrastructure like application code. You get peer reviews, automated tests, rollbacks, and the ability to see exactly what changed between releases. Two heavyweight contenders in the AWS world are Terraform and CloudFormation. Both let you declare resources, but they differ in language, ecosystem, and workflow.

Terraform uses its own declarative language (HCL) and works across many providers—AWS, Azure, GCP, you name it. CloudFormation is native to AWS and uses JSON or YAML templates. If you’re all‑in on AWS, CloudFormation feels like a natural extension of the console. If you crave multi‑cloud flexibility or love a rich module registry, Terraform often wins the day.

What blew my mind was how a single file could describe an entire VPC, subnets, security groups, and even an EC2 fleet, and then a single command could bring it to life—or tear it down—safely. It felt like Neo seeing the Matrix code for the first time: everything snapped into focus, and I could manipulate reality with a few keystrokes.

Wielding the Power (Code & Examples)

The Struggle: Manual Click‑Ops

Before IaC, I’d spin up a web tier like this (pseudo‑steps, not actual code):

  1. Open AWS Console → VPC → Create VPC (10.0.0.0/16)
  2. Create two subnets (public/private) in each AZ
  3. Attach an Internet Gateway, configure route tables
  4. Create a security group, open ports 80/443
  5. Launch an Auto Scaling Group with a launch configuration
  6. Tag everything for billing

Repeat steps 1‑6 for staging, prod, and each new account. Miss a tag? Forget a route? You’re debugging at 2 a.m.

The Victory: Terraform

Here’s a compact Terraform example that does the same thing in ~30 lines. Save it as main.tf.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  description = "AWS region to deploy into"
  type        = string
  default     = "us-east-1"
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "fellowship-vpc"
    Env  = var.env
  }
}

# Internet Gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "fellowship-igw"
  }
}

# Public Subnet (example: one AZ)
resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "${var.region}a"

  tags = {
    Name = "fellowship-public"
  }
}

# Route Table for public subnet
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "fellowship-rt-public"
  }
}

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

# Security Group (allow HTTP/HTTPS)
resource "aws_security_group" "web" {
  name        = "fellowship-sg-web"
  description = "Allow web traffic"
  vpc_id      = aws_vpc.main.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 = "fellowship-sg-web"
  }
}

# Launch Template for ASG
resource "aws_launch_template" "web_lt" {
  name_prefix   = "fellowship-web-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  network_interfaces {
    security_groups = [aws_security_group.web.id]
    associate_public_ip_address = true
  }

  tag_specifications {
    resource_type = "instance"

    tags = {
      Name = "fellowship-web"
    }
  }
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "web_asg" {
  name                      = "fellowship-asg"
  max_size                  = 3
  min_size                  = 1
  desired_capacity          = 2
  vpc_zone_identifier       = [aws_subnet.public.id]
  launch_template {
    id      = aws_launch_template.web_lt.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "fellowship-asg"
    propagate_at_launch = true
  }
}
Enter fullscreen mode Exit fullscreen mode

What just happened?

  • The terraform block locks the provider version—no surprise upgrades breaking your stack.
  • Variables let you reuse the same code for dev, staging, prod by simply changing -var env=prod.
  • Resources are declared declaratively; Terraform figures out the correct order (VPC → IGW → Subnet → Route Table → SG → Launch Template → ASG).
  • Running terraform init && terraform apply creates everything in seconds. terraform destroy tears it down cleanly.

Common Traps (The “Monsters” to Avoid)

  1. Hard‑coding IDs – If you copy‑paste a subnet ID from the console into your code, you’ll lose portability. Always reference resources by their Terraform attributes (aws_subnet.public.id).
  2. Missing State Locking – Terraform state is the source of truth. Without a remote backend (S3 + DynamoDB lock), two teammates running apply simultaneously can corrupt state. Set up a backend early:
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "fellowship/terraform.tfstate"
    region = "us-east-1"
    dynamodb_table = "terraform-locks"
  }
}
Enter fullscreen mode Exit fullscreen mode

The CloudFormation Counterpart

If you prefer staying purely within AWS, here’s the same VPC + ASG skeleton in YAML (about 45 lines).

AWSTemplateFormatVersion: '2010-09-09'
Description: Fellowship VPC with Web ASG

Parameters:
  Env:
    Type: String
    Default: dev

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: fellowship-vpc
        - Key: Env
          Value: !Ref Env

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: fellowship-igw
    DependsOn: VPC

  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: !Select [0, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: fellowship-public

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

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

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

  WebSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow web traffic
      VpcId: !Ref VPC
      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: fellowship-sg-web

  WebLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: !ResolveAWS::SSM::Parameter::Value:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
        InstanceType: t3.micro
        NetworkInterfaces:
          - AssociatePublicIpAddress: true
            SecurityGroupIds:
              - !Ref WebSG
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: fellowship-web

  WebASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
        - !Ref PublicSubnet
      LaunchTemplate:
        LaunchTemplateId: !Ref WebLaunchTemplate
        Version: '$Latest'
      MinSize: '1'
      MaxSize: '3'
      DesiredCapacity: '2'
      Tags:
        - Key: Name
          Value: fellowship-asg
          PropagateAtLaunch: true
Enter fullscreen mode Exit fullscreen mode

Why you might choose one over the other:

  • Terraform shines when you need to manage resources across clouds or want access to the massive public module registry (think “one‑click VPC with NAT gateway”).
  • CloudFormation feels like a natural extension of the AWS console; changes are visible in the Stacks UI, and you get drift detection out of the box.

Pick the tool that matches your team’s comfort zone, but remember: the real win is treating infrastructure as version‑controlled, testable code.

Why This New Power Matters

Now you can spin up a whole environment for a feature branch, run integration tests against it, and tear it down when the PR merges—all without clicking through a dozen consoles. Imagine delivering a new microservice with a single terraform apply and knowing the exact same definition runs

Top comments (0)