Ship a Production Web App to AWS
The project that convinces recruiters: a containerized app on ECS Fargate behind HTTPS, a private database, all in Terraform, deployed by a pipeline with no long-lived keys. The app is simple, the engineering is the point.
From your mentor
Most grad portfolios are click-built toys. This one says: I can design a network, run containers, manage secrets, automate delivery, and tear it all down for cost. That’s the actual job, and it’s rare on a junior résumé.
What you’re building (and why recruiters care)
One coherent system that exercises every domain in this track, network, compute, storage, identity, delivery, the way a real team runs it.
Time
8–12 hours, spread over a week
Cost
~$1–2/day while running, destroy after each session (you’ll learn how)
Difficulty
Capstone, ties the whole AWS track together
What you’ll build
A containerized web app on ECS Fargate behind an HTTPS load balancer, with PostgreSQL on RDS in private subnets, secrets in Secrets Manager, all defined in Terraform and deployed by GitHub Actions via OIDC.
VPC · 2 Availability Zones
Public subnets
Private subnets
Delivery (no long-lived keys)
Nice, that’s the win
The three things you walk away with
A live HTTPS URL on your own domain, a clean GitHub repo with an architecture diagram and README, and a working terraform destroy, proof you understand cost. Those three are your interview.The architecture, decision by decision
Every choice here is a small piece of production judgment you can defend in an interview.
| Component | Choice | Why this is the production answer |
|---|---|---|
| Compute | ECS Fargate | Run containers without managing servers, the sweet spot between EC2 and full Kubernetes |
| Entry + TLS | ALB + ACM + Route 53 | HTTPS on a real domain; the load balancer is the only thing exposed |
| Database | RDS PostgreSQL (private subnets) | Managed, backed-up, and unreachable from the internet |
| Secrets | Secrets Manager | DB credentials never live in code, env files, or the task definition in plaintext |
| Identity | IAM task role + GitHub OIDC | The app and the pipeline get scoped, temporary credentials, no static keys |
| Infrastructure | Terraform | The entire stack is reproducible and reviewable, one command stands it up or tears it down |
| Delivery | GitHub Actions → ECR → ECS | Push to main builds the image and deploys it automatically |
| Operations | CloudWatch + Budget | Logs, a dashboard, an alarm, and a cost guardrail |
The build, phase by phase
Build it in this order, each phase has a checkpoint so you always know it worked before moving on. Never skip a checkpoint.
App in a container
Get the app running locally in Docker, the cloud doesn’t care what language, only that it’s a container.
✓ Checkpoint:docker run serves the app on localhost.
Network (as code)
Terraform a VPC: public + private subnets across 2 AZs, IGW, and a NAT gateway.
✓ Checkpoint:terraform apply creates the VPC; subnets show in the console.
Data tier (private)
RDS PostgreSQL in the private subnets, credentials in Secrets Manager, an S3 bucket for assets.
✓ Checkpoint:RDS is “available” and NOT publicly accessible; the secret exists.
Run the app
Push the image to ECR; run it on ECS Fargate in private subnets behind the ALB.
✓ Checkpoint:The ALB DNS name returns your app over HTTP; target group is healthy.
HTTPS + domain
An ACM certificate, a Route 53 record, an HTTPS listener, and an 80→443 redirect.
✓ Checkpoint:https://yourapp.com loads with a valid padlock.
The pipeline
GitHub Actions builds, pushes to ECR, and updates ECS, authenticating via an OIDC role, no static keys.
✓ Checkpoint:A push to main auto-deploys; there are zero AWS keys in the repo.
Production polish
Autoscaling on the ECS service, a CloudWatch dashboard + alarm, and an AWS Budget.
✓ Checkpoint:The service scales out under load; an alarm and a budget are active.
Do it as code from phase 2 on
Resist the urge to click. From the VPC onward, write it in Terraform, even though it’s slower at first. That’s the whole signal you’re sending, and it makes the teardown one command.The maturity moment: a pipeline with no keys
This is the part that makes a reviewer sit up, most juniors paste an AWS access key into GitHub secrets. You won't.
Instead of storing an AWS key, GitHub authenticates to AWS with OIDC: a trust relationship lets the workflow assume a scoped IAM role and receive short-lived credentials for that run only. Nothing long-lived ever leaves AWS.
AWS docs: create-open-id-connect-provider# register GitHub as an OIDC identity provider, the foundation of keyless CI$ aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com
Checkpoint, what you should see
A commit tomain triggers the workflow, it assumes the role, builds and pushes the image to ECR, and ECS rolls out the new task, and a search of your repo for AKIA finds nothing.Worth pondering
The win
The moment it’s real.
🎉 You did it
Cost discipline (this is a skill too)
Two resources actually bill: the NAT gateway and the load balancer (~$1–2/day). Treating teardown as routine is itself a thing to show off.
Terraform docs: destroy# end every session with this, the whole stack disappears, the bill stops$ terraform destroy
The habit that signals seniority
Stand it up to demo or develop, thenterraform destroy. Being able to rebuild from zero in minutes, and choosing to, to save money, is exactly the instinct teams want. Keep an AWS Budget alarm on as a backstop.When it breaks (it will)
When it doesn’t work (it happens, work down this list)
ALB returns 503
The ECS task is unhealthy or not registered, check the target group health and the container’s health-check path and port.
App can’t reach the database
The RDS security group must allow the ECS task’s security group on 5432; both must be in the VPC’s private subnets.
Task can’t pull the image
Private subnets need a NAT gateway (or VPC endpoints for ECR/S3) for the pull, and the execution role needs ECR permissions.
HTTPS won’t validate
The ACM cert must be validated via a Route 53 record and be in the same region as the ALB.
Pipeline “not authorized to AssumeRole”
The role’s trust policy must reference your GitHub OIDC provider and restrict the repo/branch in the condition.
Make it recruiter-ready
The build is half the project. The presentation is the half recruiters actually see.
The README that gets callbacks
Open with a one-line what + the live URL. Add an architecture diagram. List the production traits as bullets (private DB, HTTPS, IaC, OIDC, autoscaling, monitoring). Show the two commands to stand it up and tear it down. End with “what I’d do next” (multi-region, blue/green, WAF), it shows you know where the ceiling is.Interview talking points to rehearse
Why Fargate over EC2 or EKS · how the database stays private · how secrets reach the container · how CI authenticates without keys · how you’d make it highly available · what it costs and how you control it. You’ve touched every one of these by building it.In plain English
Next up
Starter scaffold (coming next)
A Terraform + GitHub Actions starter repo for this capstone is the next deliverable, so you start from a working skeleton and fill in each phase rather than from a blank file.