Each stage leans on the one before it — EC2 makes no sense without IAM, containers on AWS make no sense without Docker, and none of it is safe without the billing alarm from Stage 00.
STAGE 00
Your AWS account, set up safely
Week 1Most people's first AWS experience is either paralysis or a surprise bill. Twenty minutes of setup prevents both — and every stage after this assumes you did it.
What you'll learn
- Regions and Availability Zones — and why picking the wrong region costs you latency
- Root user vs IAM user, and why root is an emergency-only account
- Billing alarms and budgets, before you launch anything
Code
STEP 1 — Create the account
aws.amazon.com → Create an AWS Account
A card is required even on free tier (identity verification).
STEP 2 — Lock down root IMMEDIATELY
Account menu → Security credentials
→ Enable MFA (phone authenticator app is fine)
<KW>Root can close the account and change billing.
Its permissions cannot be restricted. Protect it.</KW>
STEP 3 — Set a budget BEFORE launching anything
Billing → Budgets → Create budget
→ Monthly cost budget, e.g. $5
→ Alert at 80% and 100% to your email
This one step prevents almost every horror story.
STEP 4 — Create your daily-driver IAM user
IAM → Users → Create user
→ Attach AdministratorAccess (for now — Stage 01 narrows it)
→ Enable MFA on this user too
→ Sign out of root. Use this user from now on.
STEP 5 — Pick your region and stay in it
ap-south-1 (Mumbai) for Indian users — lowest latency.
<KW>Resources are region-scoped. An instance in Mumbai is
invisible from the Ohio console — a classic "where did my
server go?" moment.</KW>
BuildCreate the account, enable MFA on root, set a ₹400 (~$5) budget alarm, create an admin IAM user, and sign out of root. Screenshot the budget as proof.
Self-checkWhy should you stop using the root user immediately after creating an AWS account?
STAGE 01
IAM — who can do what
Week 1–2IAM is where AWS security actually lives, and where beginners create the biggest holes: AdministratorAccess on everything, and long-lived access keys committed to git. Getting this right early is far cheaper than retrofitting it.
What you'll learn
- Users, groups, roles, and policies — what each one is actually for
- Least privilege in practice, without grinding development to a halt
- Why services get roles, never access keys
Code
THE FOUR THINGS
User — a person (you). Has a password / access keys.
Group — a bag of users sharing permissions.
Role — assumed temporarily. For SERVICES and cross-account.
Policy — the JSON that says what's allowed.
STEP 1 — Read a policy (this is the whole language)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-uploads/*"
}]
}
Effect + Action + Resource. That's 90% of IAM.
STEP 2 — Create a role for EC2 (not a key!)
IAM → Roles → Create role
→ Trusted entity: AWS service → EC2
→ Attach the policy above
→ Name: ec2-s3-uploads
STEP 3 — Attach it to the instance
EC2 → select instance → Actions
→ Security → Modify IAM role → pick ec2-s3-uploads
STEP 4 — Verify from inside the instance
aws s3 ls s3://my-app-uploads
No keys anywhere. AWS rotates the credentials for you.
<KW>NEVER: aws_access_key_id in your code, .env, or Dockerfile.
A leaked key is someone else mining crypto on your card.</KW>BuildCreate a policy that allows read-only access to exactly one S3 bucket, attach it to a role, and attach that role to an EC2 instance. Confirm it can read that bucket and nothing else.
Self-checkYour EC2 instance needs to read from an S3 bucket. Why is attaching an IAM role better than putting an access key on the server?
STAGE 02
EC2 — your first server
Week 2EC2 is the most fundamental piece of AWS: a computer you rent by the hour. Almost every higher-level service is a managed abstraction over this, so understanding it makes the rest make sense.
What you'll learn
- AMIs, instance types and families, and what the letters/numbers mean
- Key pairs and SSH — and what to do when you lose the .pem
- Security groups, EBS volumes, and the difference between stop and terminate
Code
STEP 1 — Launch
EC2 → Launch instance
→ AMI: Amazon Linux 2023 (or Ubuntu)
→ Type: t3.micro (free-tier eligible in most accounts)
→ Key pair: create new → download the .pem → KEEP IT
→ Security group: allow SSH (22) from MY IP only
Never 0.0.0.0/0 on port 22. Bots scan it within minutes.
STEP 2 — Connect
chmod 400 my-key.pem # SSH refuses loose perms
ssh -i my-key.pem ec2-user@<PUBLIC-IP>
STEP 3 — Install something
sudo dnf update -y
sudo dnf install -y docker
sudo systemctl start docker
sudo usermod -aG docker ec2-user # re-login to take effect
STEP 4 — Open a port to serve traffic
Security group → Edit inbound rules
→ Add: HTTP (80) from 0.0.0.0/0
STEP 5 — Clean up (this is the part people forget)
Stop = keeps the disk, stops compute billing
Terminate = deletes the instance and (usually) its volume
A forgotten running instance is the #1 surprise bill.
INSTANCE TYPE DECODER
t3.micro → t = burstable family, 3 = generation, micro = size
t/m general purpose · c compute · r memory · g GPU
BuildLaunch a t3.micro, SSH in, install Docker, open port 80, then terminate it. Time the whole loop — you should get it under 10 minutes on the second try.
Self-checkYou launched an EC2 instance but can't SSH into it. Which is the most likely cause?
STAGE 03
Networking — VPC, subnets, security groups
Week 2–3"It works locally but I can't reach it on AWS" is almost always networking. This stage is the difference between guessing and diagnosing.
What you'll learn
- VPC, subnets, and what makes a subnet public vs private
- Internet Gateway, route tables, and NAT Gateway (the expensive one)
- Security groups vs NACLs — stateful vs stateless
Code
THE MENTAL MODEL
VPC your own private network in AWS
├─ Public subnet → route table points 0.0.0.0/0 to IGW
│ └─ things needing a public IP (load balancer, bastion)
└─ Private subnet → no route to IGW
└─ databases, app servers (safer)
<KW>A subnet is "public" ONLY because its route table has an
Internet Gateway route. That's the entire definition.</KW>
SECURITY GROUP vs NACL
Security Group NACL
Attaches to instance subnet
State stateful stateless
Rules allow only allow AND deny
Return traffic automatic must allow explicitly
<KW>Stateful means: allow inbound 443, and the response is
automatically permitted out. With a NACL you'd have to allow
the ephemeral return ports yourself.</KW>
DEBUG CHECKLIST — "I can't reach my instance"
1. Security group inbound allows your port + source IP?
2. Instance in a public subnet (route to IGW)?
3. Instance actually has a public IP?
4. NACL allows it (default NACL allows all — rarely the cause)
5. Is the app even listening? (curl localhost from inside)
<KW>NAT Gateway lets private subnets reach the internet OUT.
It bills ~$32/month plus data. It is the classic
"why is my bill so high" answer.</KW>BuildCreate a VPC with one public and one private subnet. Put an instance in each. Confirm the public one is reachable and the private one genuinely isn't.
Self-checkWhat's the practical difference between a security group and a network ACL?
STAGE 04
Docker — packaging your app
Week 3"Works on my machine" stops being a joke once your app ships as an image. Containers are also the unit almost all modern AWS deployment expects.
What you'll learn
- Image vs container vs registry — the three things people conflate
- Writing a Dockerfile that isn't needlessly huge or slow to rebuild
- Ports, volumes, and environment variables
Code
IMAGE vs CONTAINER
Image = the blueprint. Immutable. Layered.
Container = a running instance of an image.
<KW>One image → many containers. Changes inside a container
vanish on restart unless written to a volume.</KW>
STEP 1 — A Dockerfile that caches well
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # deps layer FIRST
RUN npm ci --omit=dev # cached unless package.json changes
COPY . . # code changes don't bust deps
EXPOSE 3000
CMD ["node", "server.js"]
<KW>Copying everything before npm ci reinstalls all
dependencies on every single code change. Slow builds
are almost always this mistake.</KW>
STEP 2 — Build and run
docker build -t my-app:1.0 .
docker run -p 3000:3000 -e NODE_ENV=production my-app:1.0
↑ host:container
STEP 3 — Look inside a running container
docker ps
docker exec -it <container-id> sh
docker logs -f <container-id>
STEP 4 — .dockerignore (don't ship your junk)
node_modules
.git
.env
<KW>alpine base images are ~5x smaller than the default.
Smaller image = faster pull = faster deploy = lower cost.</KW>BuildContainerize any app you've built. Get the image under 200MB, and make a one-line code change rebuild in under 5 seconds.
Self-checkWhat's the difference between a Docker image and a container?
STAGE 05
ECR & ECS Fargate — running containers on AWS
Week 3–4This is the actual deployment path most teams use: push an image to a registry, and let AWS run it. Fargate removes the server you'd otherwise have to patch, scale, and babysit.
What you'll learn
- ECR as your private image registry, and authenticating to it
- ECS concepts: cluster, task definition, service
- Fargate vs EC2 launch type — what you're actually trading
Code
STEP 1 — Create the registry
ECR → Create repository → name: my-app
STEP 2 — Authenticate Docker to ECR
aws ecr get-login-password --region ap-south-1 \
| docker login --username AWS \
--password-stdin <ACCT>.dkr.ecr.ap-south-1.amazonaws.com
STEP 3 — Tag and push
docker tag my-app:1.0 \
<ACCT>.dkr.ecr.ap-south-1.amazonaws.com/my-app:1.0
docker push \
<ACCT>.dkr.ecr.ap-south-1.amazonaws.com/my-app:1.0
STEP 4 — Task definition (the "how to run it" spec)
ECS → Task definitions → Create
→ Launch type: Fargate
→ CPU 0.25 vCPU, Memory 0.5 GB
→ Container: image URI from step 3, port 3000
→ Task role: the IAM role your app needs (Stage 01)
STEP 5 — Run it as a service
ECS → Clusters → Create cluster
→ Create service → your task definition
→ Desired tasks: 1
→ Attach an Application Load Balancer for a stable URL
FARGATE vs EC2 LAUNCH TYPE
Fargate no servers to manage, pay per task/second,
costs more per unit of compute
EC2 you manage/patch/scale instances,
cheaper at steady high utilisation
<KW>Start with Fargate. Move to EC2 only when the bill
justifies the operational work.</KW>BuildPush your Stage 04 image to ECR and run it on Fargate behind a load balancer. Get a working public URL, then scale the service to 2 tasks.
Self-checkYou push your container image to ECR and want to run it without managing servers. Which is the most direct path?
STAGE 06
Lambda — code without servers
Week 4Lambda is the cheapest way to run code that isn't running constantly. Knowing its limits is what stops you from forcing it into a job it can't do.
What you'll learn
- Handler signature, triggers, and the execution model
- Cold starts — what causes them and when they actually matter
- The hard limits: 15 minutes, memory/CPU coupling, package size
Code
STEP 1 — Create the function
Lambda → Create function → Author from scratch
→ Runtime: Node.js 20.x
→ Execution role: create new (gets CloudWatch Logs access)
STEP 2 — The handler
export const handler = async (event) => {
console.log("Event:", JSON.stringify(event));
return {
statusCode: 200,
body: JSON.stringify({ ok: true }),
};
};
<KW>event is whatever the trigger sends. Log it first —
every trigger has a different shape.</KW>
STEP 3 — Configure
Memory: 512 MB # CPU scales WITH memory. More RAM = faster.
Timeout: 30s # default is 3s — too short for most real work
Environment variables for config (never hardcode)
STEP 4 — Add a trigger
+ Add trigger → API Gateway / S3 / EventBridge / SQS
STEP 5 — Watch it run
Monitor → View CloudWatch logs
console.log goes to CloudWatch. That IS your debugger.
THE LIMITS THAT MATTER
15 min max execution # long jobs → ECS/Batch instead
250 MB unzipped package # big deps → container image Lambda
/tmp is ephemeral # don't store state between calls
COLD STARTS
First call after idle loads the runtime: ~100ms–1s.
<KW>Irrelevant for a nightly job. Very relevant for a
user-facing API — that's when you consider provisioned
concurrency or a container instead.</KW>BuildWrite a Lambda triggered by an S3 upload that logs the file name and size. Then deliberately set the timeout to 1s and watch it fail — read the error in CloudWatch.
Self-checkWhich workload is a poor fit for Lambda?
STAGE 07
EventBridge — wiring services together
Week 5EventBridge is how AWS services talk without knowing about each other. It replaces both cron jobs and a lot of brittle direct-call plumbing.
What you'll learn
- Event buses, rules, event patterns, and targets
- Scheduled rules as managed cron — without a server to keep alive
- Why event-driven decoupling matters more as a system grows
Code
THE MODEL
Something emits an EVENT → a RULE matches it → TARGETS run
STEP 1 — Scheduled rule (cron, without a server)
EventBridge → Rules → Create rule
→ Rule type: Schedule
→ cron(0 3 * * ? *) # 03:00 UTC daily
→ Target: your Lambda
No EC2 to keep running just for crontab.
STEP 2 — Event pattern rule (react to things)
{
"source": ["aws.s3"],
"detail-type": ["Object Created"],
"detail": {
"bucket": { "name": ["my-app-uploads"] }
}
}
Matches only S3 uploads to that one bucket.
STEP 3 — Your own custom events
await eventBridge.putEvents({
Entries: [{
Source: "my-app.orders",
DetailType: "OrderPlaced",
Detail: JSON.stringify({ orderId, amount }),
}],
});
STEP 4 — Add consumers without touching the producer
Rule A: OrderPlaced → send-confirmation-email
Rule B: OrderPlaced → update-inventory
Rule C: OrderPlaced → notify-analytics
<KW>The order service never learns any of these exist.
That's the whole point.</KW>
WHY NOT JUST CALL THE API DIRECTLY?
Direct: A must know B's address, handle B being down,
and get redeployed to add C.
Events: A emits and forgets. Add or remove consumers
freely. Failures retry independently.BuildCreate a scheduled rule that runs a Lambda every 5 minutes and logs the time. Then add a second rule matching a custom event you emit yourself.
Self-checkWhy use EventBridge instead of having Service A call Service B's API directly?
STAGE 08
S3 & RDS — where your data lives
Week 5–6Compute is disposable; data isn't. This stage covers the two services that hold almost everything, and the single misconfiguration behind a large share of real-world breaches.
What you'll learn
- S3 buckets, objects, storage classes, and bucket policies
- Why 'make the bucket public' is almost never the right fix
- RDS basics: engines, backups, and public accessibility
Code
S3 — object storage
STEP 1 — Create a bucket
S3 → Create bucket
→ Block ALL public access: LEAVE IT ON
<KW>If you're turning this off, you almost certainly want
CloudFront with an Origin Access Control instead.</KW>
STEP 2 — Serve files safely
Public bucket → the whole internet lists + reads everything
Presigned URL → time-limited link to ONE object
CloudFront + OAC → CDN reads privately, users never touch S3
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: "my-app-uploads", Key: "invoice.pdf",
}), { expiresIn: 300 }); // 5 minutes
STEP 3 — Storage classes = cost control
Standard hot data
Intelligent-Tiering unpredictable access (safe default)
Glacier archives, retrieval takes time
RDS — managed relational database
STEP 4 — Launch
RDS → Create database → PostgreSQL
→ Template: Free tier (if eligible)
→ Public access: NO
<KW>Put it in a private subnet. Your app reaches it inside
the VPC; the internet never should.</KW>
STEP 5 — The settings that save you
Automated backups: 7 days
Multi-AZ: production only (doubles cost, survives AZ loss)
Store the password in Secrets Manager, not in your code
<KW>RDS bills whether or not anyone queries it. A forgotten
db.t3.micro is ~$15/month of nothing.</KW>BuildUpload a file to a fully private S3 bucket and serve it to a browser using a 60-second presigned URL. Confirm the link stops working after it expires.
Self-checkWhy is a publicly readable S3 bucket one of the most common causes of real data breaches?
STAGE 09
CodeArtifact & CI/CD — reproducible builds
Week 6Builds that depend on the public internet break in ways you don't control. A private registry plus a real pipeline is what makes deployments boring — which is the goal.
What you'll learn
- CodeArtifact as a private, cached package registry
- Upstream repositories — proxying npm/PyPI so builds survive upstream changes
- CodeBuild and CodePipeline: build → test → push image → deploy
Code
WHY A PRIVATE REGISTRY
- An upstream package gets yanked → your build still works
- Internal packages that must not be public
- Every dependency version is auditable and cached
STEP 1 — Create domain and repository
CodeArtifact → Create domain: my-org
→ Create repository: npm-store
→ Add upstream: npm-store → public npmjs
First request proxies from npm and caches it forever.
STEP 2 — Point npm at it
aws codeartifact login --tool npm \
--domain my-org --repository npm-store
Rewrites .npmrc with a 12-hour token.
STEP 3 — Publish an internal package
npm publish # goes to CodeArtifact, never public
STEP 4 — buildspec.yml for CodeBuild
version: 0.2
phases:
pre_build:
commands:
- aws codeartifact login --tool npm --domain my-org --repository npm-store
- aws ecr get-login-password | docker login --username AWS --password-stdin $ECR
build:
commands:
- npm ci
- npm test
- docker build -t $ECR/my-app:$CODEBUILD_RESOLVED_SOURCE_VERSION .
post_build:
commands:
- docker push $ECR/my-app:$CODEBUILD_RESOLVED_SOURCE_VERSION
STEP 5 — Wire the pipeline
CodePipeline: Source (GitHub) → Build (CodeBuild) → Deploy (ECS)
<KW>Tag images with the commit SHA, never just :latest.
":latest" makes rollback guesswork — you can't tell which
build is actually running.</KW>BuildSet up a CodeArtifact repo with an npm upstream, install a package through it, and confirm it appears in the repo's package list.
Self-checkWhy run a private package registry like CodeArtifact instead of pulling straight from the public npm registry?
STAGE 10
Cost, monitoring & not getting burned
Week 6–7The two things that actually end side projects on AWS: a bill nobody expected, and having no idea something broke. Both are preventable with an afternoon of setup.
What you'll learn
- CloudWatch logs, metrics, and alarms that are worth alerting on
- The specific resources that quietly bill forever
- Tagging, Cost Explorer, and finding what's actually costing money
Code
THE THINGS THAT BILL WHILE YOU SLEEP
NAT Gateway ~$32/mo + data ← the usual culprit
Load balancer (idle) ~$18/mo
Elastic IP unattached ~$3.6/mo free ONLY while attached
Unattached EBS volumes per GB forever terminate ≠ always deletes
Old EBS snapshots per GB forever
RDS running idle per hour regardless of queries
STEP 1 — Find what's actually costing you
Billing → Cost Explorer → Group by: Service
Then by Tag, once you're tagging things.
STEP 2 — Tag everything from day one
Project = side-project
Env = dev | prod
Untagged resources are unattributable later.
STEP 3 — Alarms on symptoms users feel
CloudWatch → Alarms → Create
- ALB 5xx count > 10 in 5 min
- Lambda Errors > 0
- RDS FreeStorageSpace < 10%
<KW>Not every CPU spike. Alerts you ignore are worse
than no alerts.</KW>
STEP 4 — Structured logs, so search works at 3am
console.log(JSON.stringify({
level: "error", event: "payment_failed",
userId, orderId, reason: err.code,
}));
<KW>CloudWatch Logs Insights can query JSON fields.
It cannot usefully query "something went wrong".</KW>
STEP 5 — The monthly habit
- Cost Explorer, grouped by service
- Delete unattached volumes + old snapshots
- Release unused Elastic IPs
- Shut down anything you launched "just to try"BuildOpen Cost Explorer on your own account, group by service, and write down your top 3 costs. Then find and delete one thing you're paying for and don't use.
Self-checkYour AWS bill jumps to ₹15,000 in a month for a small side project. Which is the most likely culprit?