
EE 547 - Unit 3
Fall 2026
Identity and Access Management
A development machine runs the full stack. Four limits keep it out of production.
Scale - the dataset exceeds memory, the model exceeds GPU capacity; no larger machine exists
Reliability - hardware fails, power drops, updates restart the OS; one machine is one failure domain
Geography - Tokyo to a Los Angeles server is ~100 ms round-trip; distance sets the floor
Elasticity - demand moves 10× in hours; new hardware takes weeks
Whatever capacity is bought is wrong twice: idle at baseline, saturated at peak.

Operating infrastructure requires
These costs are largely fixed: a datacenter serving 100 users costs nearly as much as one serving 10,000.
The rental model
Providers
Provisioning for peak strands capacity
Staffing has a floor
Pooled peaks average out
Scale lowers unit costs
CapEx: locked in upfront
OpEx: scales with usage
| Scenario | CapEx | OpEx |
|---|---|---|
| Bought 50, need 20 | Pay for 50 | Pay for 20 |
| Bought 50, need 100 | Cannot serve | Scale to 100 |
| Project canceled | Stranded asset | Stop paying |
Market position
API stability
2006-03-01Azure and GCP offer the same concepts under different names.
AWS packages each infrastructure capability as a service with its own API:
| Service | Capability |
|---|---|
| EC2 | Virtual machines |
| S3 | Object storage |
| EBS | Block storage (virtual disks) |
| VPC | Virtual networks |
| IAM | Identity and access control |
| RDS | Managed relational databases |
| DynamoDB | Key-value database |
| Lambda | Function execution |
| SQS | Message queues |
About 200 services share three patterns: API access, regional deployment, metered billing.
Creating an EC2 instance:
POST / HTTP/1.1
Host: ec2.us-east-1.amazonaws.com
Authorization: AWS4-HMAC-SHA256 ...
Action=RunInstances
&ImageId=ami-0abcdef1234567890
&InstanceType=t3.micro
&MinCount=1&MaxCount=1
The response contains an instance ID - a handle to a running VM in AWS infrastructure.
Three interfaces construct the same request:
Console - web UI
CLI - command-line tool
SDK - library (Python, Go, etc.)
The Console suits exploration; the CLI and SDK enable automation. Anything the Console does, code can do.
AWS deploys infrastructure in geographic locations called regions.
| Code | Location |
|---|---|
us-east-1 |
N. Virginia |
us-east-2 |
Ohio |
us-west-2 |
Oregon |
eu-west-1 |
Ireland |
eu-central-1 |
Frankfurt |
ap-northeast-1 |
Tokyo |
ap-southeast-1 |
Singapore |
sa-east-1 |
São Paulo |
Scale (2026):
Each region has its own:
Resources created in us-east-1 do not exist in eu-west-1. An outage in one region does not directly affect others.
Region selection determines:
Each region contains multiple Availability Zones (AZs) - physically separate datacenter facilities.
us-east-1 contains six AZs:
us-east-1a through us-east-1f
Physical characteristics:
Interconnected:
Failure isolation
us-east-1a loses power, us-east-1b through us-east-1f continue operatingAZ names are per-account:
us-east-1a may map to a different physical facility than another account’suse1-az1, use1-az2, etc.Each service exposes an endpoint per region.
{service}.{region}.amazonaws.com
| Service | Region | Endpoint |
|---|---|---|
| EC2 | N. Virginia | ec2.us-east-1.amazonaws.com |
| EC2 | Ireland | ec2.eu-west-1.amazonaws.com |
| S3 | N. Virginia | s3.us-east-1.amazonaws.com |
| IAM | (global) | iam.amazonaws.com |
A request is processed in the region its endpoint names, and resources are created there. There is no account-wide region setting: a client configured for a different region addresses a different, independent deployment. Global services (IAM) are the exception, with a single endpoint and no region component.
Global - exist once, no region
Regional - one region, span its AZs
Per-AZ - hardware in one facility
The attachment constraint
us-east-1a cannot attach to an instance in us-east-1b
An API call describes desired state; AWS materializes it on physical hardware it selects.
Specified in the request
Decided by AWS
RunInstances → VM running on some server
CreateBucket → storage on some drives
CreateDBInstance → database on some hardware
Latency
Jurisdiction
eu-west-1 physically resides in Ireland, subject to EU lawFailure
The abstraction hides operational complexity, not physical reality.
Orders of magnitude
| Operation | Latency |
|---|---|
| Memory access | 0.0001 ms |
| SSD random read | 0.1 ms |
| Same-AZ network | <1 ms |
| Cross-AZ network | 1-2 ms |
| Cross-region | 50-200 ms |
Local operations measure in nanoseconds to microseconds; network operations in milliseconds - a factor of 10⁴ to 10⁶.
A web request making 10 database queries
The algorithm is unchanged; the environment multiplies its cost.
Design responses:
On a single machine, programs work or fail. Distributed systems partially work.
Local failure model
Distributed failure model
Failure is partial and subtle: the system keeps running, incorrectly.
Example: uploading a file
Required patterns
At scale these are continuous operation, not edge cases.

An application calls AWS services through their APIs. Services make the same calls to each other - an EC2 instance reading from S3, a Lambda function writing to DynamoDB. Every call, from either source, is authenticated and authorized through IAM.
EC2 (Elastic Compute Cloud) provides virtual machines.
A physical server in an AWS datacenter runs a hypervisor. The hypervisor partitions hardware resources - CPU cores, memory, network bandwidth - and presents them to multiple virtual machines as if each had dedicated hardware.
What the instance receives:
From inside the VM, this looks like a physical machine. The OS sees CPUs, RAM, disks, network interfaces.
What stays with AWS:
The request specifies resources; AWS selects the physical server that provides them.

Multiple EC2 instances share a physical server. The hypervisor enforces isolation - one instance cannot access another’s memory or observe its network traffic. From each instance’s perspective, it has dedicated hardware.
EC2 offers many instance types - fixed allocations of CPU, memory, storage, and network.
Naming convention: {family}{generation}.{size}
| Type | vCPUs | Memory | Network | Use Case |
|---|---|---|---|---|
| t3.micro | 2 | 1 GB | Low | Development, light workloads |
| t3.large | 2 | 8 GB | Low-Mod | Small applications |
| m5.large | 2 | 8 GB | Moderate | Balanced workloads |
| m5.4xlarge | 16 | 64 GB | High | Larger applications |
| c5.4xlarge | 16 | 32 GB | High | Compute-intensive |
| r5.4xlarge | 16 | 128 GB | High | Memory-intensive |
General purpose (t3, m5):
Balanced CPU-to-memory ratio. Suitable for most workloads without extreme requirements in either dimension.
Compute optimized (c5):
High CPU-to-memory ratio. For CPU-bound workloads: batch processing, scientific modeling, video encoding.
c5.4xlarge: 16 vCPUs, 32 GB memory (2:1 ratio)
Memory optimized (r5, x1):
High memory-to-CPU ratio. For workloads that keep large datasets in memory: in-memory databases, caching, analytics.
r5.4xlarge: 16 vCPUs, 128 GB memory (1:8 ratio)
GPU instances (p3, g4):
Include NVIDIA GPUs. For ML training, inference, graphics rendering.
p3.2xlarge: 8 vCPUs, 61 GB, 1× V100 GPU
Storage optimized (i3, d2):
High sequential I/O. For data warehousing, distributed filesystems.
The t3 family uses a CPU credit model.
How it works:
| Type | Baseline | Credits/hour |
|---|---|---|
| t3.micro | 10% | 12 |
| t3.small | 20% | 24 |
| t3.medium | 20% | 24 |
| t3.large | 30% | 36 |
t3.micro can burst to 100% CPU, but sustained usage above 10% depletes credits.
Implications:
Good for:
Not good for:
For sustained workloads, m5 or c5 provide consistent performance without the credit system.
Launching an EC2 instance combines named configuration components:
| Component | Determines | After launch |
|---|---|---|
| AMI | Operating system, pre-installed software | Fixed |
| Instance type | CPU, memory, network capacity | Change while stopped |
| Key pair | SSH authentication | Fixed |
| Security group | Allowed inbound/outbound traffic | Modifiable |
| Subnet | VPC, Availability Zone, IP range | Fixed |
| IAM role | Permissions for AWS API calls | Changeable |
| EBS volumes | Persistent storage | Attach or detach |
AMI (Amazon Machine Image):
Template containing OS and software. AWS provides Amazon Linux, Ubuntu, Windows. AMI IDs are region-specific - the same Ubuntu version has different IDs in different regions.
AWS generates unique identifiers for resources. The prefix indicates the resource type.
AWS-generated IDs
| Prefix | Resource Type |
|---|---|
i- |
EC2 instance |
vol- |
EBS volume |
sg- |
Security group |
vpc- |
VPC |
subnet- |
Subnet |
ami- |
Machine image |
snap- |
EBS snapshot |
Example: i-0abcd1234efgh5678
IDs are immutable - an instance keeps its ID through stop/start cycles - and region-scoped.
User-defined names
Some resources take user-chosen names:
my-company-data-2025)EC2-S3-Reader)Tags never affect behavior - they exist for organization, billing attribution, and automation (e.g., “terminate all instances tagged Environment=dev”).
A security group is a stateful firewall applied to instances.
Inbound rules specify what traffic can reach the instance:
Type Port Source
─────────────────────────────
SSH 22 0.0.0.0/0
HTTP 80 0.0.0.0/0
HTTPS 443 0.0.0.0/0
PostgreSQL 5432 10.0.0.0/16
Custom 8080 sg-0abc1234
Each rule: protocol, port range, source (CIDR or security group).
Default inbound: deny all
Outbound rules specify what traffic the instance can send:
Type Port Destination
─────────────────────────────────
All All 0.0.0.0/0
Default outbound: allow all
Stateful behavior:

Rules are evaluated per-packet. Traffic no rule allows is dropped (default deny). Multiple security groups can attach to one instance - the rules combine as a union.
An EC2 instance moves through states:

| State | Instance | Root volume |
|---|---|---|
| pending | Starting | - |
| running | On a physical host | Attached |
| stopped | Definition kept, host released | Persists |
| terminated | Gone | Deleted by default |
Stop preserves the instance: EBS volumes remain, the private IP is kept, and a later start may land on a different physical host.
Terminate destroys the instance permanently. There is no recovery.
Compute bills only while running; storage bills as long as volumes exist.
On-Demand
Reserved / Savings Plans
| Commitment | Typical Discount |
|---|---|
| 1 year | 30-40% |
| 3 year | 60-72% |
Spot Instances
Spot for ML training
EBS (Elastic Block Store) provides persistent storage for EC2 instances.
Characteristics:
Volume types:
| Type | Performance | Use Case |
|---|---|---|
| gp3 | Balanced SSD | General purpose |
| io2 | High IOPS SSD | Databases |
| st1 | Throughput HDD | Big data |
| sc1 | Cold HDD | Infrequent access |
The AZ constraint:
Volumes attach only within their AZ - the data lives on drives in that facility. A volume in us-east-1a attaches only to instances in us-east-1a.
Snapshots:
Point-in-time backup stored in S3 (regionally). New volumes can be created from a snapshot in any AZ within the region.

By default, root volumes are deleted on termination. Additional volumes persist unless explicitly deleted. Data survives instance replacement.
To reach an EC2 instance:
A network path must exist:
Authentication must succeed:
The username depends on the AMI: ec2-user (Amazon Linux), ubuntu (Ubuntu), Administrator (Windows).
http://169.254.169.254/latest/meta-data/
This link-local address is routed to the instance metadata service, accessible only from within the instance; requests require an IMDSv2 token.
Available information:
| Path | Returns |
|---|---|
/instance-id |
i-0123456789abcdef0 |
/instance-type |
t3.micro |
/ami-id |
ami-0abcdef1234567890 |
/local-ipv4 |
172.31.16.42 |
/public-ipv4 |
54.xxx.xxx.xxx |
/placement/availability-zone |
us-east-1a |
/iam/security-credentials/{role} |
Temporary credentials JSON |
The SDKs use this endpoint to obtain IAM role credentials automatically when running on EC2 - no access keys in code.
Code on an EC2 instance calls other AWS services:
Each of these is an API call. S3 receives an HTTPS request and nothing more; before acting, it must establish who sent the request and whether that identity is permitted to act.
The request arrives at S3:
PUT /my-bucket/results.json HTTP/1.1
Host: s3.us-east-1.amazonaws.com
Authorization: ???
Content-Type: application/json
{"accuracy": 0.94, ...}
S3 must establish:
Without proof of both, S3 rejects the request.
IAM provides both.
Authentication: who sent this request?
Authorization: may this principal perform this action?
Every AWS API call - from any source - goes through this evaluation. No exceptions.
A principal is an identity that can make AWS API requests.
IAM User - a human operator
IAM Role - an assumable identity
AWS Service - services act as principals too
ec2.amazonaws.com can appear as a principal, e.g. when EC2 assumes a role on an instance’s behalfRoot account - full access to everything
Every AWS resource and principal has an Amazon Resource Name (ARN):
arn:aws:service:region:account:resource
| Component | Example | Notes |
|---|---|---|
| Partition | aws |
Usually aws; aws-cn for China |
| Service | iam, s3, ec2 |
The AWS service |
| Region | us-east-1 |
Empty for global services (IAM) |
| Account | 123456789012 |
12-digit AWS account ID |
| Resource | user/alice, role/MyRole |
Service-specific format |
Examples:
arn:aws:iam::123456789012:user/alice
arn:aws:iam::123456789012:role/EC2-S3-Reader
arn:aws:s3:::my-bucket
arn:aws:s3:::my-bucket/data/*
arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123
ARNs uniquely identify resources across all of AWS. Policies reference ARNs to specify who can do what to which resources.
API requests must be signed with credentials. AWS verifies the signature to authenticate the caller.
Long-term credentials:
~/.aws/credentials or environment variablesRisk: a leaked key grants access until someone notices and revokes it.
Short-term credentials:
Benefit: a leaked credential expires on its own.

The SDK signs each request with the secret key and sends only the signature in the Authorization header. AWS verifies by recomputing the signature with its copy of the same secret. The secret itself is never transmitted.
Permissions are defined in policy documents - JSON that specifies what is allowed or denied.
| Field | Purpose | Values |
|---|---|---|
Version |
Policy language version | Always "2012-10-17" |
Statement |
Array of permission rules | One or more statements |
Effect |
Allow or deny | "Allow" or "Deny" |
Action |
API operations | "s3:GetObject", "ec2:*", etc. |
Resource |
What the action applies to | ARN or ARN pattern |
Actions and resources take patterns:
s3:GetObject # One operation
s3:* # All S3 actions
ec2:Describe* # All Describe actions
* # Everything (dangerous)
arn:aws:s3:::my-bucket/data/* # One prefix
arn:aws:s3:::my-bucket/* # All objects
arn:aws:s3:::* # All buckets
* # Everything
Broad patterns are rarely appropriate; the widest ones are dangerous.
Different actions need different resources:
s3:ListBucket authorizes against the bucket ARN; object operations authorize against /* - a distinction developed with S3.
Sid (statement ID) is optional - useful for documentation and debugging.

| Rule | Meaning |
|---|---|
| Default deny | An action no policy mentions is denied |
| Explicit deny wins | A "Deny" statement overrides any "Allow" |
| Explicit allow grants | An "Allow" statement permits the action, unless denied |
This allows all S3 actions except DeleteBucket. The deny wins.
Least privilege follows directly: start with nothing, add only what is needed.

Identity-based: “this user/role can do X to Y”
Resource-based: “this resource allows X from Y” (includes a Principal field)
Both are evaluated. For same-account access, either can grant. Identity-based policies are the more common form.
Back to EC2 accessing S3. One approach: create an IAM user, generate access keys, embed them in code.
Problems:
| Issue | Consequence |
|---|---|
| Keys in code | Checked into git, visible in the repository |
| Keys on disk | Anyone with instance access can read them |
| Keys never expire | A leaked key grants indefinite access |
| Keys per application | Managing many keys is error-prone |
| Key rotation | Manual process, often neglected |
This is how credentials get leaked. Public GitHub repositories are scanned constantly for AWS keys.
A role is an IAM identity that:
Trust policy - who can assume this role:
This says: the EC2 service can assume this role.
When an entity assumes a role, AWS STS (Security Token Service) issues temporary credentials:

Credentials expire (default 1 hour, configurable). After expiry, the role is assumed again for new ones.
EC2 instances assume roles through instance profiles.
Instance profile = container that holds an IAM role
When an instance launches with an instance profile:
http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
The response is the expiring-credential JSON: AccessKeyId, SecretAccessKey, Token, Expiration.

1. Create an IAM role with a trust policy:
2. Attach a permissions policy to the role:
3. Create an instance profile and attach the role:
4. Launch EC2 with the instance profile:
5. Code on the instance:
| With access keys | With IAM roles |
|---|---|
| Keys in code or config files | No keys to leak |
| Keys valid indefinitely | Credentials expire automatically |
| Manual rotation required | Automatic rotation |
| Keys can be copied anywhere | Credentials tied to the instance |
| Compromised key: long-term access | Compromised instance: temporary access |
Instance compromise is still serious - the attacker gets whatever permissions the role has. But no permanent credentials leave with them; access ends with the instance.
Least privilege: give roles only the permissions they need. s3:GetObject on one bucket, not s3:* on *.
EBS provides block storage - raw disk that an OS formats and manages as a filesystem. S3 provides object storage - a different abstraction entirely.
Block storage (EBS):
The model local code assumes.
Object storage (S3):
A different model optimized for different access patterns.
S3 is not a mounted filesystem; it is a service called over HTTP.
S3 organizes data into buckets containing objects.
Bucket: a container with a globally unique name
training-data-2025 - once claimed, no other account can use the nameObject: a key-value pair
models/v1/weights.pt)Nothing else exists - no volumes, no directories, no hierarchy. Two coordinates, bucket and key, address every byte in S3.

The AWS Console and CLI show a folder-like view. This is a UI convenience, not reality.
What “listing a directory” actually does:
This calls ListObjectsV2 with Prefix="data/train/" and Delimiter="/". S3 returns objects whose keys start with that prefix. The slash delimiter groups results to simulate folders.
No directory was traversed. A string filter was applied.
The flat namespace and immutable objects have consequences:
No rename
Renaming old-name.csv to new-name.csv requires:
For a 5 GB file, that copies 5 GB (within S3, but still a copy).
Filesystems rename by changing a pointer. S3 has no pointers.
No move
Same as rename - copy, then delete.
No append
Adding 100 bytes to a 5 GB file requires:
Or: store separate objects and concatenate at read time.
Filesystems append by extending an allocation. S3 objects are immutable blobs.
No partial update
Changing byte 1000 requires replacing the entire object.
Once written, an object cannot be modified - only replaced entirely.
Design implications:
Immutability is a model to design for, not a defect to work around. Many distributed systems work well with immutable data.
S3 is an HTTP API. Every operation is an HTTP request.
| Operation | HTTP Method | What It Does |
|---|---|---|
PutObject |
PUT | Create/replace object |
GetObject |
GET | Retrieve object (or byte range) |
DeleteObject |
DELETE | Remove object |
HeadObject |
HEAD | Get metadata without body |
ListObjectsV2 |
GET on bucket | List keys matching prefix |
PUT /my-bucket/data/file.csv HTTP/1.1
Host: s3.us-east-1.amazonaws.com
Content-Length: 1048576
Authorization: AWS4-HMAC-SHA256 ...
<file bytes>
The CLI and SDK construct these requests. The operation set follows from HTTP, which has no append or rename either.
# Create bucket (name must be globally unique)
aws s3 mb s3://my-bucket-unique-name-12345
# Upload file
aws s3 cp ./local-file.csv s3://my-bucket/data/file.csv
# Download file
aws s3 cp s3://my-bucket/data/file.csv ./local-file.csv
# List objects (prefix filter, not directory listing)
aws s3 ls s3://my-bucket/data/
# Sync local directory to S3 (uploads new/changed files)
aws s3 sync ./local-dir/ s3://my-bucket/data/
# Delete object
aws s3 rm s3://my-bucket/data/file.csv
# Delete all objects with prefix
aws s3 rm s3://my-bucket/data/ --recursiveaws s3 commands are high-level conveniences. aws s3api exposes the raw API operations.
import boto3
import json
s3 = boto3.client('s3')
# Upload object
s3.put_object(
Bucket='my-bucket',
Key='results/experiment-001.json',
Body=json.dumps({'accuracy': 0.94, 'loss': 0.23}),
ContentType='application/json'
)
# Download object
response = s3.get_object(Bucket='my-bucket', Key='results/experiment-001.json')
data = json.loads(response['Body'].read())
# List objects with prefix
response = s3.list_objects_v2(Bucket='my-bucket', Prefix='results/')
for obj in response.get('Contents', []):
print(f"{obj['Key']}: {obj['Size']} bytes")A common permissions mistake: the policy grants object access and nothing else.
This allows downloading objects. But listing the bucket:
ListBucket is a bucket operation, not an object operation. It needs the bucket ARN:
| ARN | Applies To |
|---|---|
arn:aws:s3:::my-bucket |
ListBucket, bucket operations |
arn:aws:s3:::my-bucket/* |
GetObject, PutObject, object operations |
arn:aws:s3:::my-bucket/data/* |
Object operations under the prefix data/ |
Getting this wrong is the most common S3 permissions error.
Two different guarantees:
Durability: probability data survives
S3 Standard: 99.999999999% (11 nines)
S3 stores copies across multiple facilities in the region, designed to sustain simultaneous loss of two facilities.
10 million objects: expect to lose 1 every 10,000 years.
A successful PUT means the data is safe.
Availability: probability of successful access
S3 Standard: designed for 99.99%
About 53 minutes/year of potential unavailability.
Availability failures are transient - a retry succeeds. Data is not lost, only temporarily unreachable.
A GET may fail occasionally; the data is still there.
High durability does not guarantee high availability. A design that retries handles the difference; a design that conflates them loses either data or uptime.
S3 serves as durable storage accessible from any compute resource:

Training runs, writes the model to S3, terminates. Serving instances start and read the model from S3. Lambda processes uploads. All access the same data. S3 persists regardless of which compute resources exist.
An EC2 instance needs a network: an IP address, routing, connectivity to other instances and the internet.
AWS does not place instances on a shared public network. Every instance lands in a VPC - a Virtual Private Cloud belonging to one account.
VPC properties:
Other accounts cannot see into a VPC. Traffic between VPCs is isolated by default.
Default VPC:
Every region has a default VPC created automatically. An instance launched without networking configuration goes there.
The default VPC suffices for simple deployments. Production environments typically use custom VPCs with deliberate network design.
A VPC has a CIDR block - the range of private IP addresses available within it.
10.0.0.0/16
This notation specifies a range:
10.0.0.0 - starting address/16 - first 16 bits fixed, remaining 16 bits vary10.0.0.0/16 includes 10.0.0.0 through 10.0.255.255 - 65,536 addresses.
| CIDR | Range | Addresses |
|---|---|---|
10.0.0.0/16 |
10.0.0.0 - 10.0.255.255 | 65,536 |
10.0.0.0/24 |
10.0.0.0 - 10.0.0.255 | 256 |
10.0.1.0/24 |
10.0.1.0 - 10.0.1.255 | 256 |
These are private addresses - not routable on the public internet. Within the VPC, instances communicate with them directly.
Reaching the internet requires either:
A VPC spans an entire region. Subnets divide it into segments, each in a specific AZ.

Each subnet:
| Property | Implication |
|---|---|
| Exists in one AZ | Instances in this subnet run in this AZ |
| Has a CIDR block | Subset of the VPC’s range (e.g., 10.0.1.0/24 within 10.0.0.0/16) |
| Has a route table | Determines where traffic goes |
| Is public or private | Based on routing, not a flag |
The AZ constraint, mechanized:
Launching an instance means specifying a subnet. The subnet fixes the AZ. This is why EBS volumes must match - volume and instance must share an AZ, and subnet selection is where the instance’s AZ gets decided.
“Public” and “private” describe routing behavior, not a setting.
Public subnet:
Route table includes:
Destination Target
10.0.0.0/16 local
0.0.0.0/0 igw-xxx ← Internet Gateway
Traffic to addresses outside the VPC routes to the Internet Gateway. Instances with public IPs can receive inbound traffic from the internet.
Private subnet:
Route table includes:
Destination Target
10.0.0.0/16 local
No route to the internet. Traffic to external addresses has nowhere to go.
Instances here cannot be reached from the internet - no inbound path exists.
A subnet becomes public by having a route to an Internet Gateway. Remove that route and it becomes private.
An Internet Gateway (IGW) connects the VPC to the public internet.

The IGW is AWS-managed, with no capacity to provision. It attaches to the VPC and handles the translation between public and private IPs.
An instance in a public subnet still needs a public IP to be reachable from the internet.
Auto-assigned public IP:
Elastic IP:
Needed when the address must stay fixed: DNS records, firewall whitelists.
The mapping:
An instance has private IP 10.0.1.5 and public IP 54.x.x.x. The IGW translates - outbound traffic appears from 54.x.x.x, inbound traffic to 54.x.x.x routes to 10.0.1.5.
Instances in private subnets often need outbound internet access - downloading packages, calling external APIs - without being reachable from the internet.
NAT Gateway provides exactly this:

Private subnet route table: 0.0.0.0/0 → nat-xxx. Outbound traffic goes to the NAT Gateway (in a public subnet), which forwards through the IGW. Inbound from the internet still has no path to private instances.
NAT Gateway is a managed service with meaningful cost:
| Component | Price |
|---|---|
| Hourly charge | ~$0.045/hour (~$32/month) |
| Data processing | $0.045/GB |
A NAT Gateway running continuously with moderate traffic can cost $50-100/month. Alternatives for development:
Production typically uses NAT Gateway for reliability. Development often skips it or uses alternatives.
A request from the internet reaching an instance traverses multiple layers:

For traffic to reach an instance:
Security groups can reference other security groups, not just IP ranges:
Type Port Source
────────────────────────────
HTTP 80 sg-loadbalancer
PostgreSQL 5432 sg-appserver
“Allow traffic from instances in security group sg-loadbalancer” - even as their IPs change.

App servers accept HTTP only from the load balancer, regardless of IP changes. The database accepts connections only from app servers.
S3, DynamoDB, and other AWS services live outside the VPC. By default, traffic to them goes over the public internet path (through the IGW or NAT).
VPC Endpoints provide private connectivity:
Gateway Endpoints (S3, DynamoDB):
Destination Target
pl-xxx (S3 prefix) vpce-xxx
Interface Endpoints (most other services):
For private subnets that need S3 access, a Gateway Endpoint avoids NAT Gateway costs and keeps traffic private.
Application Load Balancer (ALB):

Users hit one DNS name. The ALB distributes requests. An instance that fails health checks stops receiving traffic.

Public subnet - internet-facing components
Private subnet - internal components
EC2 provides virtual machines. S3 provides object storage. Building an application takes more.
Computation beyond VMs:
Persistence beyond objects:
For each service: what it provides, its constraints, and the selection rule.
Lambda provides functions as a service. EC2 requires provisioned instances that run and bill continuously; Lambda takes code and runs it when triggered.
The model:
No instances to manage. No servers to patch. Code runs, then nothing exists until the next trigger.
| Trigger | Example Use |
|---|---|
| API Gateway | HTTP endpoint calls Lambda |
| S3 | Object uploaded, process it |
| Schedule | Run every hour (cron-like) |
| SQS | Message arrives, process it |
| DynamoDB | Record changes, react |
Common patterns:
Lambda suits workloads that are event-driven, short-lived, and stateless between invocations.
Lambda trades flexibility for simplicity. Constraints to know:
| Constraint | Limit |
|---|---|
| Execution timeout | 15 minutes maximum |
| Memory | 128 MB to 10 GB |
| Deployment package | 250 MB (unzipped) |
| Concurrency | 1000 default (can request increase) |
| Stateless | No persistent local storage between invocations |
Cold starts: the first invocation (or the first after an idle period) takes longer - Lambda must initialize the code. Subsequent invocations reuse the warm environment. Latency-sensitive applications notice this delay.
Lambda is not an EC2 replacement. Long-running processes, persistent connections, or large memory requirements need EC2.
Pay for what executes:
| Component | Price |
|---|---|
| Requests | $0.20 per million |
| Duration | $0.0000166667 per GB-second |
A function using 512 MB running for 200 ms:
1 million invocations at this configuration ≈ $2.
Contrast with EC2: a t3.micro running continuously costs ~$7.59/month whether or not it is doing work. Lambda costs nothing when idle.
SQS (Simple Queue Service) provides message queues - a way for one component to send work to another without a direct connection.
A queue holds messages between components.
# Producer: send message
sqs.send_message(
QueueUrl='https://sqs.../my-queue',
MessageBody='{"task": "process", "id": 123}'
)
# Consumer: receive and process
messages = sqs.receive_message(QueueUrl='...')
for msg in messages.get('Messages', []):
process(msg['Body'])
sqs.delete_message(...) # Acknowledge
Decoupling in time
Decoupling in scale
Decoupling in deployment
Failure isolation
SNS (Simple Notification Service) provides publish/subscribe delivery.
The concept:
A topic is a channel. Publishers send messages to topics; the topic delivers each message to its subscribers.
Subscriber types:

Example: an order-placed event published to an orders topic invokes an inventory Lambda, sends a confirmation email, and enqueues the order for the shipping system. One event, multiple reactions.
| SQS | SNS | |
|---|---|---|
| Model | Queue (one consumer per message) | Pub/sub (many subscribers per message) |
| Delivery | Consumer pulls | SNS pushes to subscribers |
| Persistence | Messages wait in the queue | Delivery attempted immediately |
| Use case | Work distribution | Event notification |
Often used together: SNS publishes to multiple SQS queues, each processed by a different consumer application.
RDS runs PostgreSQL, MySQL, or other relational databases on EC2 instances that AWS operates.
What RDS is:
Constraints to know:
| Instance | Max Connections | Cost/month |
|---|---|---|
| db.t3.micro | ~85 | ~$12 |
| db.t3.medium | ~170 | ~$50 |
| db.r5.large | ~1,700 | ~$175 |
DynamoDB provides a managed key-value store - items indexed by primary key. No SQL, no joins, no instance to manage.
Access patterns:
Constraints to know:
Pricing (on-demand):
| Operation | Cost |
|---|---|
| Write (1 KB) | $0.625 per million |
| Read (4 KB) | $0.125 per million |
No idle cost: zero requests, zero charges.
RDS for:
The relational model fits when the queries keep changing.
DynamoDB for:
The key-value model fits when the access patterns are fixed in advance.
AWS bills based on resource usage. Different resources meter in fundamentally different ways:
| Metering Model | How It Works | Examples |
|---|---|---|
| Time-based | Charge per unit time the resource exists/runs | EC2 instances, RDS instances, NAT Gateway |
| Capacity-based | Charge per unit capacity provisioned | EBS volumes, provisioned IOPS |
| Usage-based | Charge per unit actually consumed | S3 storage, S3 requests, Lambda invocations |
| Movement-based | Charge per unit data transferred | Data transfer out, cross-region transfer |
A single deployment involves multiple metering models simultaneously. An EC2 instance incurs time-based charges (compute), capacity-based charges (EBS), and potentially movement-based charges (data transfer).
EC2 instances charge per-second while in the running state (60-second minimum).
| Instance Type | Hourly | Monthly (continuous) |
|---|---|---|
| t3.micro | $0.0104 | $7.59 |
| t3.small | $0.0208 | $15.18 |
| t3.medium | $0.0416 | $30.37 |
| m5.large | $0.096 | $70.08 |
| m5.xlarge | $0.192 | $140.16 |
On-Demand pricing, us-east-1, Linux. Other regions ±10-20%.
Instance state determines billing:
| State | Compute Charge | EBS Charge |
|---|---|---|
| running | Yes | Yes |
| stopped | No | Yes |
| terminated | No | No (volume deleted) |
Stopping an instance stops compute charges. The EBS volume still exists and still bills. Terminating ends all charges (root volume deleted by default).
EBS and S3 use different metering models:
EBS: Capacity-based
Charges for provisioned size, not used space.
| Volume Type | Per GB-month |
|---|---|
| gp3 | $0.08 |
| gp2 | $0.10 |
| io2 | $0.125 + IOPS |
A 100 GB gp3 volume: $8/month
The charge is the same whether the volume holds 1 GB or 100 GB - the fee buys reserved capacity.
S3: Usage-based
Charges for actual storage plus operations.
| Component | Price |
|---|---|
| Storage (Standard) | $0.023/GB-month |
| PUT/POST/LIST | $0.005/1,000 |
| GET/SELECT | $0.0004/1,000 |
100 GB stored: $2.30/month
S3 charges grow with the data actually stored. An empty bucket costs nothing.
Data transfer charges are based on movement, independent of the resources involved:
| Transfer | Price |
|---|---|
| Inbound from internet | Free |
| Outbound to internet | $0.09/GB |
| Between regions | $0.02/GB |
| Between AZs (same region) | $0.01/GB each direction |
| Within same AZ | Free |
| To S3/DynamoDB (same region) | Free |

Architectures that move large amounts of data across regions or to the internet accumulate transfer costs.
New AWS accounts receive $100 in credits (up to $200 with onboarding activities), not free monthly usage:
What $100 of credits covers:
| Usage | Rate | Credit Lifetime |
|---|---|---|
| 1 × t3.micro, continuous | $7.59/month | ~13 months |
| 100 GB gp3 volume | $8.00/month | ~12 months |
| NAT Gateway, idle | $32/month | ~3 months |
| 1 × p3.2xlarge | $3.06/hour | ~33 hours |
A GPU instance consumes in a day what a t3.micro consumes in months.
The billing model is pay-as-you-go: charges accumulate automatically, and nothing interrupts them.
Visibility mechanisms
Protection measures
A laptop runs ML training as a single process. Data, compute, and storage are all local.
Local training
Everything shares memory, disk, and failure fate. If the process crashes, everything stops together. If the disk fails, data and model are both gone.
One machine, one failure domain.
Distributed training
Components are networked. S3 can fail while EC2 runs. EC2 can terminate while S3 persists. The network can drop between them.
Data outlives compute. Compute is ephemeral. The network connects - and separates - them.
Every arrow in an architecture diagram is a network hop.
Local operations
| Operation | Time |
|---|---|
| Read 1MB from SSD | 1 ms |
| Load pandas DataFrame | 10 ms |
| PyTorch forward pass | 5 ms |
| Write checkpoint to disk | 2 ms |
Total per batch: ~20 ms. Predictable. Consistent.
With S3 in the loop
| Operation | Time |
|---|---|
| Fetch 1MB from S3 | 20-50 ms |
| Load pandas DataFrame | 10 ms |
| PyTorch forward pass | 5 ms |
| Write checkpoint to S3 | 30-100 ms |
Total per batch: 65-165 ms. Variable. Depends on the network.
Checkpointing every batch adds 3-8× overhead. Checkpointing every 100 batches does not.
Design for latency: batch S3 operations, prefetch data, checkpoint strategically.
Distributed systems fail partially.
Independent failure domains
S3 keeps serving while an EC2 instance is down; EC2 keeps computing while an S3 request fails. The boundaries are the application’s to handle.
Failure scenarios
| Event | Data | Model | Training |
|---|---|---|---|
| EC2 terminates | Safe (S3) | Lost (if not saved) | Lost |
| S3 request fails | Retry works | Retry works | Continues |
| OOM on EC2 | Safe (S3) | Lost (if not saved) | Lost |
| Network partition | Safe (S3) | Stuck | Stuck |
The pattern: S3 is durable, EC2 is ephemeral. Save state to S3 frequently enough that losing EC2 is recoverable.
S3 is the durable integration point. EC2 is stateless compute.

Data flow pattern
Why this works
Recovery
No state is lost because state lives in S3, not EC2.
EC2 instances assume IAM roles. No access keys in code.
GetObject/PutObject on the training bucket’s objects, ListBucket on the bucketWithout checkpoints
With checkpoints every epoch
Checkpoint frequency trade-off
| Frequency | S3 Writes | Recovery Loss | Overhead |
|---|---|---|---|
| Every batch | 10,000/epoch | Seconds | High (latency) |
| Every epoch | 100 total | Minutes | Low |
| Every 10 epochs | 10 total | ~1 hour | Minimal |
Choose based on:
For spot instances: handle the interruption signal - the 2-minute warning is enough to save a checkpoint.
Operations should be safe to retry. After a network failure, the caller often cannot tell whether the operation succeeded.
The problem
For S3 put_object: yes, safe to retry. The same key overwrites with the same content. No harm.
Design for idempotency
Safe to retry:
Not safe to retry (without care):
Pattern: use deterministic keys
Three things cost money: compute time, storage, and data transfer.
Compute (EC2)
Design response: terminate when done. Use spot instances for fault-tolerant work. Leave nothing running overnight.
Storage (S3)
Design response: delete intermediate files. Use lifecycle policies for old checkpoints.
Data transfer
Design response: keep data and compute in the same region. Avoid repeatedly downloading large datasets from S3 to a local machine.
Example costs for a training job
| Resource | Usage | Cost |
|---|---|---|
| EC2 p3.2xlarge | 8 hours | $24.48 |
| S3 storage | 50 GB/month | $1.15 |
| Data transfer | Within region | $0 |
Compute dominates. Optimize instance usage first.
Resources cost money from the moment they exist, and nothing stops the meter automatically.
Real student mistakes
| Mistake | Cost |
|---|---|
| p3.2xlarge left running over a weekend | $220 |
| Forgotten 500GB S3 bucket | $12/month, indefinitely |
| Auto-scaling launched 20 instances | $50/hour |
| Cross-region replication enabled | $45 transfer |
S3 has no directories; the key scheme carries all the structure. Automation - resuming a run, comparing experiments, deleting old checkpoints - depends on it.
ml-project-{username}/
├── data/
│ ├── raw/ # Original, immutable
│ │ └── dataset_v1.csv
│ └── processed/ # Transformed, ready for training
│ ├── train.parquet
│ └── test.parquet
├── checkpoints/
│ └── experiment_001/
│ ├── epoch_0010.pt
│ ├── epoch_0020.pt
│ └── epoch_0030.pt
├── models/
│ └── experiment_001/
│ └── final.pt
└── logs/
└── experiment_001/
└── training.log
What each convention buys:
epoch_0010 not epoch_10) sort correctly, so code can find the latest checkpointA training job as cloud operations:
Startup
Training loop
Completion
What can fail and what happens
| Failure | Impact | Recovery |
|---|---|---|
| S3 read fails | Training cannot start | Retry with backoff |
| S3 write fails | Checkpoint lost | Retry; if persistent, alert |
| EC2 terminates | Training stops | New instance + last checkpoint |
| OOM | Process crashes | Reduce batch size, restart |
| Code bug | Process crashes | Fix bug, restart from checkpoint |
Every recovery in the table works only because the design provides:
Two approaches: download to file, or stream into memory.
Download to local file
Use when:
Stream directly into memory
Use when:
Both approaches use the same IAM permissions: s3:GetObject on the object ARN.
Upload from file or from memory buffer.
Upload from file
upload_file switches to multipart upload above 8 MB (the TransferConfig default).
Upload with metadata
Upload from memory
import boto3
from io import BytesIO
import torch
s3 = boto3.client('s3')
# Save model to memory buffer
buffer = BytesIO()
torch.save(model.state_dict(), buffer)
buffer.seek(0) # Rewind to beginning
# Upload buffer contents
s3.put_object(
Bucket='my-bucket',
Key='models/final.pt',
Body=buffer.getvalue()
)Use when:
Requires s3:PutObject on the object ARN.
List operations return metadata, not contents.
List objects with a prefix
Output:
checkpoints/experiment_001/epoch_0010.pt: 45678 bytes
checkpoints/experiment_001/epoch_0020.pt: 45702 bytes
checkpoints/experiment_001/epoch_0030.pt: 45689 bytes
Pagination for many objects
list_objects_v2 returns max 1000 objects. For more:
Find latest checkpoint
Requires s3:ListBucket on the bucket ARN (not object ARN).
S3 operations can fail. Handle transient errors with retries.
Common errors
from botocore.exceptions import ClientError
try:
s3.download_file('bucket', 'key', 'local')
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'NoSuchKey':
# Object doesn't exist
print("File not found in S3")
elif error_code == 'AccessDenied':
# Permission issue
print("Check IAM policy")
elif error_code == '403':
# Often bucket vs object ARN issue
print("Check resource ARN in policy")
else:
raiseRetry with backoff
import time
from botocore.exceptions import ClientError
def download_with_retry(bucket, key, local, max_retries=3):
for attempt in range(max_retries):
try:
s3.download_file(bucket, key, local)
return # Success
except ClientError as e:
error_code = e.response['Error']['Code']
# Don't retry permanent errors
if error_code in ['NoSuchKey', 'AccessDenied']:
raise
# Retry transient errors
if attempt < max_retries - 1:
wait = 2 ** attempt # 1, 2, 4 seconds
time.sleep(wait)
else:
raiseboto3 has built-in retry logic for some errors; explicit handling sets the policy.
Code running on EC2 can query information about itself. IMDSv2 requires a session token.
Get token, then query
import requests
METADATA = 'http://169.254.169.254/latest/meta-data'
# Step 1: Get session token (required for IMDSv2)
token = requests.put(
'http://169.254.169.254/latest/api/token',
headers={'X-aws-ec2-metadata-token-ttl-seconds': '300'},
timeout=1
).text
headers = {'X-aws-ec2-metadata-token': token}
# Step 2: Query with token
instance_id = requests.get(
f'{METADATA}/instance-id', headers=headers, timeout=1
).text
region = requests.get(
f'{METADATA}/placement/region', headers=headers, timeout=1
).text
instance_type = requests.get(
f'{METADATA}/instance-type', headers=headers, timeout=1
).textWhy IMDSv2?
IMDSv1 allowed simple GET requests - vulnerable to SSRF attacks, where malicious input tricks the application into fetching credentials.
IMDSv2 requires:
New instances default to IMDSv2-only. Old code using direct GET requests will fail with 401 Unauthorized.
boto3 handles IMDSv2 automatically when fetching credentials; the token exchange matters only for direct metadata queries.
Metadata endpoint only works from within EC2. Times out elsewhere.
Spot instances can be reclaimed with 2 minutes warning.
Check for interruption notice
import requests
def check_spot_interruption():
"""Returns termination time if spot will be interrupted."""
try:
token = requests.put(
'http://169.254.169.254/latest/api/token',
headers={'X-aws-ec2-metadata-token-ttl-seconds': '60'},
timeout=1
).text
response = requests.get(
'http://169.254.169.254/latest/meta-data/'
'spot/instance-action',
headers={'X-aws-ec2-metadata-token': token},
timeout=1
)
if response.status_code == 200:
data = response.json()
return data.get('time') # Termination time
except requests.exceptions.RequestException:
pass
return NoneReturns None normally. Returns timestamp when termination is imminent.
Graceful training loop
def train_with_interruption_handling(model, data):
for epoch in range(num_epochs):
# Check before each epoch
if check_spot_interruption():
print("Spot interruption! Saving checkpoint...")
save_checkpoint(model, epoch)
return "interrupted"
# Train one epoch
train_epoch(model, data)
# Regular checkpoint
if epoch % checkpoint_frequency == 0:
save_checkpoint(model, epoch)
return "completed"The 2-minute warning is enough time to save a checkpoint.
The first match wins.
Search order
Explicit in code (never do this)
Environment variables
Credentials file (~/.aws/credentials)
Config file (~/.aws/config)
Instance metadata (EC2 role)
Container credentials (ECS/EKS)
Best practice by environment
| Environment | Credential Source |
|---|---|
| Local dev | ~/.aws/credentials file |
| EC2 instance | Instance profile (IAM role) |
| Lambda | Execution role (automatic) |
| CI/CD | Environment variables |
Never in code. Keys in code get committed to git, leaked, compromised.
Verify the active identity
Permission errors follow patterns.
“Access Denied” on S3
botocore.exceptions.ClientError:
An error occurred (AccessDenied) when calling
the GetObject operation: Access Denied
Checklist:
s3:GetObject)The bucket vs object ARN trap
Wrong. GetObject needs object ARN:
“Access Denied” on ListBucket
An error occurred (AccessDenied) when calling
the ListObjectsV2 operation: Access Denied
ListBucket needs bucket ARN:
Not object ARN:
Complete policy for read/write
Different environment, different credentials, different permissions.
Check the active identity
Locally: the IAM user. On EC2: the instance role.
Different identities have different permissions.
Check what region
S3 buckets are regional. EC2 and bucket must agree; a mismatch adds transfer costs and latency.
Common causes
| Symptom | Likely Cause |
|---|---|
| Access Denied | Role missing permission |
| No Credentials | Instance has no role attached |
| Bucket not found | Wrong region configured |
| Timeout | Security group blocks outbound |
Verify instance role
# From the EC2 instance (IMDSv2: token first)
TOKEN=$(curl -s -X PUT http://169.254.169.254/latest/api/token \
-H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Should return role name, e.g.:
# EC2-S3-Access-RoleAn empty response means no role is attached.
Network problems manifest as hangs or timeouts.
S3 operations hang
Possible causes:
nslookup s3.amazonaws.comQuick network test
Set timeouts explicitly
Without explicit timeouts, operations can hang indefinitely.
VPC endpoint for S3
If in a private subnet without NAT:
VPC Endpoint (Gateway type) for S3
├── No NAT gateway needed
├── No internet gateway needed
├── Traffic stays in AWS network
└── Often faster and cheaper
EC2 instances have finite memory. Training can exhaust it.
Monitor memory usage
From command line
Common causes
| Cause | Solution |
|---|---|
| Batch size too large | Reduce batch size |
| Loading full dataset | Use data loader with batching |
| Accumulating history | Clear gradients; store loss scalars, not tensors |
| Memory leak | Check for growing lists/dicts |
Reduce memory usage
If still OOM: use a larger instance type, or redesign to process in smaller chunks.
Putting the patterns together.
import io
import time
import boto3
import pandas as pd
import torch
from botocore.exceptions import ClientError
def load_checkpoint(s3, bucket, prefix):
"""Load latest checkpoint if exists."""
try:
response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix)
if not response.get('Contents'):
return None, 0
latest_key = sorted(response['Contents'], key=lambda x: x['Key'])[-1]['Key']
obj = s3.get_object(Bucket=bucket, Key=latest_key)
checkpoint = torch.load(io.BytesIO(obj['Body'].read()))
epoch = checkpoint['epoch']
print(f"Resumed from {latest_key} (epoch {epoch})")
return checkpoint, epoch + 1 # Saved epoch is complete; resume at the next
except ClientError as e:
if e.response['Error']['Code'] == 'AccessDenied':
raise # Permission error, not a missing checkpoint
return None, 0
def save_checkpoint(s3, bucket, model, epoch):
"""Save checkpoint to S3."""
checkpoint = {'epoch': epoch, 'model_state': model.state_dict()}
buffer = io.BytesIO()
torch.save(checkpoint, buffer)
buffer.seek(0)
key = f"checkpoints/epoch_{epoch:04d}.pt"
s3.put_object(Bucket=bucket, Key=key, Body=buffer.getvalue())
print(f"Saved checkpoint: {key}")
def train():
s3 = boto3.client('s3')
bucket = 'my-training-bucket'
# Load data
obj = s3.get_object(Bucket=bucket, Key='data/train.csv')
data = pd.read_csv(obj['Body'])
# Initialize or resume
model = MyModel()
checkpoint, start_epoch = load_checkpoint(s3, bucket, 'checkpoints/')
if checkpoint:
model.load_state_dict(checkpoint['model_state'])
# Training loop
for epoch in range(start_epoch, 100):
train_epoch(model, data)
if epoch % 10 == 0:
save_checkpoint(s3, bucket, model, epoch)
# Save final model
buffer = io.BytesIO()
torch.save(model.state_dict(), buffer)
s3.put_object(Bucket=bucket, Key='models/final.pt', Body=buffer.getvalue())
if __name__ == '__main__':
train()Loading model from S3, serving predictions.
from flask import Flask, request, jsonify
import boto3
import torch
from io import BytesIO
app = Flask(__name__)
model = None
def load_model():
"""Load model from S3 once at startup."""
s3 = boto3.client('s3')
obj = s3.get_object(
Bucket='my-bucket',
Key='models/final.pt'
)
buffer = BytesIO(obj['Body'].read())
model = MyModel()
model.load_state_dict(torch.load(buffer, map_location='cpu'))
model.eval()
return model
@app.route('/health')
def health():
return jsonify({'status': 'healthy', 'model_loaded': model is not None})
@app.route('/predict', methods=['POST'])
def predict():
if model is None:
return jsonify({'error': 'Model not loaded'}), 503
try:
data = request.json
features = torch.tensor(data['features'])
with torch.no_grad():
output = model(features)
return jsonify({'prediction': output.tolist()})
except KeyError as e:
return jsonify({'error': f'Missing field: {e}'}), 400
except Exception as e:
return jsonify({'error': str(e)}), 500
# Load model at startup
model = load_model()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)