AWS, without the fog.

AWS has 200+ services and almost all of them are noise when you're starting. This path takes the dozen that actually matter — IAM, EC2, VPC, Docker, ECS/Fargate, Lambda, EventBridge, S3, RDS, CodeArtifact — in the order they make sense, with every step written out click by click.

Duration
6–7 weeks
Format
Step-by-step
Prerequisite
Comfortable in a terminal
You'll build
A deployed containerised app

Every step written out, in order

No "just configure the VPC" hand-waving. Each stage is numbered steps — which console screen, which button, which command — plus the reason behind it, so you can adapt when your screen looks slightly different.

A live playground, right here

No AWS account needed for this one. It's the bill for a "small" project that nobody cleaned up — the single most common thing that ends a side project on AWS. Edit the numbers and see what idle infrastructure really costs.

playground.js
Console
Run the code to see output here.

This is what's waiting at the end

Certificate of Completion
Your Name
has completed AWS, Without the Fog — 11 stages, 11 passed quizzes
11 / 11 stagesDeployed on AWSVerified ID
🔒

Complete all 11 stages and their quizzes to unlock this.

Eleven stages, in this order

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 1

Most 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–2

IAM 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 2

EC2 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–4

This 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 4

Lambda 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 5

EventBridge 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–6

Compute 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 6

Builds 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–7

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

The capstone: your app, live on AWS

Take any app you've built, containerise it, and run it on AWS properly — IAM roles instead of keys, a private subnet for the database, a scheduled Lambda, a budget alarm, and a documented teardown. "I deployed it on AWS" is common; being able to explain every one of those choices is not.

Containerised, pushed to ECR, running on Fargate
IAM roles — zero long-lived access keys anywhere
Database in a private subnet, not publicly reachable
One scheduled Lambda via EventBridge
Budget alarm + CloudWatch alarm on 5xx
A written teardown checklist that zeroes the bill

Common pitfalls

  • Access keys in code. Use IAM roles for services. A key committed to git is someone else mining crypto on your card.
  • AdministratorAccess on everything. It works, so nobody fixes it — until one leaked credential owns the whole account.
  • Forgetting the NAT Gateway. ~$32/month whether or not a single byte flows. The classic surprise bill.
  • 0.0.0.0/0 on port 22. Bots find open SSH within minutes. Restrict it to your IP.

Resources worth your time

  • AWS Free Tier pagecheck limits
  • AWS Well-Architectedthe framework
  • AWS Skill Builderfree courses
  • Docker docsreference
  • AWS Pricing Calculatorestimate first