DEV Community

Timevolt
Timevolt

Posted on

The Terraform Awakens: My Journey from Manual Clicking to IaC Glory

The Quest Begins (The "Why")

Honestly, I used to treat cloud resources like a messy desk—spin up an EC2 instance here, toss a security group there, and pray nothing broke when I came back tomorrow. One day our CTO asked, “Can we replicate this whole environment for a demo in under an hour?” I stared at the AWS console, clicked through ten tabs, and realized I was basically playing a never‑ending game of Whac‑A‑Mole. The moment I missed a single tag, the demo environment drifted, and the QA team started filing tickets like they were sending owls to Hogwarts.

That was my dragon: manual, error‑prone, and utterly unscalable. I needed a way to describe infrastructure once, version it like code, and spin it up (or tear it down) with a single command. Enter Infrastructure as Code.

The Revelation (The Insight)

The first time I wrote a Terraform file and saw terraform apply create a VPC, two subnets, and an EC2 instance all at once, I felt like I’d just unlocked a new spell in a RPG. The secret? Declarative language. Instead of telling the cloud how to build each piece, I declared what I wanted, and the provider figured out the order.

It clicked when I compared it to building LEGO sets: you don’t snap each brick individually while hoping you didn’t miss a step; you follow the instruction manual, and the model appears. Terraform (and CloudFormation) are those manuals, but they live in Git, so you get pull requests, code reviews, and the sweet satisfaction of a green CI pipeline.

Wielding the Power (Code & Examples)

The Struggle: Click‑Ops Bash Script

Before IaC, I’d cobbled together a Bash script that used the AWS CLI. It looked something like this (simplified for brevity):

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

# Tag VPC
aws ec2 create-tags --resources $VPC_ID --tags Key=Name,Value=my-demo-vpc

# Create 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 --query 'Subnet.SubnetId' --output text)
aws ec2 create-tags --resources $SUBNET_ID --tags Key=Name,Value=public-subnet

# ... and so on for route tables, security groups, EC2, etc.
Enter fullscreen mode Exit fullscreen mode

The script was fragile. If any command failed, I had to manually clean up half‑created resources. Tagging was easy to forget, and reproducing the exact same setup in another region meant copying and pasting, then hoping I didn’t miss a variable.

The Victory: Terraform (HCL)

Here’s the same setup in Terraform—declarative, idempotent, and ready for version control:

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

resource "aws_vpc" "demo" {
  cidr_block           = "10.0.0.0/16"
  tags = {
    Name = "demo-vpc"
  }
}

resource "aws_internet_gateway" "demo" {
  vpc_id = aws_vpc.demo.id
  tags = {
    Name = "demo-igw"
  }
}

resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.demo.id
  cidr_block        = "10.0.1.0/24"
  map_public_ip_on_launch = true
  tags = {
    Name = "public-subnet"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.demo.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.demo.id
  }
  tags = {
    Name = "public-rt"
  }
}

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.demo.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 = {
    Name = "web-sg"
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0" # Amazon Linux 2
  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

Run terraform init && terraform apply and—boom—you get a fully networked VPC with a web server, all tagged, all ready to be destroyed with terraform destroy. No manual cleanup, no forgotten tags, and the whole thing lives in a .tf file you can review, branch, and merge like any other code.

Traps to Avoid

  1. Hardcoding IDs – In the Bash script I once grabbed an AMI ID from the console and pasted it. When the region changed, the script broke. In Terraform, use data sources or variables:
   data "aws_ami" "amazon_linux" {
     most_recent = true
     owners      = ["amazon"]
     filter {
       name   = "name"
       values = ["amzn2-ami-hvm-*-x86_64-gp2"]
     }
   }
Enter fullscreen mode Exit fullscreen mode

Then reference data.aws_ami.amazon_linux.id.

  1. State Drift – If you edit resources directly in the console after terraform apply, Terraform will try to revert them on the next run. Treat the console as read‑only for IaC‑managed resources, or import existing items with terraform import before you start.

CloudFormation Flavors

If you’re all‑in on AWS native tooling, CloudFormation does the same thing in JSON/YAML. Here’s a tiny snippet for the VPC and subnet:

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

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref DemoVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: public-subnet
Enter fullscreen mode Exit fullscreen mode

The principle stays identical: declare, version, apply.

Why This New Power Matters

Now I can spin up a full staging stack in under five minutes, run integration tests against it, and tear it down when the feature branch merges. My teammates review the Terraform changes just like they review application code—no more “it works on my machine” excuses. When we need to replicate the production environment for a disaster‑recovery drill, we just point the same code at a different AWS account and hit apply.

The real win? Confidence. Knowing that the exact same configuration that passed review is what’s running in production eliminates a whole class of “we forgot to set that tag” bugs. It’s like having a safety net that also lets you fly higher.

Your Turn

Grab a small piece of your current setup—maybe a single S3 bucket or a Lambda function—and write a Terraform (or CloudFormation) file for it. Commit it, open a pull request, and watch your CI pipeline plan and apply the change. Then try destroying it with a single command and see how cleanly everything disappears.

What’s the first resource you’ll bring under IaC control? Share your win (or your hilarious first‑time‑gotcha) in the comments—I’d love to hear your war stories!


Happy coding, and may your stacks always be *plan-ted correctly.*

Top comments (0)