rithik.ghantasala
← Work/Infrastructure2026

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
01The problem

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.

02How it works

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.

VPC 10.0.0.0/16PUBLIC · 10.0.1.0/24 + 10.0.2.0/24 · THE ONLY SUBNETS WITH PUBLIC IPSWEB (PRIVATE) · 10.0.11.0/24 + 10.0.12.0/24APP (PRIVATE) · 10.0.21.0/24 + 10.0.22.0/24 · INTERNAL ALB LIVES HEREDB (PRIVATE) · 10.0.31.0/24 + 10.0.32.0/24 · SINGLE-AZ RDSInternet:80 / :443External ALBinternet-facing · sg: ext-albNAT Gatewaypublic[0] only · single-AZ SPOFNginx · AZ-at3.microNginx · AZ-bt3.microinfo-server127.0.0.1:8080 · IMDSv2 → instance + AZInternal ALBlistener :80 → target :5000Flask · AZ-agunicorn -w 2Flask · AZ-bgunicorn -w 2Auto Scaling2 → 4 per tier · target 70% CPURDS MySQL 8.0db.t3.micro · publicly_accessible = false
CloudTask, drawn from the source, not the README. Scroll to trace the flow.
03Security chain

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.

  1. ext-alb-sgallows 80, 443from 0.0.0.0/0 (the only CIDR rule)
  2. web-sgallows 80from ext-alb-sg
  3. int-alb-sgallows 80from web-sg
  4. app-sgallows 5000from int-alb-sg
  5. db-sgallows 3306from app-sg
04Decisions worth defending

Why it is built this way

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

05What I took from it
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.
06Stack
TerraformAWS VPCEC2ALBAuto ScalingRDS MySQLNAT GatewayIAMNginxFlaskGunicorn

Next project

GlassBox

One AI endpoint, instrumented until nothing is hidden