Infrastructure as Code: Terraform and CloudFormation – The Matrix Reloaded

# devops# docker# kubernetes# cicd
Infrastructure as Code: Terraform and CloudFormation – The Matrix ReloadedTimevolt

The Quest Begins (The "Why") Honestly, I used to feel like I was stuck in a never‑ending...

The Quest Begins (The "Why")

Honestly, I used to feel like I was stuck in a never‑ending loop of clicking through the AWS console, copying CLI commands into a wiki, and praying that nothing changed between environments. One Friday night, after yet another “oops, I forgot to tag that subnet” incident that took down our staging stack, I stared at my screen and thought: there has to be a better way.

That moment was my call to adventure. I realized the real dragon I needed to slay wasn’t a rogue Lambda function—it was manual, undocumented infrastructure. If I could codify everything, I could version‑control it, review it with peers, and spin up identical environments in minutes instead of hours. The promise of Infrastructure as Code (IaC) sounded like finding the One Ring: one tool to rule them all.

The Revelation (The Insight)

The magic click came when I discovered that IaC isn’t just about writing scripts—it’s about declaring the desired state of your world and letting the tool figure out how to get there. Terraform and AWS CloudFormation both speak that language, but they have different flavors.

Terraform feels like picking up a lightsaber: it’s elegant, works across multiple clouds, and its HCL syntax reads like plain English. CloudFormation, on the other hand, is the trusty blaster you already have in your holster if you live entirely in AWS—it’s native, tightly integrated, and understands every AWS service out of the box.

The insight? Choose the tool that matches your quest. If you’re juggling AWS, Azure, and GCP, Terraform is your party’s wizard. If you’re a pure‑AWS squad and want deep service parity, CloudFormation is your seasoned warrior.

Wielding the Power (Code & Examples)

Before: The Manual Struggle

Here’s what a typical “create a web tier” looked like in our old runbook:

  1. Open the AWS console → VPC → Create VPC (10.0.0.0/16).
  2. Create two public subnets (10.0.1.0/24, 10.0.2.0/24).
  3. Attach an Internet Gateway, add routes.
  4. Launch an EC2 instance in each subnet, assign a security group that opens port 80.
  5. Tag everything with Environment=staging, Owner=dev-team.

Miss a step? You’ll spend the next hour hunting down why the instance can’t reach the internet.

After: Terraform Spell

# main.tf
provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Environment = "staging"
    Owner       = "dev-team"
  }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = {
    Environment = "staging"
  }
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
  map_public_ip_on_launch = true
  tags = {
    Environment = "staging"
    Owner       = "dev-team"
  }
}

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 = {
    Environment = "staging"
  }
}

resource "aws_route_table_association" "assoc" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "web_sg" {
  name        = "web-sg"
  description = "Allow HTTP inbound"
  vpc_id      = aws_vpc.main.id
  ingress {
    from_port   = 80
    to_port     = 80
    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 = {
    Environment = "staging"
  }
}

resource "aws_instance" "web" {
  count           = 2
  ami             = "ami-0c55b159cbfafe1f0" # Amazon Linux 2
  instance_type   = "t3.micro"
  subnet_id       = aws_subnet.public[count.index].id
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  tags = {
    Environment = "staging"
    Owner       = "dev-team"
    Name        = "web-${count.index}"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run terraform init, terraform plan, then terraform apply. Boom—your VPC, subnets, IGW, route table, security group, and two EC2 instances appear, all tagged correctly. If you change the CIDR block, Terraform computes the diff and updates only what’s needed.

Trap #1 – Forgetting to lock provider versions.

If you omit required_version or required_providers, a later provider update can silently change behavior. Always pin:

terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Trap #2 – Hard‑coding secrets.

Never put keys or passwords directly in .tf files. Use AWS Secrets Manager or Parameter Store and reference them with data.aws_secretsmanager_secret_version.

After: CloudFormation Incantation

If you prefer staying within the AWS ecosystem, the same stack in YAML looks like this:

AWSTemplateFormatVersion: '2010-09-09'
Description: Staging web tier – VPC, subnets, IGW, route table, SG, EC2

Parameters:
  Env:
    Type: String
    Default: staging
  Owner:
    Type: String
    Default: dev-team

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Environment
          Value: !Ref Env

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

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Environment
          Value: !Ref Env

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

  SubnetRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref RouteTable

  SubnetRouteTableAssociationB:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetB
      RouteTableId: !Ref RouteTable

  WebSG:
    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: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner

  WebInstanceA:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c55b159cbfafe1f0
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnetA
      SecurityGroupIds:
        - !Ref WebSG
      Tags:
        - Key: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner
        - Key: Name
          Value: web-a

  WebInstanceB:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c55b159cbfafe1f0
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnetB
      SecurityGroupIds:
        - !Ref WebSG
      Tags:
        - Key: Environment
          Value: !Ref Env
        - Key: Owner
          Value: !Ref Owner
        - Key: Name
          Value: web-b
Enter fullscreen mode Exit fullscreen mode

Deploy with aws cloudformation deploy --template-file template.yaml --stack-name staging-web --capabilities CAPABILITY_NAMED_IAM.

Trap #1 – Ignoring change sets.

Running aws cloudformation deploy without reviewing the change set can surprise you with unexpected deletions. Always peek at aws cloudformation create-change-set first.

Trap #2 – Over‑using intrinsic functions.

While Fn::Join and Fn::Sub are handy, nesting them too deep makes the template unreadable. Keep it simple; if you find yourself writing a one‑liner that spans three lines, break it out into a parameter or a macro.

Why This New Power Matters

Now that I treat infrastructure like code, my workflow feels like I’ve leveled up from a novice adventurer to a seasoned guild master:

  • Speed: Spin up a fresh environment in under five minutes. No more “let me just click that…”.
  • Safety: Peer reviews catch misconfigurations before they hit prod. The plan/output shows exactly what will change.
  • Consistency: Every environment—dev, staging, prod—is a carbon copy (modulo variables).
  • Documentation: The code is the documentation. New hires can read the repo and understand the architecture instantly.

If you’re still wrestling with manual consoles or ad‑hoc scripts, give IaC a shot. Pick the tool that fits your world—Terraform for multi-cloud flexibility, CloudFormation for deep AWS integration—and start small: version‑control a single S3 bucket or a security group.

Your Turn

Here’s a challenge: fork a simple “hello‑world” Terraform module (or CloudFormation stack) that creates an S3 bucket with versioning enabled, then add a Lambda function that logs every PUT event. Share your repo link in the comments and let’s see who can make the most creative twist—maybe a bucket that triggers a Step Function, or a bucket that serves a static React app.