Project 1 — A Production Django API
coreintermediateThe first project is the one most backend engineers are actually asked to build: a containerised Django API behind a load balancer, with a managed database, a cache, object storage and no secrets in the image. Build it in seven milestones, and finish each one with something you can demonstrate rather than something you have configured.
Think of it as
Build outside-in and keep it reachable at every step. A hello-world container behind a load balancer on day one gives you a working deployment pipeline to add to; a perfect task definition with nothing serving traffic gives you a debugging session. Every milestone here ends with a request you can make from a browser.
What we're doing: Sequence the build so every milestone leaves something working.
- 3
- Getting the network and the deployment path working with a trivial container is the single highest-value first day, because everything later is debugged through it.
- 12
- `--source-group` rather than a CIDR is what stops a future workload in the same address range inheriting database access.
- 20
- Gating the service update on the migration exit code turns "we ran migrations" into a release step that can fail safely.
Why this works: Building outside-in means every milestone is verifiable from a browser or a single command, so a mistake is caught while only one thing has changed. The alternative — assembling the whole task definition, database, cache and secrets before the first request — produces a failure with six plausible causes and no way to bisect them.
Putting the database password in the environment block
Wrong
Better
What you see: The password is visible to anyone with `ecs:DescribeTaskDefinition`, is committed to the repository holding the template, and appears in CloudFormation change sets and CI logs — with no way to rotate it that does not require a deployment.
Why: The `environment` block is part of the task definition, which is a readable API object and usually also a file in version control. The `secrets` block stores only a reference: the value is fetched by the execution role at start and never becomes part of the definition, so rotating it is a Secrets Manager operation rather than a release.
- An architecture diagram of a Django API on AWS, read top to bottom.
- A browser resolves the domain through Route 53 and reaches an Application Load Balancer in the public subnets across two Availability Zones.
- The load balancer forwards to ECS Fargate web tasks in private subnets, running gunicorn, spread across the same two zones.
- The web tasks read and write RDS PostgreSQL with a Multi-AZ standby, use ElastiCache Redis for caching and sessions, and put user uploads into an S3 bucket.
- A separate set of ECS worker tasks runs Celery, consumes from the same Redis or an SQS queue, and has no load balancer in front of it.
- Secrets Manager supplies the database password and API keys to both task sets at start time, by ARN.
- CloudWatch collects logs, metrics and alarms from every component.
- A one-off migration task runs before the new service revision rolls out; static assets are served from S3 through CloudFront.
Seven milestones, each ending in something you can demonstrate
Together
Remember: Build outside-in: a reachable hello-world before anything else, then the database, then secrets by ARN, then migrations as their own gated task, then cache and storage, then workers and alarms. Two roles, two services, two scaling signals — and a health check that touches the database.
See also: mapping a backend to aws · separate scaling for web and workers · deploying a web service end to end · three tier web application · project 2 serverless api

