Case study · June 2026
28 AWS Resources for $22/Month
Terraform · AWS EC2 · ALB · Auto Scaling · CloudWatch · GitHub Actions
What it builds
An internet-facing Application Load Balancer in front of an Auto Scaling Group of t3.micro instances running Amazon Linux 2023 and Apache, spread across two public subnets in us-east-1a and us-east-1b. The group runs a minimum of 1, desired 2 and maximum 4. CloudWatch alarms add an instance when CPU passes 70% and remove one when it drops below 20%.
The 28 resources
- Remote state: 5 (versioned, encrypted, public access blocked, native locking)
- Networking: 8 (VPC, two subnets, internet gateway, route tables)
- Security groups: 2
- Compute: 5 (IAM role and SSM profile, launch template, Auto Scaling Group)
- Load balancer: 4 (ALB, target group, listener, attachment)
- Scaling: 4 (CloudWatch alarms and scale-out and scale-in policies)
Design decisions
- S3 native state locking (
use_lockfile) instead of DynamoDB. - The AMI is resolved with a data source. A hardcoded AMI ID had broken an earlier Jenkins build.
- The instance security group allows port 80 only from the load balancer’s security group, not from a CIDR range.
- IMDSv2 is required. Access is through SSM Session Manager, so port 22 is never open.
- The ALB drops invalid header fields and uses defensive desync mitigation.
create_before_destroyon the Auto Scaling Group andprevent_destroyon the state bucket.
Cost
About $22.19 a month at typical load with two instances, and about $14.70 at minimum capacity with one. The load balancer is roughly $7.00, each t3.micro roughly $7.49, S3 state about a cent, and the two CloudWatch alarms about $0.20.
Five things that went wrong
mkdirwas missing. Ubuntu 26.04 uses the Rust-based uutils instead of GNU coreutils, andmkdirwasn’t linked. I worked around it with a Python one-liner.- I exposed credentials. A screenshot of
aws configureoutput showed an access key and secret. I rotated the key in the IAM console, and it’s a good argument for OIDC roles over static credentials. - AWS rejected an em dash. Security group rule descriptions have a restricted character set, and a
sedcommand replaced the dashes. - A file landed in the wrong directory. I created
variables.tfin my home directory instead of the project folder, then deleted the stray file and edited the right one. - CI failed on formatting.
terraform fmt -checkexited with code 3.terraform fmt -recursivefixed the files, and I updated the action versions to clear a Node.js 20 deprecation warning.
CI
GitHub Actions runs terraform fmt, validate and plan on every push and pull request.