Learn AWS by deploying one chat app ☁️

No service catalog to memorize. We take MiniChat, an AI chat app, and put it live on AWS one need at a time. Each service earns its place by solving a real problem, and every idea starts with a real-world picture.

The build: users must reach the app, code must run, messages must be stored, the AI must be called, slow jobs must not block, and the whole thing must be secure and observable. We start with an empty account and end with a full reference architecture.
Opened 0/17 · tap any section to expand
1

Where your app physically livesRegions, AZs & shared responsibility

🤔 Goal: put MiniChat "in the cloud." But the cloud is real computers in real buildings. If they all sit in one building and that building loses power, MiniChat is down. And who patches those machines, you or Amazon?
🌍 Think of it like
A Region is a city, an Availability Zone is a separate building in that city with its own power and network. You spread copies across buildings so one flood or outage never takes you fully down.

A Region (like us-east-1, N. Virginia) is a geographic area. Inside it are 3+ Availability Zones, each an isolated datacenter miles apart but linked by fast fiber. You pick a Region close to your users and spread across its AZs for resilience.

Region: us-east-1 (a city) AZ us-east-1a server copy own power + net AZ us-east-1b server copy miles apart AZ us-east-1c server copy fast fiber link

Shared Responsibility: Amazon secures the buildings, hardware, and hypervisor (security of the cloud). You secure your data, access, and config (security in the cloud). Most breaches are the second half, a misconfigured bucket, not a hacked datacenter.

Now the words: a Region is a named geographic area; an Availability Zone (AZ) is one or more discrete datacenters within it. Edge locations (used by CloudFront) are a separate, much larger set of points-of-presence for caching. Multi-AZ = highly available; multi-Region = disaster recovery.

2

Who is allowed to do whatIAM: users, roles & policies

🤔 Goal: MiniChat's code needs to read the database and call the AI, nothing else. If you hand it your all-powerful account password, a single bug or leak hands an attacker the whole account.
🌍 Think of it like
Keycards in an office. You don't give the intern the master key. You issue a card that opens only the rooms they need. A role is a keycard on a hook that anyone (or any service) can borrow for a shift, never a permanent card in their pocket.

A policy is the list of what's allowed (JSON). A user is a human with long-lived credentials. A role is a set of permissions that a service or person temporarily assumes, getting short-lived keys that auto-expire. Give MiniChat's Lambda a role, never a user password.

the keycard for MiniChat's Lambda · least-privilege policy
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["dynamodb:PutItem", "dynamodb:Query"],
    "Resource": "arn:aws:dynamodb:us-east-1:111122223333:table/Messages"
  },{
    "Effect": "Allow",
    "Action": "bedrock:InvokeModel",
    "Resource": "arn:aws:bedrock:*::foundation-model/anthropic.claude-*"
  }]
}
Never use the root account for daily work. Turn on MFA, lock the root keys in a drawer, and do everything through IAM roles/users. Avoid "Action": "*" on "Resource": "*", that is the master key you swore not to hand out.

Now the words: the JSON is an IAM policy. When Lambda "borrows the keycard" it is assuming a role via STS, which returns temporary credentials. Least privilege means grant only the exact actions and resources needed. Policies can be identity-based (attached to who) or resource-based (attached to what, like a bucket policy).

3

Where should the code runEC2 vs Lambda vs ECS/Fargate

🤔 Goal: MiniChat's API has to run somewhere. Rent a whole always-on server? Only pay when a request hits? Package it as a container? Picking wrong means either overpaying for idle machines or fighting cold starts.
🌍 Think of it like
Getting around a city. EC2 is owning a car (full control, but you park, insure, and maintain it 24/7). Lambda is a taxi (appears when you call, meter runs only during the ride, vanishes after). Fargate is a rental car (your own vehicle spec, but you don't own the fleet or the garage).

For MiniChat's bursty, event-driven API we'll pick Lambda: no servers to manage, pay per request, scales to zero. If we later needed a long-running websocket server or a big custom runtime, we'd reach for Fargate (containers, no server management) or EC2 (full VM control).

OptionYou manageBillingBest when
EC2 (virtual machine)OS, patching, scaling, capacityPer second the instance runs (idle still costs)Full control, legacy apps, GPUs, steady load
Lambda (functions)Just your codePer request + ms of compute, scales to zeroBursty, event-driven, short jobs (≤15 min)
ECS/Fargate (containers)The container image onlyPer vCPU/GB while tasks runContainers, long-running services, no server ops

Now the words: EC2 = Elastic Compute Cloud (rented VMs called instances). Lambda = Function as a Service (FaaS), serverless. ECS = Elastic Container Service (orchestrator); Fargate = the serverless engine that runs ECS/EKS tasks without you managing EC2. EKS = managed Kubernetes if you specifically want k8s.

4

The function that runs MiniChatLambda: handler, cold starts, concurrency

🤔 Goal: run our chat logic without babysitting a server. But if there's no server, what actually executes our code when a request arrives, and why is the first call sometimes slow?
🌍 Think of it like
A pop-up food stall. When an order (event) comes in, AWS sets up the stall (loads your code into a container), cooks, then may tear it down. The first order after a quiet spell waits for setup, that wait is the cold start. Busy periods keep stalls warm and instant.

You write a handler function. AWS runs it in a fresh micro-container triggered by an event (an HTTP call, a queue message, a file upload). You set its memory (which also scales CPU) and a timeout. Each concurrent request gets its own copy, that's concurrency.

3 requests arrive → 3 isolated Lambda copies req 1 req 2 req 3 copy A · COLD start copy B · warm copy C · warm DynamoDB+ Bedrock
MiniChat's real handler · Node.js
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
const bedrock = new BedrockRuntimeClient({});   // reused across warm invokes

export const handler = async (event) => {           // the entry point
  const { message } = JSON.parse(event.body);
  const out = await bedrock.send(new ConverseCommand({
    modelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
    messages: [{ role: "user", content: [{ text: message }] }]
  }));
  return { statusCode: 200,
    body: JSON.stringify({ reply: out.output.message.content[0].text }) };
};
Cold start fixes: put the SDK client outside the handler so warm invokes reuse it; keep the deployment package small; bump memory (more memory = more CPU = faster init); or use Provisioned Concurrency / SnapStart to keep copies pre-warmed. Timeout caps at 15 minutes, so long AI jobs go to a queue (section 10).

Now the words: the invoked function is a Lambda; its trigger is an event source; the number of simultaneous copies is concurrency (default account cap ~1000, adjustable). Reusing globals between invokes is execution context reuse.

5

The public front doorAPI Gateway + Lambda

🤔 Goal: the browser needs a URL to POST messages to. But a Lambda has no public address by default, and you don't want the raw internet hammering your function with no auth, throttling, or HTTPS.
🌍 Think of it like
The reception desk of a building. Visitors don't wander to your office directly. They check in at the front desk, which verifies them, logs them, limits the crowd, and then routes them to the right room.

Put API Gateway in front of Lambda. It gives you a public HTTPS endpoint, maps POST /chat to your function, and handles auth, rate limiting, and CORS so the function only ever sees clean, allowed requests.

BrowserPOST /chat API GatewayHTTPS · auth · throttle Lambdachat handler Bedrock

Two flavors: HTTP API (cheaper, faster, fewer features, great default for Lambda) and REST API (older, more knobs like request validation and API keys). For MiniChat, HTTP API is the right call.

Now the words: the mapping from a route to Lambda is an integration. Auth can be a Lambda authorizer or a Cognito / JWT authorizer. Throttling caps requests per second; usage plans + API keys meter per client. CORS config lets your S3-hosted frontend call the API.

6

Serve the React app & store filesS3: buckets, classes, presigned URLs

🤔 Goal: MiniChat's built React app is just static files (HTML/JS/CSS). Running a whole server 24/7 just to hand out static files is wasteful. And later, users will upload images to chat, where do those go?
🌍 Think of it like
An infinite self-storage facility. You rent a numbered unit (a bucket), drop labeled boxes (objects) in, and each box has a globally unique address. You never think about how big the warehouse is, it just grows.

S3 stores objects (files) in buckets, addressed by a key. It's not a disk you mount; it's an HTTP key/value store for blobs. Point S3 static hosting (fronted by CloudFront) at MiniChat's build output and the frontend is served, no server required.

upload MiniChat's build · watch the object keys
bucket: minichat-web (empty)

Presigned URLs solve uploads securely: your backend signs a short-lived URL that lets the browser PUT one file straight to S3, without your credentials ever touching the client, and without proxying big files through Lambda.

// backend hands the browser a 5-min upload link
const url = await getSignedUrl(s3, new PutObjectCommand({
  Bucket: "minichat-uploads", Key: `user/${id}/pic.png`
}), { expiresIn: 300 });
Storage classUse for
S3 StandardHot files served often (the web app, recent uploads)
S3 Intelligent-TieringUnknown/variable access, auto-moves to save cost
Standard-IA / One Zone-IAInfrequent access, cheaper storage, retrieval fee
Glacier / Deep ArchiveBackups, compliance, minutes-to-hours to restore
Event notifications: S3 can fire an event when an object lands (e.g. a new upload), triggering a Lambda to make a thumbnail or scan it. That's how MiniChat processes images without polling.

Now the words: S3 = Simple Storage Service. Buckets are globally unique names; objects have keys. Modern S3 gives strong read-after-write consistency automatically. Block Public Access is on by default, serve the site via CloudFront + OAC, not a public bucket.

7

Store the chat messagesDynamoDB: partition & sort keys

🤔 Goal: save every message and instantly load one conversation's history. Millions of messages, single-digit-millisecond reads, no server to scale. A classic SQL box would need careful tuning to keep up and never scales to zero.
🌍 Think of it like
A giant filing cabinet with numbered drawers. The partition key is which drawer (say, the conversation id). The sort key is the order of folders inside that drawer (the timestamp). Ask for a drawer and you get all its folders already in order, instantly, no matter how many drawers exist.

DynamoDB is a managed NoSQL key/value + document store. You design around your access pattern: pick a partition key that spreads data evenly and a sort key that gives you free ordering and range queries. For MiniChat: PK = CONV#42, SK = TS#2026-07-02T10:00.

Query PK = CONV#42 → whole conversation, in order Partition CONV#42 TS#10:00 · "hi" TS#10:01 · "help?" TS#10:02 · "sure" Partition CONV#77 TS#09:30 · ... Partition USER#9 PROFILE · ...
write & query one conversation · live
Query vs Scan: Query jumps straight to one partition (fast, cheap). Scan reads the entire table (slow, expensive), avoid it in hot paths. A bad partition key (e.g. everyone in one drawer) creates a hot partition.

Now the words: partition key + sort key = the primary key. Want another access pattern? Add a Global Secondary Index (GSI). Cramming many entity types (users, messages, convos) into one table with clever key prefixes is single-table design. Billing is on-demand or provisioned capacity.

8

When you need SQL & vectorsRDS / Aurora + pgvector

🤔 Goal: MiniChat needs to answer questions from the company handbook (RAG). That means storing text chunks as embeddings and finding the ones closest in meaning to a question. DynamoDB doesn't do "nearest neighbor" search, and some data (billing, joins across tables) genuinely wants SQL.
🌍 Think of it like
A librarian who shelves by topic, not title. Embeddings turn each paragraph into coordinates on a "meaning map." A question lands at a point, and you grab the paragraphs nearest to it, even if they share no exact words.

RDS is managed relational databases (PostgreSQL, MySQL, etc.), AWS handles backups, patching, failover. Aurora is AWS's cloud-native, faster, auto-scaling flavor. Add the pgvector extension to Postgres and it can store embedding vectors and run similarity search with plain SQL.

retrieve handbook chunks closest to the question · SQL
-- pgvector: find the 3 most similar chunks
SELECT chunk, 1 - (embedding <=> :q_vec) AS similarity
FROM handbook
ORDER BY embedding <=> :q_vec   -- <=> = cosine distance
LIMIT 3;
DynamoDB vs RDS, the honest cut: reach for Dynamo when access patterns are known, simple, and need massive scale + scale-to-zero. Reach for RDS/Aurora when you need flexible queries, joins, transactions, or vector search. MiniChat uses both, Dynamo for chat history, Aurora+pgvector for RAG.

Now the words: an embedding is a list of numbers representing meaning. RAG = Retrieval-Augmented Generation: fetch relevant chunks, stuff them into the prompt, then let the model answer grounded in them. Aurora Serverless v2 scales capacity automatically. Alternatives: OpenSearch or Bedrock Knowledge Bases (next section) for a fully managed vector store.

9

MiniChat's brain: the AI itselfAmazon Bedrock, deep dive

🤔 Goal: actually generate replies. You could rent GPUs, download model weights, and run inference yourself, then drown in ops, scaling, and safety. You just want to call a great model over an API, keep your data private, and swap models without rewriting code.
🌍 Think of it like
A power outlet for intelligence. You don't build a power plant to run a lamp. Bedrock is the socket: plug in, draw as much "smarts" as you need, pay by usage, and switch which plant supplies you (Claude, Titan, Llama) without rewiring the lamp.

Amazon Bedrock is one API to reach many foundation models (including Anthropic's Claude). No servers, your prompts aren't used to train the base models, and you pick a model by id. Three ways to call it, from oldest to what you should use:

APIWhat it doesUse when
InvokeModelRaw request/response in the model's own JSON shapeLow-level or non-chat models (e.g. embeddings)
ConverseOne unified chat schema across all models, full reply at onceStandard chat, easy model swapping
ConverseStreamSame, but streams tokens as they generateMiniChat, so words appear live
MiniChat calling Claude · JS SDK v3 · Converse
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
const client = new BedrockRuntimeClient({ region: "us-east-1" });

const res = await client.send(new ConverseCommand({
  modelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
  system: [{ text: "You are MiniChat, concise and friendly." }],
  messages: [{ role: "user", content: [{ text: "What are embeddings?" }] }],
  inferenceConfig: { maxTokens: 512, temperature: 0.7 }
}));
console.log(res.output.message.content[0].text);
same call · .NET · AWSSDK.BedrockRuntime
using Amazon.BedrockRuntime;
using Amazon.BedrockRuntime.Model;

var client = new AmazonBedrockRuntimeClient();
var res = await client.ConverseAsync(new ConverseRequest {
    ModelId = "anthropic.claude-sonnet-4-5-20250929-v1:0",
    Messages = { new Message {
        Role = ConversationRole.User,
        Content = { new ContentBlock { Text = "What are embeddings?" } }
    }},
    InferenceConfig = new InferenceConfiguration { MaxTokens = 512, Temperature = 0.7f }
});
Console.WriteLine(res.Output.Message.Content[0].Text);

The RAG payoff: Bedrock Knowledge Bases is managed RAG. Point it at an S3 folder of docs; it chunks, embeds (with Titan embeddings), stores vectors, and on a query retrieves the relevant chunks and feeds them to the model. You skip building the pipeline in section 8 yourself.

RAG with Bedrock Knowledge Bases Docs in S3handbook.pdf Titan embeddingschunk → vectors Vector storeOpenSearch / pgvector User question Retrieve top-knearest chunks Claude answersgrounded in chunks
Guardrails: Bedrock Guardrails sit around any model to block disallowed topics, filter hate/violence, redact PII, and reduce hallucination, one config applied consistently, so MiniChat stays safe regardless of which model you swap in.

Now the words: a foundation model is a large pretrained model you call, not train. InvokeModel = per-model shape; Converse/ConverseStream = unified shape (prefer these). Titan = Amazon's own model family (text + embeddings). Knowledge Bases = managed RAG; Guardrails = policy layer. Enable model access per-region in the console first.

10

Long AI jobs must not blockSQS & SNS: queues vs pub/sub

🤔 Goal: a user asks MiniChat to summarize a 100-page PDF. That takes minutes, longer than an HTTP request should wait and past Lambda's window if done inline. If Bedrock is briefly rate-limited, you don't want to just drop the request either.
🌍 Think of it like
A restaurant order rail. The waiter (API) doesn't stand at the table until the food is cooked. They clip the ticket to the rail (SQS queue) and walk away. The kitchen (workers) pulls tickets when free. SNS is the opposite: the chef shouts "order up!" and every station that cares hears it at once (pub/sub).

SQS is a queue: one message, pulled by one worker, retried if it fails. SNS is a broadcast: one message, fanned out to many subscribers. For MiniChat, the API drops a "summarize" job on SQS and returns instantly; a worker Lambda processes it and notifies the user when done.

SQS: decouple & buffer (one worker per message) API drops job SQS queuebuffers, retries worker Lambdasummarize PDF SNS: fan out one event to many (pub/sub) "job done" SNS topic email user update DynamoDB analytics log
Dead-letter queue (DLQ): if a worker keeps failing on a poison message, SQS moves it to a DLQ after N tries so it stops blocking the line and you can inspect it. Always configure one.

Now the words: SQS = Simple Queue Service (point-to-point, pull, at-least-once; FIFO variant for strict order). SNS = Simple Notification Service (pub/sub, push). EventBridge is the richer event bus for routing by content. Visibility timeout hides a message while one worker processes it.

11

The private networkVPC: subnets, SGs vs NACLs

🤔 Goal: MiniChat's Aurora database must never be reachable from the open internet, only from our Lambda. But the web tier does need to be reachable. How do you make some things public and some things sealed off?
🌍 Think of it like
An office building with floors. The VPC is the whole building. Public subnets are the lobby (open to the street). Private subnets are the back offices (no street door). A security guard checks each person at each office door (security group), while building-wide rules cover whole floors (NACL).

A VPC is your own isolated network. You carve it into subnets: public ones (route to an internet gateway) for anything internet-facing, private ones (no direct internet route) for databases. Put Aurora in a private subnet; nothing outside can even reach it.

VPC 10.0.0.0/16 (the building) Public subnet (lobby) Internet Gateway ↔ Load Balancer reachable from internet Private subnet (back office) Lambda Aurora DB no route from internet
Security GroupNACL
Attached toIndividual resource (per office door)Whole subnet (whole floor)
StateStateful (reply traffic auto-allowed)Stateless (must allow both directions)
RulesAllow onlyAllow and deny, evaluated in order
NAT gateway cost trap: a private subnet that needs outbound internet (to pull packages, call an external API) routes through a NAT gateway, which bills hourly plus per-GB. For AWS services, prefer VPC endpoints to skip NAT entirely and stay on the AWS backbone.

Now the words: VPC = Virtual Private Cloud. Security group = stateful, instance-level firewall (allow rules). NACL = network ACL, stateless, subnet-level (allow + deny). Internet Gateway for public egress; NAT Gateway for private outbound; VPC endpoints for private access to AWS services.

12

See what's happeningCloudWatch logs, metrics, alarms + X-Ray

🤔 Goal: MiniChat is live and a user says "it's slow." Which piece? Lambda? Bedrock? The DB? Without visibility you're guessing in the dark, and you won't even know it broke until users complain.
🌍 Think of it like
The dashboard and black box of a car. Metrics are the live gauges (speed, fuel). Logs are the black-box recorder of every event. Alarms are the warning lights that flash before you break down. X-Ray is the mechanic tracing one trip end to end to find the slow part.

CloudWatch is AWS's built-in observability. Lambda auto-ships logs and metrics (invocations, errors, duration). You set alarms on a metric (e.g. error rate > 1%) that page you or trigger auto-scaling. X-Ray traces a single request across API Gateway → Lambda → Bedrock so you see exactly where the time went.

an alarm watching MiniChat's error rate · live
errors: 0 / total: 0 · state: OK
Cheap early win: a single CloudWatch alarm on Lambda Errors and on Throttles catches most incidents before customers do. Structured JSON logs make searching in Logs Insights trivial.

Now the words: a log group holds a service's log streams; metrics are time-series numbers; an alarm fires an action (SNS, auto-scaling) when a metric crosses a threshold. X-Ray = distributed tracing. CloudTrail is separate: it logs who did what in your account (audit), not app behavior.

13

Build it repeatably, not by clickingCloudFormation vs CDK vs Terraform

🤔 Goal: we clicked together buckets, Lambdas, tables, and a VPC. Now recreate it exactly in a staging account, and remember every setting six months later. Clicking is unrepeatable and undocumented.
🌍 Think of it like
A recipe vs cooking from memory. Infrastructure as Code writes the recipe down. Anyone can recreate the identical dish, review changes as a diff, and roll back a bad batch, instead of hoping you remember which knobs you turned.

Describe your infra in files, commit them to git, and let a tool create/update it. Change the file, apply, and AWS reconciles reality to match. No more "works in prod because someone clicked something once."

ToolWhat it isPick when
CloudFormationAWS-native YAML/JSON templatesPure AWS, no extra tooling, declarative
AWS CDKWrite infra in TypeScript/Python; compiles to CloudFormationYou want loops, types, and real code for infra
TerraformThird-party, multi-cloud (HCL)Multi-cloud, or a team standard on Terraform
# CloudFormation: MiniChat's message table, declared
Resources:
  MessagesTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - { AttributeName: pk, AttributeType: S }
        - { AttributeName: sk, AttributeType: S }
      KeySchema:
        - { AttributeName: pk, KeyType: HASH }
        - { AttributeName: sk, KeyType: RANGE }

Now the words: a stack is one deployed CloudFormation unit; drift is when someone clicks a change outside the template. SAM is a CloudFormation flavor tuned for serverless. CDK gives you loops and abstraction but you still deploy CloudFormation underneath.

14

Secrets, encryption & the billKMS, Secrets Manager, cost traps

🤔 Goal: MiniChat has a DB password and third-party API keys. Hard-coding them in the Lambda env in plaintext means anyone with read access to the config, or a leaked repo, owns them. And how do you keep the bill from quietly ballooning?
🌍 Think of it like
A bank vault with a master key. KMS holds the one master key (that never leaves) and lends you locked boxes. Secrets Manager is the safe-deposit room that stores your valuables, rotates the combinations automatically, and logs every access.

KMS manages encryption keys; S3, DynamoDB, and Aurora use it to encrypt data at rest with one checkbox. Secrets Manager stores credentials, rotates them, and hands them to your code at runtime via an IAM-controlled call, never checked into code or pasted into plaintext env vars.

// fetch the DB password at runtime, never hard-coded
const sm = new SecretsManagerClient({});
const { SecretString } = await sm.send(
  new GetSecretValueCommand({ SecretId: "minichat/db" }));
Top cost traps that surprise people:
1. NAT Gateway: hourly + per-GB even when idle. Use VPC endpoints for AWS services.
2. Data egress: data leaving AWS to the internet is billed per GB; cross-AZ traffic costs too.
3. Idle resources: forgotten EC2, unattached EBS volumes, old snapshots, provisioned-but-unused capacity.
4. No budget alarm: set an AWS Budgets alert on day one so a runaway loop doesn't cost thousands silently.

Now the words: KMS = Key Management Service (envelope encryption, keys never leave). Secrets Manager = stored + rotated secrets (vs SSM Parameter Store, cheaper, no auto-rotation). Encryption at rest (stored data) vs in transit (TLS). AWS Budgets + Cost Explorer for spend visibility.

15

MiniChat, fully assembledthe reference architecture

🤔 Goal: put every piece together into one production-shaped diagram, so you can trace a single message from a user's keyboard to the AI and back.

Follow the arrows: the browser loads the app from S3 via CloudFront, calls API Gateway, which invokes a Lambda. The Lambda writes to DynamoDB, calls Bedrock for the reply, and for slow jobs drops a message on SQS for a worker. Everything sits inside a VPC, uses IAM roles, pulls secrets from Secrets Manager, and reports to CloudWatch.

BrowserReact app CloudFront + S3 (static site) API GatewayPOST /chat Lambdachat handler DynamoDB save msg BedrockClaude reply SQS → worker slow jobs KnowledgeBase (RAG) Cross-cutting (wrap everything) VPCprivate subnets IAM rolesleast privilege Secrets Mgr+ KMS CloudWatchlogs · alarms
The one-message journey: user types → CloudFront-served app POSTs to API Gateway → Lambda (in a role) saves to DynamoDB and calls Bedrock (grounded by the Knowledge Base) → streams the reply back. A "summarize this PDF" request instead lands on SQS and a worker handles it, notifying via SNS. All of it inside a VPC, secrets in Secrets Manager, watched by CloudWatch. That is a real, defensible serverless architecture.
16

Interview rapid-firetap a card to flip

17

Mistakes that fail an AWS reviewanti-patterns