Filter concepts by levelShowing all levels.

AWS · Practical Projects

Practical AWS Projects

Five builds in order, each with milestones you can demonstrate and one decision that is expensive to defer

  • Build outside-inreachable on day one, then add inward
  • Milestones, not componentseach ends in something you can show
  • Secrets by ARNnever in an image, a repo or an env block
  • Prove it with drillszone loss, timed restore, rebuild from code

1.Project 1 — A Production Django API

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

Key features

  • Two task definitions, not one: the web tasks run gunicorn behind the load balancer, the worker tasks run Celery and sit behind no listener at all
  • The execution role pulls the image and reads the secrets; the task role is what the application code itself uses — they are different roles for a reason
  • Migrations run as a one-off task before the new service revision rolls, so a failed migration stops the release instead of racing four starting containers
  • Secrets are referenced by ARN in the task definition and injected at start; the value never appears in the image, the repository or the environment block of a template
  • Static files go to S3 behind CloudFront and user uploads go to a separate bucket, because they have different access rules and different lifecycle needs
  • The health check path must touch the database, or a task with an exhausted connection pool keeps receiving traffic

Seven milestones, each ending in something you can demonstrate

Seven milestones, each ending in something you can demonstrate
#MilestoneDone whenWhat it teaches
1Network and a hello-world containerA public URL returns 200 from a container in a private subnetVPC layout, ALB target groups, ECS service basics
2The real image, from ECRThe Django app serves a page, still with no databaseImage build, ECR authentication, task definition shape
3RDS PostgreSQL, Multi-AZA view reads a table; the security group allows only the app groupSubnet groups, security-group-to-security-group rules
4Secrets ManagerNo credential exists in the image, the repo or the task definition bodyExecution role permissions, secret ARN injection
5Migrations as a release stepA failed migration stops the release before any task rollsOne-off tasks, ordering, deployment gates
6Redis, S3 media, static via CloudFrontAn upload lands in S3 and is served back; a cached view is measurably fasterCache-aside, presigned uploads, origin access control
7Workers, alarms and a runbookA queued job runs; an alarm fires and points at a written actionSeparate scaling, deep health checks, operability
Project 1, end to end

Traffic flows downward through the left column. The right column is what the tasks reach out to, and the bottom band is what runs beside the release rather than during a request.

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

Signature

Route 53 → ALB → ECS (web + workers) → RDS + Redis + S3, secrets by ARN, migrations as a gated task

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.

2.Project 2 — A Serverless API

The second project is the same kind of API with none of the servers. API Gateway takes the request, a Lambda function handles it, DynamoDB stores the result, and anything slow is pushed onto EventBridge or SQS so the response can return immediately. The design work moves from capacity planning to access patterns and limits.

Key features

  • The DynamoDB table is designed from the access patterns first — write the list of queries before the table, because a query you did not plan for may need a new index
  • REST API integration timeout is capped at 29 seconds, so anything longer must return early and finish asynchronously
  • Every asynchronous consumer must be idempotent: SQS delivers at least once, and Lambda retries asynchronous invocations on its own
  • A Lambda triggered by an SQS queue scales with the queue, so the database or third-party API behind it needs a concurrency cap or it becomes the bottleneck
  • Large uploads go straight to S3 with a presigned URL rather than through the API, which sidesteps the payload limit and the function duration entirely
  • Every asynchronous path needs a dead-letter queue or an on-failure destination, or a permanently failing event disappears after its retries

Six milestones, and the limit each one makes you meet

Six milestones, and the limit each one makes you meet
#MilestoneDone whenThe limit it teaches
1Access patterns, then the tableEvery query the API needs is listed and maps to a key or an indexA pattern you did not plan may need a new GSI
2API Gateway → Lambda → DynamoDBCreate and read work end to end with real dataThe 29-second REST integration timeout
3Presigned uploads to S3A file reaches S3 without passing through the APIPayload size, and function duration on large bodies
4Publish an event and consume itA slow step happens after the response, on a queueAt-least-once delivery, so the consumer is idempotent
5Failure pathsA permanently failing event lands in a dead-letter queue with its causeAsync retries end silently without a destination
6Limits and alarmsConcurrency is capped where a downstream is fragile, and alarms existConcurrency, throttling, and queue depth as a signal
Project 2, and where the synchronous path ends

Everything above the dashed line happens while the caller waits and must fit inside 29 seconds. Everything below it happens afterwards, at least once, and needs a dead-letter queue.

  • A serverless API diagram split into a synchronous half and an asynchronous half.
  • Above the line: a client calls API Gateway, which invokes a Lambda function, which reads and writes DynamoDB and returns. This path must complete within the 29-second REST API integration timeout.
  • Large uploads bypass the API: the client requests a presigned URL and uploads directly to S3.
  • Below the line: the handler publishes an event to EventBridge. Rules route it to SQS queues, each with its own consumer Lambda and its own dead-letter queue.
  • A DynamoDB stream and an S3 object-created event are two more sources that feed the same asynchronous side.
  • Footnote: each asynchronous consumer must be idempotent, because delivery is at least once and Lambda retries on its own.

Signature

API Gateway → Lambda → DynamoDB, EventBridge/SQS for the rest, DLQ on every async path

Write the access patterns before the table. Keep the synchronous path inside 29 seconds and push everything else onto an event. Every asynchronous consumer is idempotent, capped where its dependency is fragile, and has a dead-letter queue with an alarm on it.

3.Project 3 — A File-Processing System

A file lands in S3, an event goes onto a queue, a worker picks it up, writes the result to another prefix and notifies whoever asked. The pipeline is short, and almost all the difficulty is in the two questions it forces: what happens when the same file is processed twice, and what happens when one file can never be processed at all.

Key features

  • Upload with a presigned URL so the file never passes through your application, and put the output under a different prefix or a different bucket than the input
  • The S3 event is the trigger and the queue is the buffer — sending events straight to a worker removes the retry, the backlog and the dead-letter queue in one step
  • Derive the output key from the input key so reprocessing overwrites rather than duplicating, which is the cheapest idempotency there is
  • The visibility timeout must exceed the slowest realistic processing time, or a long job is redelivered while the first attempt is still running
  • For work longer than fifteen minutes, or needing more memory than a function allows, the worker is an ECS task rather than a Lambda
  • A dead-letter queue with an alarm on its depth is the difference between a poison message being noticed and a file silently never appearing

Five milestones, and the decision each one forces

Five milestones, and the decision each one forces
#MilestoneDone whenThe decision it forces
1Presigned upload into an input prefixA file reaches S3 without touching your applicationWhich bucket and prefix, and who may write there
2Event into a queue, not into a workerAn upload produces exactly one queued messageThe queue is the buffer; the event alone has no retry story
3A worker that writes a derived output keyReprocessing the same file overwrites and does not duplicateLambda or ECS — decided by duration and memory, not preference
4Visibility timeout sized to the workThe slowest realistic file completes before redeliveryWhat the slowest file actually is, measured rather than guessed
5Dead-letter queue and notificationA poison message is visible; a finished file notifies the requesterWhat "failed" means to the user, not only to the worker
One file, one message, one derived output

The two failure paths are drawn as prominently as the success path, because they are where the design work is. Everything else is a straight line.

  • A file-processing pipeline drawn left to right, with failure paths shown below.
  • A client requests a presigned URL and uploads a file directly to the S3 input prefix.
  • S3 emits an ObjectCreated event, which is delivered to an SQS queue rather than straight to the worker.
  • A worker — a Lambda for short jobs, an ECS task for long or memory-heavy ones — receives the message, reads the input object, processes it, and writes the result to the output prefix using a key derived from the input key.
  • On success the worker deletes the message and publishes a completion notification through SNS or EventBridge.
  • Failure path one: the message becomes visible again after the visibility timeout and is retried; after the maximum receive count it moves to a dead-letter queue with an alarm on its depth.
  • Failure path two: the visibility timeout is shorter than the processing time, so the message is redelivered while the first attempt is still running and two workers process the same file.

Command

text
presigned upload → S3 event → SQS → worker → derived output key → notify · DLQ on the queue, alarm on its depth

S3 event into a queue, never straight into a worker. Derive the output key from the input key so a retry overwrites rather than duplicates. Size the visibility timeout from the slowest real file, keep the function timeout under it, and put an alarm on the dead-letter queue depth.

4.Project 4 — A Multi-Tenant SaaS Platform

This project has the same components as the first one and a completely different hard part. Tenant identity has to be present in every query, every cache key, every object prefix, every log line and every metric — from the first table, not from the first incident. Retrofitting it is a data migration plus an audit of every access path in the codebase.

Key features

  • Resolve the tenant once per request, at the edge of the application, and carry it in a request context — never read it again from a header deeper in the code
  • PostgreSQL row-level security enforces isolation in the database, so a query that forgets its tenant filter returns nothing rather than everything
  • Every cache key is namespaced by tenant, because a key collision across tenants is a data leak that looks like a caching bug
  • Object storage separates tenants by prefix, and access is granted with a policy condition on that prefix rather than by the application choosing a path
  • A noisy tenant is a reliability problem: per-tenant rate limits and separate queues stop one customer consuming the capacity of all of them
  • Cost per tenant needs a tag or a dimension from day one, because the question "which customer is unprofitable?" cannot be answered retrospectively

The seven places tenant identity appears, and what goes wrong without it

The seven places tenant identity appears, and what goes wrong without it
WhereWhat it looks likeWithout it
Request contextThe tenant is resolved once from the token and attached to the requestEach layer re-reads a header, and one of them trusts a client-supplied value
AuthorizationEvery handler asserts the resource belongs to the resolved tenantAn id in a URL is enough to read another customer's record
DatabaseTenant id in the primary key, plus row-level security as the backstopOne query without a `WHERE tenant_id` returns the whole table
CacheKeys namespaced: `t:<tenant>:orders:42`Two tenants share a key and one sees the other's data intermittently
Object storageA prefix per tenant, with the IAM condition on the prefixA path built in application code, and one bug crosses the boundary
Queues and limitsThe tenant travels with the work; per-tenant rate limits, or its own queueOne customer's bulk import is an outage for everyone else
Observability and costTenant as a log field, a metric dimension and a cost allocation tag"Which customer is affected?" and "which is unprofitable?" cannot be answered
Where the tenant id has to appear

The tenant is resolved once, at the top, and then appears at every marked point. Any point where it can be absent is where the isolation fails.

  • A request path down the left with the seven places tenant identity must appear marked along it.
  • A request arrives through CloudFront and an Application Load Balancer to ECS tasks.
  • Point 1, authentication: the tenant is resolved once from the token and put into a request context.
  • Point 2, authorization: every handler checks that the requested resource belongs to the resolved tenant.
  • Point 3, the database: the tenant id is part of every query, enforced by PostgreSQL row-level security so a forgotten filter returns nothing.
  • Point 4, the cache: every Redis key is namespaced with the tenant, so a collision cannot cross customers.
  • Point 5, object storage: each tenant has its own S3 prefix, and access is granted by a policy condition on that prefix.
  • Point 6, queues: work carries the tenant, and a noisy tenant is rate limited or given its own queue.
  • Point 7, observability and cost: logs, metrics and cost allocation all carry the tenant as a dimension.
  • Footnote: retrofitting any of these is a data migration plus an audit of every access path.

Signature

context · authz · RLS · namespaced cache keys · S3 prefix · per-tenant limits · tenant as a dimension

Tenant identity goes in from the first table: request context, authorization, primary key with row-level security behind it, namespaced cache keys, per-tenant S3 prefixes, per-tenant limits, and tenant as a log field, metric dimension and cost tag. Design so a missing tenant id denies rather than matching everyone.

5.Project 5 — A Highly Available Platform, Built From Code

The last project is the first four assembled properly and, crucially, defined entirely in code. Nothing is created by hand. The finishing test is not that it works — it is that you can destroy an Availability Zone in a drill, restore the database from a backup, and rebuild the whole environment from the repository, each with a measured time.

Key features

  • Every tier spans two Availability Zones and is sized so the remaining zone can carry peak load, otherwise it is multi-AZ in name only
  • Application and data subnets are private; the internet gateway route belongs to the public subnets alone, and outbound traffic uses NAT or a VPC endpoint
  • Gateway endpoints for S3 and DynamoDB carry that traffic off the NAT path, which removes a data processing charge and adds an endpoint policy as a control
  • Logs, metrics and audit trails land in a separate account, so a compromised or broken workload cannot alter the record of what it did
  • Backups are proved by a timed restore, and disaster recovery is proved by a drill — the numbers those produce are the real RPO and RTO
  • The pipeline deploys from the repository only: a manual console change is drift, and the next apply either reverts it or fails

Eight milestones, and what each one has to prove

Eight milestones, and what each one has to prove
#MilestoneProved byWhat it usually finds
1Network from codeA `terraform apply` or a stack deploy creates every subnet and routeA hand-made route or peering nobody put in the repository
2Multi-AZ compute with real capacityPeak load served with one zone removedAn Auto Scaling maximum sized for two zones, not one
3Private data tier and endpointsNo 0.0.0.0/0 route in the data subnets; S3 traffic off the NAT pathS3 and DynamoDB traffic quietly billed as NAT data processing
4Managed data with a standbyA forced failover completes with the application runningA connection pool that holds dead sockets after the endpoint moves
5CloudFront, S3 and cachingStatic assets served from the edge, `index.html` never cachedA cache key forwarding every cookie, so the hit rate is near zero
6Centralised logging in another accountThe workload cannot write to or delete its own audit trailTrails writing into the account they audit
7Pipeline-only deploymentA console change is reverted or fails on the next applySeveral console changes nobody had recorded
8Backups and a DR runbookA timed restore, and a drill someone else can run from the document"We take snapshots" with no restore ever performed
The full platform, and the three drills that prove it

The left is the architecture; the right is what makes it a claim you can defend. Without the three drills the diagram is an intention.

  • A complete platform architecture on the left with three verification drills on the right.
  • Edge: Route 53, CloudFront and WAF in front of everything, with static assets served from S3.
  • Public subnets across two Availability Zones: the Application Load Balancer and a NAT gateway.
  • Private application subnets across two zones: ECS or EC2 capacity in an Auto Scaling group, sized so one zone can carry peak load.
  • Private data subnets across two zones: RDS or Aurora with a standby, and ElastiCache with a replica.
  • Gateway endpoints carry S3 and DynamoDB traffic without using the NAT path.
  • A separate log account receives CloudTrail, VPC flow logs, load balancer access logs and application logs.
  • The whole environment is defined in infrastructure as code and deployed by a pipeline from the repository.
  • Drill 1, zone loss: remove one zone from service and measure recovery.
  • Drill 2, restore: restore the database to a point in time and measure how long it took.
  • Drill 3, rebuild: create the entire environment in an empty account from the repository alone.

Signature

2 AZs with 1-AZ capacity · private data · endpoints off NAT · logs elsewhere · IaC only · 3 drills

Everything from the repository, nothing from the console. Two zones with capacity for one, private data subnets, endpoints off the NAT path, and logs in another account. Then prove it three times: lose a zone, restore the database, and rebuild the environment in an empty account — each with a measured number and a date.

Remember

  • Execution role pulls the image and reads the secret; task role is what your code uses. Two roles, on purpose.
  • Migrations are a one-off task gated on exit code, never the entrypoint of every container.
  • Derive an output key from the input key — the cheapest idempotency in any pipeline.
  • Tenant identity goes in from the first table. Retrofitting it is a migration plus an audit of every access path.

Pro tip

  • Finish project 1 with a hello-world container reachable on day one. Every later problem is debugged through that path, so build it before there is anything else to blame.
  • On project 5, run the rebuild-into-an-empty-account drill. It is the one people skip and the one that finds the certificate, the hosted zone and the key policy nobody put in the repository.

Next up

  • AWS Learning Order
  • Target Outcome
  • Cloud Architecture Patterns

Sources

  • Amazon ECS services — docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-services.html
  • API Gateway quotas and important notes — docs.aws.amazon.com/apigateway/latest/developerguide/limits.html
  • Disaster recovery options in the cloud — docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html
  • Checked against official AWS documentation 2026-09-05

Spotted a mistake on this page? Tell us.