Skip to content

Phase 3 · CLOUD

Project 2, Day 1: plan the AWS architecture

Day 62 of 90 ~60 min 0/20 in phase Builds on Day 61

By the end of today

  • Design the linkstash AWS topology: VPC, public/private subnets across two AZs
  • Justify public-vs-private placement and chain security groups by reference
  • Estimate the monthly cost and list every resource you'll build

Planning a production AWS architecture: public edge, private core

Section 1 of 5 · ~3 min

Welcome to Project 2 — the Phase 3 capstone. Over four days you take linkstash, the FastAPI URL shortener you built and shipped in Project 1 (image ghcr.io/pushkar/linkstash:v1.0.0), and stand it up on AWS the way a real team would: private compute, a managed database, a load balancer at the edge, DNS with TLS, and monitoring. Today you build nothing billable — you design. A day spent drawing the topology and pricing it is what separates a deploy that works from one that surprises you on the invoice.

The target architecture is the standard production shape. One VPC (10.0.0.0/16) spanning two Availability Zones. In each AZ, one public subnet and one private subnet — four subnets total. The only thing facing the internet is an Application Load Balancer in the two public subnets. Behind it, in the private subnets, an ECS Fargate service runs the linkstash container; alongside it an RDS PostgreSQL instance (db.t4g.micro) holds the links table. Route 53 answers DNS for your domain and ACM issues the TLS certificate the ALB terminates. CloudWatch collects logs, metrics and alarms across the whole stack.

Why public-vs-private placement matters. The ALB is the single front door — it needs a public IP, so it lives in public subnets with a route to an internet gateway. Everything valuable — your app and your database — sits in private subnets with no inbound internet route. Nobody on the internet can open a socket to the Fargate task or to Postgres; they reach only the ALB, which forwards to the app. The app still needs outbound internet to pull its image and call AWS APIs, so the private subnets egress through a NAT gateway in a public subnet — outbound only. This layered edge/core split is exactly what the AWS Well-Architected Framework prescribes in its security and reliability pillars.

Why two AZs. An Availability Zone is an isolated datacentre. Spreading subnets across us-east-1a and us-east-1b means one AZ failing doesn’t take linkstash down: the ALB routes to the healthy task, and RDS can run a standby in the second zone. Two is the minimum an ALB requires, and the cheapest real answer to “what if a datacentre catches fire?”

Real world: Think of a bank branch. The lobby (public subnet) opens to the street through one guarded door (the ALB). The vault and back office (private subnets) have no street door at all — staff reach the outside only through a controlled service exit (the NAT gateway). Customers transact at the counter; they never walk into the vault.

Security groups enforce the same story at the packet level, chained by reference not by IP: the ALB group allows 443 from anywhere; the app group allows the container port (8000) from the ALB group only; the RDS group allows 5432 from the app group only. Each tier trusts exactly one caller.

Users reach an Application Load Balancer in the VPC's public subnets over TLS; the ALB forwards to an ECS Fargate service running linkstash in the private subnets; the Fargate task talks to RDS Postgres on 5432, also private, and egresses to the internet through a NAT gateway. The VPC spans two Availability Zones. VPC 10.0.0.0/16 · two Availability Zones users the internet ALB public · TLS :443 Fargate service linkstash · private RDS Postgres db.t4g.micro · private NAT gateway (egress only) :5432
Only the ALB faces the internet; the app and database stay private, and outbound traffic leaves through the NAT gateway.

Today’s job is to lock this design down — the CIDR plan, the resource list, and a monthly cost estimate — so the next three days are execution, not improvisation.

Hands-On Lab

Section 2 of 5 · ~4 min

Budget about 30 minutes. You’ll drive this from the AWS CLI v2 as your IAM user (not root), in the us-east-1 region, with Docker running locally. Nothing here creates a billable resource — today is design plus two read-only checks. Account IDs, ARNs and image digests are unique to you — yours will differ from the samples.

What this costs: ₹0 today. This is a planning day: aws sts get-caller-identity, listing AZs, and pulling an image you already built are all free, and you create zero AWS resources. The spend starts on Day 3, when the first billable pieces come online — the NAT gateway ($0.045/hr), the ALB (~$0.0225/hr), RDS db.t4g.micro, and the Fargate task. The db.t4g.micro has a Free Tier allowance on eligible accounts, but the ALB and NAT gateway do not — so from Day 3 this stack bills by the hour whether or not anyone visits. That is exactly why Day 4 ends by tearing everything down. The cost table you build in step 7 is the number to remember.

# 1. Confirm you're your IAM user (NOT root) and pin the region for the whole project.
aws sts get-caller-identity
export AWS_DEFAULT_REGION=us-east-1
# Output (your Account and Arn will differ):
# {
#     "UserId": "AIDA...EXAMPLE",
#     "Account": "123456789012",
#     "Arn": "arn:aws:iam::123456789012:user/devops-you"
# }
# 2. Confirm the region has at least two Availability Zones (the ALB and RDS both need two).
aws ec2 describe-availability-zones \
  --query 'AvailabilityZones[].ZoneName' --output text
# Output (us-east-1 has six; you'll use the first two):
# us-east-1a	us-east-1b	us-east-1c	us-east-1d	us-east-1e	us-east-1f
# 3. Pull the linkstash image you shipped on Day 45 — Day 3 re-tags and pushes it to ECR.
docker pull ghcr.io/pushkar/linkstash:v1.0.0
# Output (the digest is yours-will-differ):
# v1.0.0: Pulling from pushkar/linkstash
# Digest: sha256:9f0c...e21a
# Status: Downloaded newer image for ghcr.io/pushkar/linkstash:v1.0.0

Write the CIDR plan below into ~/linkstash/deploy/aws/PLAN.md. Four subnets, one public and one private per AZ — the address plan Day 2 will execute exactly:

# VPC & subnet plan (us-east-1)
VPC          10.0.0.0/16
public-a     10.0.0.0/24    us-east-1a    ALB, NAT gateway
public-b     10.0.1.0/24    us-east-1b    ALB
private-a    10.0.10.0/24   us-east-1a    Fargate task + RDS primary
private-b    10.0.11.0/24   us-east-1b    Fargate task + RDS standby

The security-group plan chains each tier to the next by reference — no raw IPs except the public edge. linkstash’s Uvicorn listens on 8000:

# Security-group chain (tightest scope that still works)
alb-sg   inbound  443 from 0.0.0.0/0        (80 -> redirect to 443)
app-sg   inbound  8000 from alb-sg only     (never from the internet)
rds-sg   inbound  5432 from app-sg only     (never from alb-sg or the internet)

This is the build order for the next three days — read it as your checklist:

# Resource inventory
Day 2  network & IAM  : VPC, IGW, 4 subnets, public/private route tables,
                        ECS task-execution role + task role, the 3 security groups
Day 3  deploy         : NAT gateway, ECR repo + image push, RDS Postgres,
                        target group, ALB + HTTPS listener, ECS cluster + Fargate service
Day 4  DNS/TLS/monitor: Route 53 hosted zone, ACM certificate, CloudWatch alarms
                        ... then TEAR DOWN everything above

Now estimate the monthly bill if this stack were left running 24/7 (730 hrs). These are us-east-1 2026 list rates — yours will differ with traffic and account terms:

# Monthly cost estimate (left running 24/7 — the number Day 4 avoids)
Resource                         Rate                    ~Monthly
Application Load Balancer         $0.0225/hr + LCUs        ~$18
NAT gateway                       $0.045/hr + $0.045/GB    ~$33
Fargate task (0.25 vCPU/0.5 GB)   ~$0.0123/hr              ~$9
RDS db.t4g.micro + 20 GB gp3      ~$0.016/hr + storage     ~$14  (Free-Tier eligible)
Public IPv4 (ALB 2 AZs + NAT GW)  $0.005/hr × 3 addrs       ~$11
Route 53 hosted zone              $0.50/zone-month         ~$0.50
ACM public certificate            free                     $0
CloudWatch logs + alarms          usage-based              ~$2
                                                    TOTAL  ~$88 / month
# 4. Prove nothing is billing yet — you created zero AWS resources today.
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName' --output text
# Output — both empty; the design lives only in PLAN.md until Day 2:
#
#

Read the plan back: four subnets across two AZs, a public edge and a private core, three security groups chained by reference, and an ~$88/month price tag that exists only if you forget to tear down. Every command tomorrow traces back to this page — that is what a planning day buys you.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These are the design mistakes this planning day exists to prevent — each surfaces as a real AWS error the moment you try to build a bad plan. Read the text slowly; parsing it is the actual skill.

Common error: Planning both public subnets in the same AZ, then trying to create the ALB on Day 3:

An error occurred (ValidationError) when calling the CreateLoadBalancer operation: At least two subnets in two different Availability Zones must be specified

Why: An Application Load Balancer is a regional, multi-AZ resource by design — it demands subnets in at least two Availability Zones so it can survive one zone failing. A CIDR plan that puts public-a and public-b both in us-east-1a looks fine on paper but can’t carry an ALB.

Fix: Assign one public subnet per AZ (public-aus-east-1a, public-bus-east-1b), exactly as the step-4 plan does. Pick the AZs today so Day 2 creates them in the right zones.

How you’d spot it in prod: A load balancer that refuses to create, or a Terraform plan failing on subnets, almost always means the subnet list doesn’t span two AZs — check the AZ of each subnet, not just that there are two of them.

Common error: Running any resource-creating command before a region is set (no AWS_DEFAULT_REGION, no --region, and none configured):

You must specify a region. You can also configure your region by running "aws configure".

Why: Almost every AWS API call is region-scoped, and the CLI has no safe default. Without a region it can’t know whether you mean us-east-1 or eu-west-2, so it refuses rather than guess — which would scatter resources across regions.

Fix: export AWS_DEFAULT_REGION=us-east-1 (lab step 1) for the whole session, or pass --region us-east-1 per command. Keeping every day of this project in one region is what lets Day 4’s teardown find everything.

How you’d spot it in prod: Resources that “vanish” — created successfully but not visible in the console — are usually in a different region than the one you’re looking at. A pipeline that works locally but fails in CI often has the region set in your shell but not the runner’s.

Common error: Planning a single private subnet (or two in the same AZ) for RDS, then creating the DB subnet group on Day 3:

An error occurred (DBSubnetGroupDoesNotCoverEnoughAZs) when calling the CreateDBSubnetGroup operation: The DB subnet group doesn't meet Availability Zone coverage requirement. Please add subnets to cover at least 2 AZs.

Why: RDS requires a subnet group spanning at least two AZs even for a single-AZ instance, because that’s the prerequisite for ever enabling Multi-AZ failover. A plan with only one private subnet, or two in the same zone, can’t host RDS.

Fix: Put private-a in us-east-1a and private-b in us-east-1b (step-4 plan) and add both to the DB subnet group on Day 3. This is the same two-AZ discipline the ALB needs — plan it once, both services are satisfied.

How you’d spot it in prod: An RDS create or a Terraform aws_db_subnet_group failing on AZ coverage means the subnet group is single-zone. Fix the network plan, not the database — RDS is telling you the subnets are wrong.

AWS Architecture Interview Questions

Section 4 of 5 · ~1 min

These four are what a screening round asks once “can you launch a server?” becomes “can you design a system?” — cover each answer, say your own version out loud first, then compare, because recalling before revealing is what makes it stick. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~30 more minutes today:

  • 10 min — Skim the AWS Well-Architected Framework reliability and security pillars, and note how today’s public-edge / private-core, two-AZ design is the pattern they recommend by default.
  • 10 min — Rebuild the step-7 estimate in the AWS Pricing Calculator for this exact stack (ALB, one Fargate task, db.t4g.micro, one NAT gateway). Watch how much the NAT gateway and ALB dominate the total.
  • 5 min — Read AWS’s VPC interface endpoints for ECR and see how they can replace the NAT gateway for image pulls — the single biggest lever on this design’s cost.
  • 5 min — Read the VPC subnet sizing guidance and sanity-check the /24 blocks in your CIDR plan against how many addresses AWS reserves per subnet.
Why put the load balancer in public subnets but the containers and database in private subnets? Both

The load balancer is the only thing that should accept connections from the internet, so it goes in public subnets with a route to an internet gateway and a public IP. The app and database hold your logic and data — nothing on the internet should be able to open a socket to them, so they sit in private subnets with no inbound internet route. Traffic reaches the app only by passing through the ALB, where I can put TLS and a WAF. It's defence in depth: even if the app has a bug, the blast radius is one tier, because the database is unreachable except from the app tier.

Why span the architecture across two Availability Zones? Product

An Availability Zone is an isolated datacentre with its own power and network. If I put everything in one AZ and that AZ has an outage — which does happen — linkstash goes down completely. Spreading the subnets across two AZs means the ALB can route to a healthy Fargate task in the surviving zone, and RDS can fail over to a standby in the other AZ. Two AZs is also the minimum an Application Load Balancer requires. It removes a whole class of single-datacentre failure while adding almost nothing to the bill — subnets and route tables are free. It's the cheapest meaningful step up in reliability.

If the containers are in private subnets, how do they pull their image and reach AWS APIs? Product

Through a NAT gateway. A private subnet has no inbound internet route, but the Fargate task still needs outbound access — to pull its image and reach AWS APIs and CloudWatch. I put a NAT gateway in a public subnet and point the private route table at it for 0.0.0.0/0, so outbound connections work while nothing can initiate a connection inward. The cheaper alternative for AWS-only traffic is VPC interface endpoints for ECR and CloudWatch, which keep that traffic on the AWS network and let me drop the NAT gateway entirely — a real saving, since the NAT gateway is one of this design's biggest always-on charges.

Which resources in this design cost money even when no one is using linkstash? Both

The always-on ones bill by the hour regardless of traffic: the Application Load Balancer (roughly $0.0225/hour), the NAT gateway (about $0.045/hour plus per-GB), the RDS instance while it runs, and each public IPv4 address at $0.005/hour. Fargate bills per second only while a task runs, so scaling to zero tasks stops that charge — but the ALB and NAT gateway keep ticking. That's why an idle demo stack still costs real money, and why Day 4 of this project tears everything down — the ALB, the Fargate service, RDS, the NAT gateway and the Route 53 zone — rather than just stopping it. On AWS, 'stopped' rarely means 'free'.

Mark Day 62 complete

Tomorrow you build the network and IAM groundwork: the VPC, four subnets across two AZs, route tables, and the ECS task roles.

Stuck on today’s lab? Ask in Mission 90 Q&A