The Quest Begins (The "Why")
Honestly, I used to spend Friday nights clicking through the AWS console, spinning up EC2 instances, attaching security groups, and hoping I didn’t forget to tag anything. It felt like playing Whac‑A‑Mole with a blindfold on—every time I thought I had the environment stable, a teammate would spin up a new resource in a different region, and suddenly our drift report looked like modern art.
One Monday morning, after a three‑hour outage caused by a missing IAM policy that someone had manually deleted (yes, really), I stared at the CloudTrail logs and thought: there has to be a better way. I wanted a single source of truth, something version‑controlled, repeatable, and—dare I say—fun to work with. That’s when I stumbled into the world of Infrastructure as Code (IaC).
The Revelation (The Insight)
The magic moment came when I realized IaC isn’t just about writing JSON or YAML; it’s about treating your infrastructure like application code. You get linting, code reviews, pull requests, and the ability to roll back a bad change with a simple git revert.
Two heavyweight contenders emerged for my AWS‑centric workflow: Terraform (cloud‑agnostic, HCL syntax) and CloudFormation (native AWS, JSON/YAML). Instead of picking a side, I decided to learn both and see where each shines. The insight? Use Terraform for the foundation—networking, IAM roles, and cross‑account resources—then let CloudFormation handle the application‑specific pieces that rely heavily on AWS‑only features like Lambda@Edge or Aurora Serverless v2.
Wielding the Power (Code & Examples)
The Struggle: ClickOps & Manual Scripts
Here’s a snippet of the Bash script I used to provision a VPC, an IGW, and a couple of subnets. Spoiler: it was fragile, hard to test, and impossible to peer review.
#!/bin/bash
# create-vpc.sh – the old way
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames
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
# Public subnet
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 > /tmp/public-subnet-id
PUBLIC_SUBNET_ID=$(cat /tmp/public-subnet-id)
aws ec2 modify-subnet-attribute --subnet-id $PUBLIC_SUBNET_ID --map-public-ip-on-launch
# Private subnet
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone us-east-1a \
--query 'Subnet.SubnetId' --output text > /tmp/private-subnet-id
PRIVATE_SUBNET_ID=$(cat /tmp/private-subnet-id)
# Route table for public subnet
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 $PUBLIC_SUBNET_ID --route-table-id $RT_ID
Running this felt like defusing a bomb—one typo and you’d end up with a dangling IGW or a subnet without a route.
The Victory: Terraform for the Base Network
Now, the same architecture lives in a few lines of HCL. Terraform keeps state, lets me plan changes, and gives me a beautiful graph of dependencies.
# main.tf – VPC with public & private subnets
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "demo-vpc"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "demo-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 = "demo-public"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "demo-private"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = {
Name = "demo-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.igw.id
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Why this feels like a win:
-
terraform planshows me exactly what will change before I apply. - If I forget to attach the IGW, the plan fails fast—no more guessing.
- The whole file lives in Git; a teammate can review it, suggest a tag, or add a new subnet with a simple pull request.
CloudFormation for Application‑Specific Goodies
When I needed to deploy a Lambda@Edge function that modifies CloudFront headers, CloudFormation’s native support for Lambda@Edge made life easier. Terraform can do it, but the CloudFormation syntax felt more straightforward for this AWS‑only feature.
# lambda-edge.template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda@Edge to add security headers
Resources:
EdgeLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Code:
ZipFile: |
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
headers['strict-transport-security'] = [{key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload'}];
headers['content-security-policy'] = [{key: 'Content-Security-Policy', value: "default-src 'self';"}];
callback(null, response);
}
Role: !GetAtt EdgeLambdaRole.Arn
EdgeLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: [lambda.amazonaws.com, edgelambda.amazonaws.com]
Action: ['sts:AssumeRole']
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
EdgeLambdaVersion:
Type: AWS::Lambda::Version
Properties:
FunctionName: !Ref EdgeLambdaFunction
Description: v1.0
EdgeLambdaAlias:
Type: AWS::Lambda::Alias
Properties:
FunctionName: !Ref EdgeLambdaFunction
Name: LIVE
FunctionVersion: !GetAtt EdgeLambdaVersion.Version
I stack this template inside a larger CloudFormation pipeline that also creates the CloudFront distribution, attaches the Lambda@Edge, and sets up the required IAM permissions. The best part? I can version‑control this YAML alongside my Terraform modules, and a single CI pipeline runs terraform apply followed by aws cloudformation deploy.
Traps to Avoid (The “Treats” on the Quest)
Hardcoding IDs – In early Terraform attempts I copied subnet IDs from the console into variables. Never. Let Terraform create and reference resources via attributes (
aws_subnet.public.id). Hardcoding leads to drift and painful refactors.Missing Dependencies in CloudFormation – I once forgot to add a
DependsOnattribute for the Lambda@Edge version, causing the CloudFront distribution to reference a$LATESTversion that wasn’t published yet. The fix was simple: addDependsOn: EdgeLambdaVersion. Always verify that downstream resources wait for the ones they need.
Why This New Power Matters
Now my infrastructure feels less like a haunted house and more like a well‑orchestrated symphony. I can spin up a complete dev environment in under five minutes, destroy it just as fast, and know that every change is traceable. Code reviews catch missing tags, over‑permissive security groups, or stray resources before they ever hit production.
The real win? Confidence. When a production incident occurs, I no longer waste hours hunting for a manually‑changed NACL; I look at the Git history, see the offending commit, roll back, and breathe.
And the best part? The skills are transferable. The same Terraform modules that build a VPC for a web app can be reused for a data‑pipeline workload, and the CloudFormation snippets I wrote for Lambda@Edge can be adapted for any edge‑compute scenario.
Your Turn – Start Your Own Quest
Ready to trade the click‑fatigue for code‑driven clarity? Pick a small piece of your environment—maybe a single S3 bucket with a static website—and write a Terraform module for it. Then, try wrapping a Lambda function in a CloudFormation template. Share your repo, ask for feedback, and watch how quickly the whole team starts to trust the process.
What’s the first IaC spell you’ll cast? Drop a link to your repo in the comments—I can’t wait to see what you build! 🚀
Top comments (0)