# AWS Core Services

> The handful of AWS services behind most apps: S3, EC2, RDS, IAM, and Lambda - what each does, how they fit together, and the IAM model that gates it all.


---

# AWS Core Services

The AWS console lists hundreds of services, and that wall of three-letter names is enough to make anyone close the tab. Here's the secret nobody puts on the front page: most real applications run on about five of them. Learn those five and how they snap together, and the other several hundred become "things I'll look up if I ever need them" instead of "things I'm failing to understand."

This guide hands you the small, durable core - object storage, virtual machines, managed databases, the permission model, and serverless functions - plus the one mental model (who-can-do-what) that everything else hangs from.

## How to read this

Read the phases in order; they build. Phase 1 gives you the map and the names so the rest stops feeling like alphabet soup. Phase 2 wires the five services into the shape of a real app, which is where it clicks. Phase 3 is the part that saves your weekend: the IAM permission model and the mistakes that page people at 3am.

You don't need an AWS account open to follow along - every command is annotated so you can read it like prose. If you want broader context on what "cloud" even means first, see [/guides/cloud-platforms-explained](/guides/cloud-platforms-explained).

## The phases

1. [The five services that matter](01-the-five-that-matter.md) - the mental model and what each core service actually does.
2. [Wiring them into an app](02-wiring-an-app.md) - how S3, EC2, RDS, IAM, and Lambda fit together in a typical stack.
3. [IAM, least privilege, and what bites you](03-iam-and-what-bites-you.md) - the permission model in depth, plus the production gotchas.


---

# The five services that matter

Open the AWS console and you're greeted by a search box and a graveyard of acronyms: EKS, ECS, SQS, SNS, EBS, EFS, ELB, KMS, VPC, on and on. It feels like you're supposed to know all of them. You're not. That long list exists because AWS sells to everyone from a solo developer to a bank's payments division, and most of those services solve problems you will never have.

Strip it down and a normal web application - the kind you'd actually build - leans on a small core. Here's the whole mental model in one breath: you need somewhere to keep files, somewhere to run code, somewhere to store structured data, a way to say who's allowed to touch what, and a way to run small bits of code without managing a server. That's five things.

## The five, in plain language

| Service | What it is | The everyday job |
|---------|-----------|------------------|
| **S3** | Object storage | Holds files: images, uploads, backups, static sites |
| **EC2** | Virtual machines | A rented computer you SSH into and run things on |
| **RDS** | Managed relational database | A PostgreSQL/MySQL database AWS babysits for you |
| **IAM** | Identity and access management | The rules for who-can-do-what across everything above |
| **Lambda** | Serverless functions | Run a function on an event; no server to keep alive |

Read that table twice. Almost every "how do I do X on AWS" question for a typical app resolves to one of these five, or to a combination of them.

## S3 - the bucket you throw files into

S3 (Simple Storage Service) is where files live. Not a filesystem with folders and a disk you mount - an **object store**. You give it a key (a string that looks like a path) and some bytes, and it hands them back later when you ask for that key. It's effectively bottomless, durable, and cheap, and it's usually the first AWS service anyone actually uses.

```bash
# Upload a file to a bucket named "my-app-uploads"
aws s3 cp ./avatar.png s3://my-app-uploads/users/42/avatar.png

# List what's in there
aws s3 ls s3://my-app-uploads/users/42/
```

*What just happened:* you stored `avatar.png` under the key `users/42/avatar.png`. That slash-separated key *looks* like a folder path, but there are no real folders - it's one flat namespace of keys, and the slashes are a convention the console renders as folders. Your app fetches that object later by the exact same key.

If you want the full mental model of object storage - why it's flat, how durability works, presigned URLs - there's a dedicated guide at [/guides/object-storage-s3](/guides/object-storage-s3). For now, "files go in S3" is enough.

## EC2 - a computer you rent by the hour

EC2 (Elastic Compute Cloud) is a virtual machine: a Linux (or Windows) box running in an AWS data center that you control. You pick a size, it boots, you SSH in, and from there it's a normal server - install your runtime, run your app, open a port.

```bash
# Connect to an EC2 instance by its public address
ssh -i my-key.pem ec2-user@ec2-13-57-1-99.compute.amazonaws.com

# Now you're on the box - it's just Linux
[ec2-user@ip-10-0-1-23 ~]$ sudo dnf install -y nodejs
[ec2-user@ip-10-0-1-23 ~]$ node server.js
```

*What just happened:* you logged into a machine that didn't exist an hour ago and ran your app on it. The `-i my-key.pem` is your SSH private key - EC2 hands you the matching public key at launch, and that key pair is how you prove it's you. The instance is yours until you stop or terminate it, and you pay for the time it runs.

EC2 is the "give me a server, I'll handle the rest" option. Maximum control, maximum responsibility: patching, scaling, and uptime are now your job.

## RDS - a database without the babysitting

You *could* install PostgreSQL on an EC2 box yourself. RDS (Relational Database Service) is the version where AWS does the tedious, easy-to-get-wrong parts: backups, version patching, failover to a standby if the primary dies. You choose the engine (PostgreSQL, MySQL, MariaDB, and others) and the size; AWS runs the database and hands you a connection endpoint.

```bash
# Connect to an RDS PostgreSQL database, same as any Postgres
psql -h mydb.abc123.us-east-1.rds.amazonaws.com -U appuser -d production
```

*What just happened:* you connected to a managed Postgres instance using the ordinary `psql` client. From your application's point of view it's a normal database at a hostname. The difference from self-hosting is invisible until 2am, when a hardware failure that would have woken you up instead triggers an automatic failover you read about in the morning.

> The trade is control for convenience. You can't `ssh` into an RDS box or touch the OS - AWS owns that layer. In exchange, the boring, high-stakes database chores stop being yours.

## IAM - the rulebook everything obeys

IAM (Identity and Access Management) is the one that's different in kind, not only in job. The other four *do* something - store, compute, query. IAM **governs** all of them. It answers one question, constantly: *is this identity allowed to perform this action on this resource?*

Every API call to AWS - your `aws s3 cp` above, your Lambda reading from a bucket, a teammate clicking a button in the console - is checked against IAM before it runs. Get IAM right and the rest is safe. Get it wrong and you've either locked yourself out or left the front door open. Phase 3 is devoted to it, because it's the part most worth understanding deeply.

## Lambda - code without a server to keep alive

Lambda runs a single function in response to an event - an HTTP request, a file landing in S3, a message on a queue, a timer. You upload the function; AWS runs it when the event fires and bills you for the milliseconds it executes. No server to provision, patch, or keep idling.

```python
# A Lambda handler - AWS calls this function when an event arrives
def handler(event, context):
    name = event.get("name", "world")
    return {"statusCode": 200, "body": f"Hello, {name}!"}
```

*What just happened:* you defined a function AWS invokes on demand. The `event` is the trigger's payload (the HTTP request, the S3 notification, etc.); `context` carries runtime metadata. Between invocations there is no running process and no bill. This is the opposite end of the spectrum from EC2: EC2 is a machine you keep; Lambda is a function that materializes only when called.

## How to hold the five in your head

Line them up on a single axis - *how much do you manage versus how much AWS manages* - and they stop being a random list:

```text
You manage MORE                                    AWS manages MORE
   EC2  ───────────  RDS  ───────────────────────  Lambda
 (whole VM)     (managed DB,        (just your function;
                you skip the OS)     AWS runs everything else)
```

*What just happened:* the same spectrum explains S3 too - you manage *nothing* about the storage hardware, you only put and get objects. IAM sits off to the side as the gatekeeper for every box on this line. Once you see services as points on "how much is my problem," picking the right one becomes a question you can actually answer.

```quiz
[
  {
    "q": "Which AWS service is the right home for user-uploaded image files?",
    "choices": ["RDS", "S3", "IAM", "EC2"],
    "answer": 1,
    "explain": "S3 is object storage - its whole job is holding files like images, uploads, and backups by key."
  },
  {
    "q": "What does IAM actually do?",
    "choices": ["Runs your application code on a schedule", "Stores relational data with automatic backups", "Decides whether an identity may perform an action on a resource", "Provides a virtual machine you SSH into"],
    "answer": 2,
    "explain": "IAM governs the others - it answers 'is this identity allowed to do this action on this resource?' for every AWS API call."
  },
  {
    "q": "Compared to running a database yourself on EC2, what does RDS take off your plate?",
    "choices": ["Writing SQL queries", "Choosing the database engine", "Backups, patching, and failover", "Connecting with a client like psql"],
    "answer": 2,
    "explain": "RDS manages the tedious, high-stakes chores - backups, version patching, and failover - while you still write SQL and pick the engine."
  }
]
```


---

# Wiring them into an app

Knowing what each service does is half the picture. The other half - the half that makes AWS finally feel like one system instead of five separate products - is seeing how they connect. So let's build the shape of an ordinary web app: a service where users sign up, upload a profile picture, and read some data. Nothing exotic. This is the stack under a huge fraction of what's running in production today.

## The shape of a typical stack

Here's the whole thing in one diagram. Read it top to bottom: the request comes in, hits your app, which talks to a database and a file store.

```text
        Browser
          │  HTTPS
          ▼
   ┌──────────────┐        ┌─────────────┐
   │  EC2 instance│──SQL──▶│     RDS     │  (your data: users, posts)
   │  (your app)  │        └─────────────┘
   └──────┬───────┘
          │ put/get objects
          ▼
   ┌──────────────┐
   │      S3      │  (profile pictures, uploads)
   └──────────────┘
```

*What just happened:* your application code runs on **EC2**, structured data (the users table, their posts) lives in **RDS**, and the heavy files (profile pictures) live in **S3**. The app is the hub; RDS and S3 are the two stores it reaches for. That triangle - compute plus a database plus a file store - is the backbone of most web apps, on AWS or anywhere.

## The request, step by step

Walk through one user uploading a profile picture, and watch the services hand off to each other.

1. The browser sends the image to your app on the EC2 instance.
2. Your app stores the bytes in S3 and gets back a key.
3. Your app saves *that key* (a short string) in RDS, on the user's row.
4. Later, to show the picture, the app reads the key from RDS and fetches the file from S3.

```sql
-- In RDS, you store the S3 key, never the image bytes themselves
UPDATE users
SET avatar_key = 'users/42/avatar.png'
WHERE id = 42;
```

*What just happened:* notice the division of labor. RDS holds a tiny string - the **key** - not the megabytes of image data. The actual bytes sit in S3, which is built for exactly that. Stuffing image blobs into your database is a classic early mistake: it bloats backups, slows queries, and costs more. The pattern "files in S3, the pointer to them in the database" is one you'll reuse forever.

## Where Lambda fits in

You don't strictly need Lambda for this app - EC2 can do everything. But Lambda shines for work that happens *in reaction to something* and doesn't need a server sitting idle. The cleanest example: the moment a file lands in S3, run code on it.

```text
   User uploads ──▶  S3  ──(object-created event)──▶  Lambda
                                                        │
                                                        ▼
                                              make a thumbnail,
                                              write it back to S3
```

*What just happened:* S3 emits an event whenever an object is created, and that event triggers a Lambda function automatically. The function generates a thumbnail and saves it back to S3 - all without your EC2 app lifting a finger or a dedicated server running 24/7 waiting for uploads. This **event-driven** style is where serverless earns its keep: spiky, occasional work that would otherwise need an always-on machine.

> A useful instinct: reach for EC2 for the long-running app that's always handling requests, and reach for Lambda for the bursty side-jobs hanging off events. Many real systems run both.

## The glue you can't see: IAM connects them too

Here's the part that surprises people. When your EC2 app writes to S3, or your Lambda reads from a bucket, *that's an AWS API call, and IAM checks it.* These services don't trust each other by default - every cross-service action needs permission.

The right way to grant it is a **role**: a bundle of permissions you attach to the EC2 instance or the Lambda function. The code then talks to S3 with no hardcoded keys at all, because the role provides temporary credentials automatically.

```python
# Code running on EC2 or Lambda with an attached role.
# Notice: no access keys anywhere - the role supplies them.
import boto3

s3 = boto3.client("s3")
s3.put_object(
    Bucket="my-app-uploads",
    Key="users/42/avatar.png",
    Body=image_bytes,
)
```

*What just happened:* `boto3` (the AWS SDK for Python) found credentials from the attached role automatically - you never wrote an access key or secret. The role says "this instance may put objects into `my-app-uploads`," IAM checks that on the call, and it succeeds. If you'd skipped the role, this exact code would fail with an access-denied error. We'll dig into how roles and policies are written in the next phase; for now, hold onto the idea that **IAM is the wiring between the services, not a thing off to the side.**

## For builders: the smallest viable AWS app

If you were standing up this app today, the minimum is smaller than you'd think:

```text
1 EC2 instance        →  runs your app
1 RDS database        →  holds your data
1 S3 bucket           →  holds your files
1 IAM role on the EC2 →  lets the app reach S3 and RDS safely
( Lambda - add later when you have event-driven work )
```

*What just happened:* four pieces get a real application online. You can grow from here - load balancers, multiple instances, caching - but none of that is required to ship. Starting with the core and adding only when a real need shows up keeps both your bill and your mental load down.

```quiz
[
  {
    "q": "When a user uploads a profile picture, what gets stored in RDS?",
    "choices": ["The full image bytes", "The S3 key pointing to the image", "Nothing - RDS isn't involved", "A thumbnail of the image"],
    "answer": 1,
    "explain": "RDS stores the short S3 key (a string); the actual image bytes live in S3. Files in S3, the pointer to them in the database."
  },
  {
    "q": "What's the best fit for generating a thumbnail the moment a file is uploaded to S3?",
    "choices": ["A cron job on the RDS instance", "A Lambda function triggered by the S3 object-created event", "Polling S3 from the browser", "A second EC2 instance running constantly"],
    "answer": 1,
    "explain": "S3 emits an event on object creation that triggers Lambda automatically - ideal for bursty, event-driven work with no idle server."
  },
  {
    "q": "How should code on EC2 get permission to write to an S3 bucket?",
    "choices": ["Hardcode an access key and secret in the source", "Use an IAM role attached to the instance", "Make the bucket fully public", "It doesn't need permission - same account"],
    "answer": 1,
    "explain": "An attached IAM role supplies temporary credentials automatically, so the code needs no hardcoded keys and IAM still checks every call."
  }
]
```


---

# IAM, least privilege, and what bites you

This is the phase that earns the price of admission. Almost everyone who learns AWS learns the fun services first - spin up a server, store a file - and treats IAM as paperwork to skip. Then they spend a frustrating afternoon staring at `AccessDenied`, or worse, they read a headline about a company that left a bucket open. IAM is where AWS is genuinely hard and genuinely important. Understand its model and you've understood the thing that actually keeps your account safe.

## The shared-responsibility line

Start with the deal AWS is offering, because it sets up everything else. Security on AWS is split:

- **AWS secures the cloud** - the buildings, the hardware, the hypervisor, the network between data centers. Not your problem.
- **You secure what's *in* the cloud** - your data, who has access, how your app is configured, your IAM rules.

```text
   ┌─────────────────────────────────────────┐
   │  YOU:  data, access control, app config  │  ← your job
   ├─────────────────────────────────────────┤
   │  AWS:  hardware, network, data centers   │  ← AWS's job
   └─────────────────────────────────────────┘
```

*What just happened:* the line is drawn at the resources you create. AWS guarantees the floor is solid; what you put on it, and who you let in, is on you. Nearly every famous "AWS breach" is actually a customer-side misconfiguration - a permission left too wide - not AWS's infrastructure failing. Knowing which side of the line a problem sits on tells you who has to fix it.

## The four nouns of IAM

IAM has a small vocabulary. Learn these four and the policies stop looking like hieroglyphics.

| Term | What it is |
|------|-----------|
| **User** | A long-lived identity for a person (or sometimes a legacy script) |
| **Role** | An identity a service or person *assumes* temporarily, with no permanent password |
| **Policy** | A JSON document listing allowed (or denied) actions on resources |
| **Group** | A bucket of users that share a set of policies |

The one to internalize is **role**. A user has standing credentials that live forever until rotated. A role is borrowed: an EC2 instance or a Lambda function assumes a role and receives *temporary* credentials that expire on their own. That expiry is the whole point - there's no long-lived secret to leak.

## Reading a policy

A policy is the actual rulebook, and it's plain JSON. Here's one that lets something read and write objects in a single bucket - exactly what your app's role from Phase 2 would carry.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-app-uploads/*"
    }
  ]
}
```

*What just happened:* this grants two actions - `s3:GetObject` (read) and `s3:PutObject` (write) - and **only** on objects inside `my-app-uploads` (the `/*` means "every object in that bucket"). It says nothing about deleting, nothing about other buckets, nothing about any other service. Anything not explicitly allowed is denied. That default-deny is IAM's most important rule: permissions are additive from a starting point of "no."

The `arn:aws:s3:::my-app-uploads/*` is an **ARN** (Amazon Resource Name), AWS's universal address for a resource. You'll see ARNs everywhere; they're how a policy names exactly what it's talking about.

## Least privilege, made concrete

"Least privilege" sounds like a lecture. In practice it's one habit: **grant the narrowest permission that makes the task work, then stop.** The temptation, especially when you're fighting an `AccessDenied` at the end of a long day, is to reach for the wildcard:

```json
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}
```

*What just happened:* this says "this identity can do anything to everything in the account." It will absolutely make your error go away - and it's the policy attached to most of the credentials that have ever been stolen and used to ransack an account. A role this wide, if its credentials leak, hands an attacker your entire AWS footprint. The narrow policy from the previous section, leaked, costs you one bucket's contents. That gap is the entire argument for least privilege.

> The straightforward workflow: start with nothing, run your code, read the exact action it was denied, and add *that one action* on *that one resource*. It's slower than `"*"` for about ten minutes and saves you from a catastrophe you'll never see coming.

## The gotchas that actually bite

Here are the ones that cost people real time and real money.

**The public S3 bucket.** The single most famous AWS mistake. A bucket policy or ACL set to allow public access means anyone on the internet can read (or sometimes write) your files. AWS now blocks public access by default and warns loudly, but people still override it by accident.

```bash
# Check whether a bucket is exposing public access
aws s3api get-public-access-block --bucket my-app-uploads
```

*What just happened:* this asks AWS whether the four public-access guardrails are on for that bucket. If they're enabled, the bucket can't be made public even by a careless policy. Run this on anything holding private data, and treat "public" as a deliberate choice you make for a static-website bucket, never a default you drift into.

**Hardcoded access keys.** Pasting a long-lived access key and secret into source code, then committing it, is how credentials end up scraped from public repos within minutes. Bots watch for exactly this.

```text
BAD:   AWS_ACCESS_KEY_ID = "AKIA..."  hardcoded in app.py, pushed to GitHub
GOOD:  an IAM role attached to the EC2/Lambda - no key in the code at all
```

*What just happened:* the fix isn't "hide the key better," it's "don't have a key." Roles (Phase 2) give your running code temporary credentials with no secret to commit. For local development, keep keys in `~/.aws/credentials`, never in the repo, and add that path's equivalents to `.gitignore`.

**The root account.** Every AWS account has a root user tied to the sign-up email, and it can do *anything*, including things no IAM policy can restrict. Using it day-to-day is like doing your daily computing as the administrator with no password - one slip is total.

```text
Root account:  lock it down - strong password, MFA, then don't log in with it.
Daily work:    a separate IAM user (or SSO identity) with only the access you need.
```

*What just happened:* the convention is to secure root, turn on MFA (multi-factor authentication) for it, and then walk away from it for everyday tasks. You create regular IAM identities scoped to what each person actually does. The root user is the fire-axe behind glass, not the door you use every morning.

## In the wild

The mature pattern, once a team grows past one person, is to stop minting individual IAM users entirely and route human access through single sign-on (often AWS IAM Identity Center), with roles granting time-limited access. The machines - EC2, Lambda, and friends - keep using roles as we've described. The thread tying all of it together is the same one from Phase 1: every action is an identity asking permission, and IAM is the thing that says yes or no. Get comfortable reading and writing these small JSON policies and you've got the load-bearing skill for everything else AWS will throw at you.

```quiz
[
  {
    "q": "Under the AWS shared-responsibility model, who is responsible for setting correct access permissions on your data?",
    "choices": ["AWS - it's part of the infrastructure", "You - access control is the customer's side of the line", "It's split evenly by default", "The S3 service handles it automatically"],
    "answer": 1,
    "explain": "AWS secures the underlying hardware and network; you are responsible for your data and who can access it, including IAM rules."
  },
  {
    "q": "Why is an IAM role preferred over a long-lived access key for code running on EC2?",
    "choices": ["Roles are faster at runtime", "Roles provide temporary credentials with no permanent secret to leak", "Roles allow wildcard permissions by default", "Keys don't work on EC2"],
    "answer": 1,
    "explain": "A role supplies temporary, auto-expiring credentials, so there's no long-lived secret to hardcode, commit, or have stolen."
  },
  {
    "q": "What does the principle of least privilege tell you to do?",
    "choices": ["Grant Action '*' and Resource '*' to avoid AccessDenied errors", "Use the root account for all work to keep things simple", "Grant the narrowest permission that makes the task work, and no more", "Make S3 buckets public so any service can reach them"],
    "answer": 2,
    "explain": "Least privilege means granting only the specific actions on the specific resources needed - so a leaked credential does minimal damage."
  }
]
```
