58.15 Project 31: Capstone - an Instagram-style app end to end (build, scan, push, deploy, monitor)
Why: the problem
This is the last project. We join everything from the last 30 projects into one real product. Suppose we are building Reelgram, a small Instagram-style app for a college in Nashik. Students sign up, upload photos and short reels, follow friends and see a feed. A real company like Instagram does not only write code. Every change goes through the same road: build an image, test it, scan it for security holes, push it to a registry, deploy it without downtime, and watch it in dashboards. If any step is missing, trouble follows. No scan means a known hole goes live. No health check means a broken release takes the site down. No monitoring means users tell us about the outage before we know. In this capstone we build that whole road. Five services run in containers: web, api, worker, Postgres and Redis, with media in S3. GitHub Actions builds, tests and scans every commit with Trivy, pushes to ECR, and deploys to EC2 over SSH with a health check and rollback. Prometheus and Grafana watch it, and an alert fires when the upload queue gets too long.
What: every term in plain English
- Capstone — One project that uses every skill from the course together.
- Pipeline — The automatic road from git push to running in production.
- Trivy — A free open-source scanner that finds known CVEs in images and misconfigurations in Dockerfiles.
- SBOM — Software Bill of Materials. A list of every package inside the image.
- Multi-stage build — Build tools stay in the first stage; the final image has only the app.
- Feed — The list of posts from people you follow, newest first, cached in Redis.
- Worker — A background container that makes thumbnails and converts reels with ffmpeg from a Redis queue.
- Queue length — How many jobs are waiting. If it grows, workers are too slow or down.
- Environment approval — A GitHub setting so production deploys wait for a human click.
How: step by step
- Lay out the repo. web, api, worker folders, compose files, ops scripts, workflow.
- Write good Dockerfiles. Multi-stage, non-root, health checks, small base images.
- Run it locally. docker compose up with MinIO in place of S3.
- Build the pipeline. Test, build, Trivy scan, SBOM, push to ECR.
- Deploy to EC2. docker context over SSH, pull the tag, health check, rollback.
- Monitor and alert. Prometheus, Grafana, cAdvisor, an alert on queue length.
- Clean up. Stack, EC2, ECR repos, bucket.
Architecture
Folder layout
reelgram/
web/ api/ worker/
compose.yaml compose.prod.yaml .env.example
monitoring/prometheus.yml monitoring/alerts.yml
ops/deploy.sh ops/cleanup.sh
.github/workflows/release.yml
Example 1: Repo layout, Dockerfiles and the local stack
Goal. Repo layout, Dockerfiles and the local stack.
reelgram/tree.txt
reelgram/
web/ React (Vite) -> nginx static, Dockerfile
api/ Node.js Express: auth, posts, follow, feed, presigned upload URLs
worker/ Python: thumbnails (Pillow) and reels (ffmpeg) from Redis queue
compose.yaml local: all services + MinIO + monitoring
compose.prod.yaml prod: images from ECR, real S3 via IAM role
monitoring/prometheus.yml monitoring/alerts.yml
ops/deploy.sh ops/cleanup.sh
.github/workflows/release.yml
worker/Dockerfile
FROM python:3.12-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY worker.py .
RUN useradd -r -u 10001 reel
USER reel
CMD ["python", "worker.py"]
compose.yaml
services:
web:
build: ./web
ports: ["8080:80"]
depends_on: [api]
api:
build: ./api
environment:
DATABASE_URL: postgresql://reel:${DB_PASSWORD}@db:5432/reelgram
REDIS_URL: redis://redis:6379
S3_ENDPOINT: http://minio:9000
S3_BUCKET: reelgram-media
JWT_SECRET: ${JWT_SECRET}
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
interval: 10s
retries: 3
worker:
build: ./worker
environment:
REDIS_URL: redis://redis:6379
S3_ENDPOINT: http://minio:9000
S3_BUCKET: reelgram-media
deploy: { replicas: 2 }
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: reel
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: reelgram
volumes: [pg:/var/lib/postgresql/data]
healthcheck: { test: ["CMD", "pg_isready", "-U", "reel"], interval: 5s, retries: 10 }
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes: [redis:/data]
minio:
image: minio/minio:latest
command: server /data --console-address :9001
ports: ["9001:9001"]
environment:
MINIO_ROOT_USER: ${MINIO_USER}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
volumes: [media:/data]
volumes: { pg: {}, redis: {}, media: {} }
What each line does
One repo, three images— web, api and worker each have their own Dockerfile and their own ECR repo.ffmpeg only in the worker— The api image stays small. Heavy tools live only where they are needed.useradd -r -u 10001— The worker runs as a normal user with a fixed id, not root.--no-install-recommends and rm lists— Smaller image and fewer packages for Trivy to complain about.MinIO locally— MinIO speaks the S3 API, so the same code works with real S3 in production.replicas: 2 for worker— Two workers share the queue. Reels are processed twice as fast.Values from .env— DB_PASSWORD, JWT_SECRET and MinIO login come from a local .env that is in .gitignore.
Green output
$ cp .env.example .env && docker compose up -d --build
Container reelgram-db-1 Healthy
Container reelgram-api-1 Started
Container reelgram-worker-1 Started
Container reelgram-worker-2 Started
Container reelgram-web-1 Started
$ curl -s -X POST localhost:8080/api/signup -H 'content-type: application/json' -d '{"user":"asha","password":"lab-only-pass"}'
{"id":1,"user":"asha"}
$ docker compose logs worker --tail 3
worker-1 | job 1 reel.mp4 -> 720p.mp4 + thumb.jpg in 4.2s
$ docker images --format '{{.Repository}} {{.Size}}' | grep reelgram
reelgram-web 49.1MB
reelgram-api 158MB
reelgram-worker 412MB
Expected result. The whole app runs on the laptop. A reel upload is converted by a worker and shows in the feed.
Example 2: The pipeline: test, build, scan with Trivy, SBOM, push to ECR, then deploy with approval
Goal. The pipeline: test, build, scan with Trivy, SBOM, push to ECR, then deploy with approval.
.github/workflows/release.yml
name: release
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
env:
REGION: ap-south-1
REG: 123456789012.dkr.ecr.ap-south-1.amazonaws.com
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
svc: [web, api, worker]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/reelgram-ci
aws-region: ap-south-1
- uses: aws-actions/amazon-ecr-login@v2
- name: Build
uses: docker/build-push-action@v6
with:
context: ${{ matrix.svc }}
load: true
tags: ${{ env.REG }}/reelgram-${{ matrix.svc }}:${{ github.sha }}
cache-from: type=gha,scope=${{ matrix.svc }}
cache-to: type=gha,mode=max,scope=${{ matrix.svc }}
- name: Unit tests inside the image
if: matrix.svc == 'api'
run: docker run --rm ${{ env.REG }}/reelgram-api:${{ github.sha }} npm test
- name: Trivy scan (fail on HIGH and CRITICAL)
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ${{ env.REG }}/reelgram-${{ matrix.svc }}:${{ github.sha }}
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: "1"
- name: SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REG }}/reelgram-${{ matrix.svc }}:${{ github.sha }}
artifact-name: sbom-${{ matrix.svc }}.spdx.json
- name: Push
run: docker push ${{ env.REG }}/reelgram-${{ matrix.svc }}:${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: SSH setup
run: |
mkdir -p ~/.ssh
echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/prod
echo "${{ secrets.PROD_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
printf "Host prod\n HostName ${{ vars.PROD_HOST }}\n User ubuntu\n IdentityFile ~/.ssh/prod\n" > ~/.ssh/config
- name: Deploy with health check
run: bash ops/deploy.sh ${{ github.sha }}
What each line does
id-token: write + role-to-assume— GitHub OIDC. CI gets short AWS credentials for one run. No AWS keys stored in GitHub at all.matrix svc— Three images build in parallel with the same steps.cache-from type=gha— Layer cache between runs. Second builds take seconds, not minutes.Tests inside the image— We test the exact image we will ship.Trivy exit-code 1— A HIGH or CRITICAL fixable CVE fails the job, so the image is never pushed.SBOM artifact— A package list saved with each run. When the next big CVE appears, we search it in minutes.Push only after scan— ECR never receives an image that failed tests or scan.environment: production— The deploy job waits for a reviewer click, set in repo Settings, Environments.Secrets in GitHub— Only the SSH key and known_hosts. The key file is created inside the runner and gone after the run.
Green output
build (api) Trivy: reelgram-api:4c1e9d0 (alpine 3.20)
Total: 0 (HIGH: 0, CRITICAL: 0)
build (worker) Trivy: reelgram-worker:4c1e9d0 (debian 12.7)
Total: 1 (HIGH: 1, CRITICAL: 0)
libexpat1 CVE-2024-45491 HIGH fixed in 2.5.0-1+deb12u1
Error: Process completed with exit code 1.
# fix: add apt-get upgrade -y to the worker stage, push again
build (worker) Total: 0 (HIGH: 0, CRITICAL: 0)
pushed reelgram-worker:7ab22f3
deploy Waiting for review: production
deploy approved by ravi -> deploy.sh 7ab22f3 -> health: healthy
Expected result. A vulnerable image was stopped by the scan. After the fix, all three images were pushed and deployed with approval.
Example 3: Deploy with health check and rollback, then monitor and alert on the upload queue
Goal. Deploy with health check and rollback, then monitor and alert on the upload queue.
ops/deploy.sh
set -e
TAG=${1:?usage: deploy.sh TAG}
export TAG
C="docker --context prod compose -f compose.yaml -f compose.prod.yaml"
docker context inspect prod >/dev/null 2>&1 || docker context create prod --docker host=ssh://prod
PREV=$(docker --context prod inspect reelgram-api-1 --format '{{index .Config.Labels "app.tag"}}' 2>/dev/null || echo none)
echo "deploying $TAG (previous $PREV)"
$C pull api web worker
$C up -d --no-deps api web worker
for i in $(seq 1 20); do
S=$(docker --context prod inspect reelgram-api-1 --format '{{.State.Health.Status}}')
[ "$S" = healthy ] && { echo "health: healthy"; exit 0; }
sleep 3
done
echo "unhealthy, rolling back to $PREV"
TAG=$PREV $C up -d --no-deps api web worker
exit 1
monitoring/alerts.yml
groups:
- name: reelgram
rules:
- alert: UploadQueueTooLong
expr: redis_list_length{key="reel_jobs"} > 50
for: 5m
labels: { severity: warning }
annotations:
summary: "More than 50 reels waiting for 5 minutes"
action: "docker compose up -d --scale worker=4, then check worker logs"
- alert: ApiDown
expr: up{job="api"} == 0
for: 1m
labels: { severity: critical }
- alert: ContainerRestarting
expr: increase(container_restart_count{name=~"reelgram.*"}[10m]) > 3
labels: { severity: warning }
ops/cleanup.sh
docker --context prod compose -f compose.yaml -f compose.prod.yaml down -v
docker context rm prod
for r in web api worker; do aws ecr delete-repository --repository-name reelgram-$r --force; done
aws s3 rm s3://reelgram-media-prod --recursive && aws s3 rb s3://reelgram-media-prod
echo "terminate the EC2 instance and delete the reelgram-ci IAM role"
What each line does
compose.prod.yaml— Sets image to ECR with ${TAG}, adds the label app.tag, removes MinIO, and uses real S3 through the EC2 instance role.pull then up— Images are downloaded first, so the switch itself takes only seconds.Health loop— 20 checks, 3 seconds apart. If the api is not healthy in a minute, the previous tag comes back.redis_list_length— Comes from redis_exporter with the check-keys option set to reel_jobs. Add it next to cAdvisor from Project 27.for: 5m— Short spikes do not wake anyone. Only a queue that stays long fires.action annotation— The alert tells the on-call person exactly what to do first.ContainerRestarting— A crash loop is caught even if the health check passes between crashes.
Green output
$ bash ops/deploy.sh 7ab22f3
deploying 7ab22f3 (previous 4c1e9d0)
Container reelgram-api-1 Recreated
health: healthy
$ for i in $(seq 1 80); do curl -s -F reel=@clip.mp4 https://reelgram.example.in/api/reels >/dev/null; done
# Prometheus after 5 minutes:
ALERTS{alertname="UploadQueueTooLong", severity="warning"} firing value 74
$ docker --context prod compose -f compose.yaml -f compose.prod.yaml up -d --scale worker=4 --no-deps worker
# 6 minutes later the queue is 0 and the alert is resolved
Expected result. The release went live safely, the queue alert fired under load, and scaling the workers fixed it.
Every command, step by step (plain docker and Compose)
-
Clone and start
git clone ... && cd reelgram && cp .env.example .env && docker compose up -d --buildWhat it does: Everything local.
reelgram-api-1 Started -
Check sizes
docker images | grep reelgramWhat it does: Compare the three images.
reelgram-web 49.1MB -
Scan locally
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --severity HIGH,CRITICAL reelgram-workerWhat it does: Same scan as CI.
Total: 0 -
Make an SBOM
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --format spdx-json reelgram-apiWhat it does: Package list.
"spdxVersion": "SPDX-2.3" -
Create OIDC role
Create IAM role reelgram-ci trusting token.actions.githubusercontent.com for your repo onlyWhat it does: No keys in GitHub.
Role created -
Push to main
git push origin mainWhat it does: The workflow starts.
release #12 queued -
Approve
GitHub, Actions, the run, Review deployments, ApproveWhat it does: Deploy job runs.
approved -
Check site
curl -s https://reelgram.example.in/healthzWhat it does: Live.
ok -
Grafana
Open port 3000 through an SSH tunnel: ssh -L 3000:localhost:3000 prodWhat it does: Dashboards without opening ports.
Grafana login page -
Load test
Upload 80 reels in a loopWhat it does: Queue grows.
UploadQueueTooLong firing -
Scale workers
compose up -d --scale worker=4 --no-deps workerWhat it does: Queue drains.
alert resolved -
Clean up
bash ops/cleanup.shWhat it does: Remove everything.
terminate the EC2 instance
When it is red 1
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Cause. The IAM role trust policy does not match the repo or branch.
Fix, step by step
- Set the sub condition to repo:OWNER/reelgram:ref:refs/heads/main.
- Keep permissions id-token: write in the workflow.
When it is red 2
Total: 3 (HIGH: 2, CRITICAL: 1) ... Error: Process completed with exit code 1.
Cause. The base image has fixable CVEs.
Fix, step by step
- Update the base tag (for example node:20-alpine latest patch).
- Add apt-get upgrade or apk upgrade in the build stage, then rebuild.
When it is red 3
worker-1 | ffmpeg: error while loading shared libraries
Cause. ffmpeg was copied from a builder stage without its libraries.
Fix, step by step
- Install ffmpeg with apt in the final stage, as in our worker Dockerfile.
When it is red 4
api-1 | AccessDenied: s3:PutObject on reelgram-media-prod
Cause. The EC2 instance role does not allow the bucket.
Fix, step by step
- Add s3:PutObject and s3:GetObject on arn:aws:s3:::reelgram-media-prod/* to the instance role.
When it is red 5
Host key verification failed.
Cause. The CI runner does not know the server's SSH key.
Fix, step by step
- Save ssh-keyscan output of the server in the PROD_KNOWN_HOSTS secret. Do not turn off host checking.
When it is red 6
unhealthy, rolling back to 4c1e9d0
Cause. The new api failed its health check, usually a missing env var or DB migration.
Fix, step by step
- Read docker --context prod compose logs api --tail 50.
- Run migrations as a one-off container before deploy.
Practice tasks
Easy Task 1: Local run
Why: Compose.
What to do: Run all services locally and upload one reel.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker compose up -d --buildEasy Task 2: Sizes
Why: Images.
What to do: Find the biggest image and say why.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker imagesEasy Task 3: Scan
Why: Trivy.
What to do: Scan the worker image locally.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
aquasec/trivy image reelgram-workerMedium Task 4: Fix a CVE
Why: Base image.
What to do: Make the scan pass by updating the base image.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
apt-get upgrade -yMedium Task 5: OIDC
Why: CI.
What to do: Create the reelgram-ci role limited to your repo's main branch.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
token.actions.githubusercontent.comMedium Task 6: Pipeline
Why: Actions.
What to do: Push to main and approve the production deploy.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
environment: productionMedium Task 7: Rollback
Why: Deploy.
What to do: Release a tag with a broken health route and watch rollback.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/deploy.sh BADTAGHard Task 8: Alert
Why: Prometheus.
What to do: Make UploadQueueTooLong fire and resolve it by scaling.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
--scale worker=4Hard Task 9: Grow
Why: Cloud.
What to do: Move the api to ECS Fargate using Project 29.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
aws ecs update-serviceEasy Task 10: Clean
Why: Bill.
What to do: Delete stack, ECR repos, bucket, role and EC2.
- Use your laptop with Docker, a GitHub repository, one AWS account with a billing alarm, and one Ubuntu EC2 t3.medium.
- Reuse code from Project 20 (reel upload), Project 27 (monitoring) and Project 28 (deploy over SSH).
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/cleanup.shHomework: try at home
Make Reelgram production-grade: move Postgres to RDS and Redis to ElastiCache, run api and worker on ECS Fargate as in Project 29, sign images with cosign in the pipeline, and add a Grafana dashboard for p95 upload time.
Recap
- A real product needs a full road: build, test, scan, push, deploy, monitor.
- Small multi-stage non-root images are faster and safer.
- Trivy in CI stops known CVEs before they reach the registry.
- OIDC gives CI short AWS access with no stored keys.
- Deploy by commit tag with a health check and automatic rollback.
- Alert on symptoms users feel, like a long upload queue, with a clear first action.
- Clean up every cloud resource after practice.
FAQ
Q. Is this enough for a real startup?
For the first thousands of users, yes. Later move the database and Redis to managed services and the apps to ECS or Kubernetes.
Q. Why not Kubernetes here?
The goal is the full Docker road. The Kubernetes chapters run the same images on a cluster.
Q. Can I use GitLab or Jenkins?
Yes. The steps are the same: build, test, trivy, push, ssh deploy.
Q. Why does Trivy fail on things I cannot fix?
We use ignore-unfixed, so only CVEs with a fix fail the build.
Q. How do I show this in my resume?
Link the repo, show the pipeline screenshot, the Grafana dashboard and the rollback test.
Q. How much does it cost?
One t3.medium for a few hours plus small S3 and ECR use: well under 1 USD if you clean up.
Q. What should I learn next?
Kubernetes with Helm, then GitOps with Argo CD, using these same images.
Interview questions
-
Walk me through your CI/CD pipeline for containers.
Model answer: Build per service, test inside the image, Trivy scan and SBOM, push by commit tag to ECR, approved deploy with health check and rollback, then monitoring.
-
How do you keep vulnerable images out of production?
Model answer: Scan in CI with a failing threshold, rebuild often on patched bases, and use minimal images.
-
How do you avoid storing cloud keys in CI?
Model answer: Use OIDC federation to assume a short-lived IAM role.
-
How would you handle slow video processing?
Model answer: Queue jobs in Redis, process with scalable worker containers, and alert on queue length.
-
What do you monitor for a containerized app?
Model answer: Up status, error rate and latency, restarts, CPU and memory per container, and business queues.
Quick quiz
Q1. Why scan before pushing, not after?
So a vulnerable image never reaches the registry or production.
Q2. How does CI get AWS access without keys?
GitHub OIDC with a role it can assume for one run.
Q3. What is an SBOM used for?
To know quickly which images contain a package when a new CVE appears.
Q4. What happens if the new api is unhealthy?
deploy.sh brings back the previous tag.
Q5. Which alert tells us workers are too slow?
UploadQueueTooLong on the Redis queue length.
Samajla ka?
Lab
- Use one free-tier AWS account and one t3.micro or t2.micro EC2 instance.
- Attach an IAM role to EC2 instead of putting keys in files.
- Set a billing alarm of 5 USD before you start.
- Delete RDS, buckets and tables in the cleanup step of each project.
- Write every command you typed in a notebook.
Practice task
Join the reel-upload app and the microservices shop behind one gateway and add Prometheus monitoring.
Ravindra Bagale's Tip
Cloud project interview madhe khup vichartat. Pan cleanup visru naka, nahitar mahinyachya shevti bill yeil.
In short
- Containers reach AWS through an IAM role, not hard-coded keys.
- Test locally first with DynamoDB Local or LocalStack, then use real AWS.
- Slow work like video transcoding goes to a worker through a queue.
- Always clean up cloud resources after practice.
Revision card
Interview card