CloudTask
A three-tier AWS application, provisioned entirely as code
A task-management application deployed across eight subnets in two availability zones, defined in Terraform. Traffic flows from an internet-facing load balancer through an Nginx web tier to an internal load balancer and a Flask API tier, backed by RDS MySQL. All compute sits in private subnets with no public addresses and no SSH access, reachable only through Session Manager, with five chained security groups enforcing tier-to-tier isolation.
Verified: each of these is checkable in the repository
- 8
- subnets across 2 availability zonespublic, web, app and db tiers × 2 AZs
- 5
- chained security groups, ingress by SG referenceext-alb → web → int-alb → app → db
- 2→4
- instances per tier under CPU loadtarget tracking at 70% average CPU
- 0
- SSH keys, bastions or open port 22AmazonSSMManagedInstanceCore only
Three-tier architecture diagrams are easy to draw and easy to get subtly wrong. I wanted to build one where the isolation was real, where I could point at the security group rule that makes the database unreachable from the web tier, rather than asserting it.
Terraform provisions a VPC with four tiers of subnets across two availability zones: public for the external load balancer and NAT, then private web, app and database tiers. Nginx serves static content and proxies API paths to an internal load balancer, which forwards to a Flask tier on port 5000, which talks to RDS MySQL. Each tier's security group accepts traffic only from the previous tier's security group by reference, never by CIDR. Both compute tiers auto-scale on CPU, and instances are stateless so they can be replaced.
Five groups, each admitting only the tier before it
Every rule names the previous tier's security group as its source rather than a CIDR block, so the chain holds as subnets change. Note that this is ingress only, all five groups allow unrestricted egress.
- ext-alb-sgallows 80, 443from 0.0.0.0/0 (the only CIDR rule)
- web-sgallows 80from ext-alb-sg
- int-alb-sgallows 80from web-sg
- app-sgallows 5000from int-alb-sg
- db-sgallows 3306from app-sg
Why it is built this way
01Ingress by security group reference, not CIDR
Each rule names the previous tier's security group as its source. The chain holds as subnets change, and the intent is legible in the code rather than encoded in address ranges.
02No SSH anywhere
No key pairs, no bastion, no port 22 in any security group. Access is Session Manager through an instance profile, which removes a whole class of exposure and the key management that goes with it.
03The internal load balancer lives in the app subnets
Not the web subnets, which is the more common drawing. It sits inside the tier it fronts, so the web tier's only route to the application is through it.
04Instance identity surfaced to the browser
A small local service queries instance metadata over IMDSv2 and returns which instance and availability zone served the request, proxied through Nginx. It makes load balancing and failover visible instead of theoretical.
05Stateless tiers so scaling is safe
Neither compute tier holds state, so the auto-scaling group can replace an instance without data loss, and the target-tracking policy can act without coordination.
Reading my own Terraform line by line taught me more than writing it did. Tracing every route table and security group is how you find out what a diagram actually guarantees, which rule makes the database unreachable, which single resource everything else quietly depends on. I now size the claim to what the code enforces, because that is the version that holds up in a design conversation.
Next project
GlassBox
One AI endpoint, instrumented until nothing is hidden