Skip to content

S3: The Object Store That Runs the Internet

  • lesson
  • s3
  • object-storage
  • aws-pricing
  • lifecycle-policies
  • encryption
  • iam
  • incident-response
  • data-architecture ---# S3 — The Object Store That Runs the Internet

Topics: S3, object storage, AWS pricing, lifecycle policies, encryption, IAM, incident response, data architecture Level: L1–L2 (Foundations → Operations) Time: 50–70 minutes Prerequisites: None (AWS account helpful for exercises, not required)


The Mission

Your team lead drops into Slack at 9:14 AM:

"Finance flagged the AWS bill. Our backup bucket is $5,000/month more than last quarter. Nobody changed anything. Can you figure out what happened?"

You open the AWS console. The bucket is acme-prod-backups. It has been running for two years. No new services write to it. No traffic spikes. But the bill says storage costs jumped from $1,200/month to $6,200/month over 90 days.

This is a cost investigation — but to solve it, you need to understand how S3 actually works: the object model, storage classes, versioning, lifecycle policies, and the pricing dimensions that catch people. By the end of this lesson, you will understand all of them, and you will find the $5,000.


Part 1: What S3 Actually Is (and Is Not)

Before you can investigate a cost problem, you need to understand the thing you are investigating.

S3 is a key-value object store. Not a filesystem. There are no directories — the / in logs/2026/03/19/app.log.gz is just a character in a string. There are no inodes, no rename operation, no append. You PUT an object, you GET an object, you DELETE an object. That is the entire model.

Concept What It Is
Bucket A globally unique container. Lives in one region.
Key The full "path" string: backups/daily/2026-03-19.tar.gz
Object The data (up to 5 TB) plus metadata
ETag Hash of the object — MD5 for single-part uploads
Version ID Unique ID per version (when versioning is enabled)
Storage class Where the object physically lives (affects price and access speed)
# Your first investigation command: what is in this bucket?
aws s3 ls s3://acme-prod-backups/ --recursive --human-readable --summarize

That --summarize flag at the end gives you total object count and total size. On a bucket with millions of objects, this command itself can take minutes and cost money (more on that later).

Name Origin: S3 stands for "Simple Storage Service." It launched on March 14, 2006 — Pi Day — as one of the first three AWS services alongside SQS and EC2. The original price was $0.15/GB/month. As of 2025, Standard storage is $0.023/GB/month — an 85% price drop over 19 years. S3 stored over 350 trillion objects by 2024.

The flat namespace trap

New engineers see logs/2026/03/app.log and think "that is a file in a directory." It is not. There is no logs/ directory. The entire string is the key. This matters because:

  • You cannot atomically rename a "directory" — you must copy every object to a new prefix, then delete the originals
  • LIST operations return keys lexicographically, not by directory tree
  • Permissions apply to key prefixes, not directories

Mental Model: Think of S3 like a massive hash table. The bucket is the table, the key is the lookup string, and the object is the value. There is no hierarchy — just keys that happen to contain slashes.


Part 2: The Consistency Model (A 14-Year Bug Factory)

Before we dig into the bill, a piece of history that explains why so many S3 tutorials have paranoid retry logic.

From 2006 to December 2020, S3 had eventual consistency for overwrite PUTs and DELETEs. What that meant in practice:

1. PUT object (version 2) to overwrite version 1
2. GET object immediately
3. Sometimes get version 1 back (stale read)

This was not a bug. It was the documented behavior. And it caused real bugs in production systems for 14 years:

  • Backup systems that verified uploads by reading them back would occasionally report success on stale data
  • Configuration management tools that wrote new config, then read it to confirm, would sometimes deploy old config
  • Data pipelines that wrote a "done" marker file, then checked for it, would sometimes miss it and reprocess the entire batch

In December 2020, AWS silently upgraded S3 to strong read-after-write consistency for all operations — PUTs, DELETEs, and LISTs — at no extra cost. Every read now returns the most recent write. No configuration needed. Retroactively, 14 years of workaround code became unnecessary.

Trivia: The consistency upgrade was so seamless that many teams never noticed. Their retry-and-verify wrappers kept working — they just never triggered anymore. Some codebases still have comments like # S3 is eventually consistent, retry 3x next to code that has not retried in years.

Why this matters for your investigation

Strong consistency means when you query the bucket right now, you see the current truth. No stale reads, no phantom objects. The numbers you get are the numbers that exist.


Part 3: The S3 Pricing Model (Where the $5,000 Hides)

S3 pricing has four dimensions. Most people only think about the first one.

Dimension What You Pay For Approximate Cost (us-east-1)
Storage GB stored per month $0.023/GB (Standard), $0.0125/GB (IA), $0.004/GB (Glacier), $0.00099/GB (Deep Archive)
Requests Every API call $0.005 per 1,000 PUT/POST, $0.0004 per 1,000 GET
Data transfer Bytes leaving AWS $0.09/GB out to internet (first 10 TB)
Lifecycle transitions Moving objects between classes $0.01 per 1,000 transitions

Let's start your investigation.

# Step 1: How big is this bucket, by storage class?
aws cloudwatch get-metric-statistics \
  --namespace AWS/S3 --metric-name BucketSizeBytes \
  --dimensions Name=BucketName,Value=acme-prod-backups \
    Name=StorageType,Value=StandardStorage \
  --start-time "$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%S)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%S)" \
  --period 86400 --statistics Average \
  --query 'Datapoints[*].[Timestamp,Average]' --output table

# Step 2: How many objects?
aws cloudwatch get-metric-statistics \
  --namespace AWS/S3 --metric-name NumberOfObjects \
  --dimensions Name=BucketName,Value=acme-prod-backups \
    Name=StorageType,Value=AllStorageTypes \
  --start-time "$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%S)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%S)" \
  --period 86400 --statistics Average

You pull the data. Storage has grown from 52 TB to 218 TB in 90 days. But the backup job writes only 500 GB per day. 90 days times 500 GB is 45 TB. Where did the other 121 TB come from?

Suspect 1: Versioning without lifecycle rules

# Check if versioning is enabled
aws s3api get-bucket-versioning --bucket acme-prod-backups
# Output: {"Status": "Enabled"}

# How many noncurrent (old) versions exist?
aws s3api list-object-versions --bucket acme-prod-backups \
  --prefix backups/daily/ --max-keys 1000 \
  --query 'length(Versions[?IsLatest==`false`])'

There it is. Versioning was enabled 90 days ago (a compliance initiative) but nobody added a lifecycle rule to expire old versions. Every daily backup overwrites the same keys — and every overwrite creates a new version while keeping the old one. After 90 days, every object has 90 versions. The bucket is storing 90x more data than it needs to.

Gotcha: Enabling versioning without a lifecycle rule is the most common S3 cost explosion. Versioning is a safety feature — it protects against accidental deletes. But without expiration rules, old versions accumulate silently. The S3 console shows the current version's size. The bill includes all versions.

Suspect 2: Incomplete multipart uploads

While you are in there, check for another invisible cost:

# List incomplete multipart uploads (these cost storage but are invisible to s3 ls)
aws s3api list-multipart-uploads --bucket acme-prod-backups

If your backup tool crashed mid-upload, the partial parts stay in S3 and you pay for them. They do not show up in aws s3 ls. They are ghosts in the bill.

Suspect 3: Delete markers piling up

With versioning enabled, DELETE does not actually delete anything — it adds a "delete marker." These markers accumulate and can slow down LIST operations (which cost money).

# Count delete markers
aws s3api list-object-versions --bucket acme-prod-backups \
  --query 'length(DeleteMarkers)'

Part 4: Fixing It — Lifecycle Policies

Time to stop the bleeding. A lifecycle policy automates storage class transitions, version expiration, and cleanup.

aws s3api put-bucket-lifecycle-configuration \
  --bucket acme-prod-backups \
  --lifecycle-configuration '{
    "Rules": [
      {
        "ID": "expire-old-versions",
        "Status": "Enabled",
        "Filter": { "Prefix": "" },
        "NoncurrentVersionTransitions": [
          { "NoncurrentDays": 30, "StorageClass": "GLACIER" }
        ],
        "NoncurrentVersionExpiration": { "NoncurrentDays": 90 }
      },
      {
        "ID": "transition-current-objects",
        "Status": "Enabled",
        "Filter": { "Prefix": "backups/" },
        "Transitions": [
          { "Days": 30, "StorageClass": "STANDARD_IA" },
          { "Days": 90, "StorageClass": "GLACIER" },
          { "Days": 365, "StorageClass": "DEEP_ARCHIVE" }
        ]
      },
      {
        "ID": "abort-incomplete-uploads",
        "Status": "Enabled",
        "Filter": { "Prefix": "" },
        "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
      },
      {
        "ID": "cleanup-delete-markers",
        "Status": "Enabled",
        "Filter": { "Prefix": "" },
        "Expiration": { "ExpiredObjectDeleteMarker": true }
      }
    ]
  }'

Let's break that down:

Rule What It Does Cost Impact
expire-old-versions Moves old versions to Glacier after 30 days, deletes after 90 Stops the 90x version accumulation
transition-current-objects Standard → IA → Glacier → Deep Archive over time 80-95% savings on aging backups
abort-incomplete-uploads Kills phantom multipart uploads after 7 days Eliminates invisible storage costs
cleanup-delete-markers Removes orphaned delete markers Speeds up LIST, minor savings

Remember: Every bucket with continuous writes needs a lifecycle rule. Set it at bucket creation time, not 90 days later when finance notices. Think of it like setting up log rotation — you do not wait until the disk fills up.

The storage class ladder

Standard ($0.023/GB)
    ↓ 30 days
Standard-IA ($0.0125/GB) — same speed, per-GB retrieval fee
    ↓ 90 days
Glacier Instant Retrieval ($0.004/GB) — millisecond retrieval
    ↓ 180 days
Glacier Flexible Retrieval ($0.0036/GB) — minutes to hours
    ↓ 365 days
Glacier Deep Archive ($0.00099/GB) — 12-48 hours retrieval

Trivia: Glacier Deep Archive stores data on magnetic tape in underground facilities. Retrieval takes 12-48 hours because robots must physically locate and mount the correct tape cartridge. This is the modern incarnation of the tape library, hidden behind an API.


Flashcard Check #1

Cover the answers and test yourself.

Question Answer
What are the four S3 pricing dimensions? Storage, requests, data transfer (egress), lifecycle transitions
What happens when you DELETE an object in a versioned bucket? A delete marker is created. The object data remains as a noncurrent version.
Why are incomplete multipart uploads dangerous? They consume storage and money but are invisible to aws s3 ls.
What did S3's consistency model change in December 2020? Changed from eventual consistency to strong read-after-write consistency for all operations.
What is the cheapest S3 storage class? Glacier Deep Archive at ~$0.00099/GB/month (but 12-48 hour retrieval).

Part 5: The Three Layers of S3 Access Control

Now that you have fixed the cost issue, your team lead has a follow-up: "While you are in there, make sure nobody can accidentally make this bucket public."

S3 has three overlapping access control mechanisms. This confuses everyone.

Layer 1: IAM Policies (identity-based)

Attached to IAM users, roles, or groups. "This role can read from these buckets."

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::acme-prod-backups",
      "arn:aws:s3:::acme-prod-backups/*"
    ]
  }]
}

Gotcha: The bucket itself (arn:aws:s3:::my-bucket) and the objects inside it (arn:aws:s3:::my-bucket/*) are separate resources. s3:ListBucket applies to the bucket ARN. s3:GetObject applies to the object ARN. Miss one and you get "Access Denied" even though you think you granted access.

Layer 2: Bucket Policies (resource-based)

Attached to the bucket. "This bucket allows these principals to do these things."

# View the current bucket policy
aws s3api get-bucket-policy --bucket acme-prod-backups | jq '.Policy | fromjson'

# Set a policy that denies unencrypted uploads
aws s3api put-bucket-policy --bucket acme-prod-backups --policy '{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyUnencryptedUploads",
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::acme-prod-backups/*",
    "Condition": {
      "StringNotEquals": {
        "s3:x-amz-server-side-encryption": "aws:kms"
      }
    }
  }]
}'

Layer 3: ACLs (legacy — avoid)

ACLs are the oldest access control mechanism. AWS now recommends disabling them entirely.

# Disable ACLs (recommended for all new buckets)
aws s3api put-bucket-ownership-controls --bucket acme-prod-backups \
  --ownership-controls '{"Rules": [{"ObjectOwnership": "BucketOwnerEnforced"}]}'

The nuclear option: Block Public Access

This overrides everything. Even if a bucket policy grants public access, Block Public Access prevents it.

# Enable at bucket level
aws s3api put-public-access-block --bucket acme-prod-backups \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Better: enable at account level (all buckets, including future ones)
aws s3control put-public-access-block --account-id 123456789012 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

War Story: Misconfigured S3 buckets with public read access have caused data leaks at Verizon (14 million customer records), Dow Jones (2.2 million records), and the US Department of Defense (1.8 billion social media posts). The problem was so widespread that AWS added Block Public Access in 2018 and made it the default for new buckets. Automated scanners find open S3 buckets within minutes of misconfiguration.

How evaluation works

When a request hits S3, the evaluation order is:

  1. Explicit deny wins — if any policy says Deny, the request is denied
  2. Bucket policy evaluated
  3. IAM policy evaluated
  4. ACLs evaluated (if not disabled)
  5. If nothing grants access, the request is denied (implicit deny)

Both the identity-based policy (IAM) and the resource-based policy (bucket policy) must allow the action for cross-account access. For same-account access, either one is sufficient.

Access Points: the modern alternative

For large organizations with many teams sharing a bucket, Access Points provide per-team entry points with their own policies:

aws s3control create-access-point --account-id 123456789012 \
  --name analytics-team --bucket acme-prod-backups

# The analytics team accesses through their access point
aws s3 ls s3://arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-team/

Part 6: War Story — The Typo That Broke the Internet

On February 28, 2017, an Amazon engineer was debugging the S3 billing system in us-east-1. They ran a command to remove a small number of servers from the S3 index subsystem. A typo in the command removed far more servers than intended.

The result: S3 in us-east-1 went down for nearly four hours. But the blast radius was not just S3. Thousands of services depended on S3:

  • The AWS Service Health Dashboard itself was hosted on S3 — so the dashboard that reports outages could not report its own outage
  • Slack, Trello, Quora, IFTTT, and thousands of other services that stored assets on S3 experienced failures
  • IoT devices, mobile apps, and CI/CD pipelines that fetched artifacts from S3 stalled
  • Services that used S3 for configuration storage could not start or restart

The lesson is architectural: S3 is so fundamental to the internet's infrastructure that a single-region outage cascades everywhere. AWS responded by adding safeguards that prevent rapid removal of subsystem capacity.

Mental Model: S3 is not just storage — it is infrastructure that other infrastructure depends on. When you architect systems, remember that S3 us-east-1 is a single point of failure for a significant portion of the internet. Cross-region replication exists for a reason.


Part 7: Encryption — Four Flavors

Your security team wants everything in the backup bucket encrypted. S3 offers four encryption approaches:

Type Who Manages Keys Audit Trail Use When
SSE-S3 AWS manages everything Minimal Default, simplest
SSE-KMS AWS KMS (your key or AWS-managed) CloudTrail logs every key use Compliance, audit, cross-account
SSE-C You provide the key per request None (you manage) You control key lifecycle entirely
Client-side You encrypt before upload None (AWS never sees plaintext) Maximum control, end-to-end
# Set default encryption to SSE-S3 (simplest)
aws s3api put-bucket-encryption --bucket acme-prod-backups \
  --server-side-encryption-configuration '{
    "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
  }'

# Or SSE-KMS with a customer-managed key (better audit trail)
aws s3api put-bucket-encryption --bucket acme-prod-backups \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/my-key-id"
      },
      "BucketKeyEnabled": true
    }]
  }'

Gotcha: SSE-KMS calls AWS KMS for every GET and PUT. KMS has a per-region request limit (5,500–30,000 requests/second depending on region). At high throughput, S3 operations fail with ThrottlingException from KMS. Enable BucketKeyEnabled: true to create a bucket-level data key that reduces KMS calls by up to 99%.


Part 8: Versioning, MFA Delete, and the "Oh No" Moment

You already discovered that versioning was the cost culprit. But versioning is also the feature that saves you when someone runs aws s3 rm --recursive on the wrong bucket.

# Enable versioning
aws s3api put-bucket-versioning --bucket acme-prod-backups \
  --versioning-configuration Status=Enabled

# List all versions of a specific object
aws s3api list-object-versions --bucket acme-prod-backups \
  --prefix backups/daily/2026-03-01.tar.gz

# Recover a deleted object (remove the delete marker)
aws s3api delete-object --bucket acme-prod-backups \
  --key backups/daily/2026-03-01.tar.gz \
  --version-id "DEL_MARKER_VERSION_ID"

# Retrieve a specific old version
aws s3api get-object --bucket acme-prod-backups \
  --key backups/daily/2026-03-01.tar.gz \
  --version-id "abc123xyz" recovered-backup.tar.gz

MFA Delete: the last line of defense

MFA Delete requires a hardware MFA token to permanently delete object versions or disable versioning. Only the root account can enable it.

# Enable MFA Delete (root account only, with MFA device)
aws s3api put-bucket-versioning --bucket acme-prod-backups \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::123456789012:mfa/root-device 123456"

This protects against ransomware, compromised credentials, and the engineer who runs delete-object --version-id at 3 AM on the wrong bucket.


Flashcard Check #2

Question Answer
What are the three S3 access control mechanisms? IAM policies (identity-based), bucket policies (resource-based), ACLs (legacy).
Why should you enable Block Public Access at the account level? It prevents any bucket in the account from being made public, even by mistake in a bucket policy.
What does BucketKeyEnabled: true do for SSE-KMS? Creates a bucket-level data key that reduces KMS API calls by up to 99%, avoiding KMS throttling.
What is the difference between a DELETE on a versioned vs non-versioned bucket? Versioned: creates a delete marker (data preserved). Non-versioned: permanent deletion.
Who can enable MFA Delete? Only the root account.

Part 9: Presigned URLs, Multipart Uploads, and S3 Select

Three features that separate S3 beginners from S3 operators.

Presigned URLs

Generate a temporary URL that grants time-limited access to a private object. No AWS credentials needed to use it.

# Generate a download link valid for 1 hour
aws s3 presign s3://acme-prod-backups/reports/quarterly-review.pdf --expires-in 3600
# Output: https://acme-prod-backups.s3.amazonaws.com/reports/quarterly-review.pdf?X-Amz-Algorithm=...

# Share that URL with anyone — they can download without AWS credentials
import boto3

s3 = boto3.client("s3")

# Upload URL — let a client upload directly to S3, bypassing your server
url = s3.generate_presigned_url(
    "put_object",
    Params={
        "Bucket": "acme-prod-backups",
        "Key": "uploads/incoming-report.pdf",
        "ContentType": "application/pdf",
    },
    ExpiresIn=900,  # 15 minutes
)

Trivia: Presigned URLs have enabled entire architectures: direct browser-to-S3 uploads (bypassing application servers), time-limited download links for paid content, and secure file sharing without running any server infrastructure. Many engineers do not know this feature exists and build unnecessary proxy layers instead.

Multipart uploads

Required for objects over 5 GB. Recommended for anything over 100 MB. Allows parallel upload of parts and resumability.

# aws s3 cp handles multipart automatically — configure thresholds
aws configure set default.s3.multipart_threshold 64MB
aws configure set default.s3.multipart_chunksize 16MB
aws configure set default.s3.max_concurrent_requests 20

# Upload a large file (multipart happens transparently)
aws s3 cp massive-backup.tar.gz s3://acme-prod-backups/backups/

# If you need to debug a stuck upload: list incomplete multipart uploads
aws s3api list-multipart-uploads --bucket acme-prod-backups

S3 Select

Query CSV, JSON, or Parquet files in-place without downloading the entire object. Useful when you need one column from a 50 GB CSV.

aws s3api select-object-content \
  --bucket acme-prod-backups --key data/billing-export.csv \
  --expression "SELECT s.service, s.cost FROM s3object s WHERE CAST(s.cost AS FLOAT) > 100" \
  --expression-type SQL \
  --input-serialization '{"CSV": {"FileHeaderInfo": "USE"}}' \
  --output-serialization '{"CSV": {}}' expensive-services.csv

Part 10: Event Notifications and Cross-Region Replication

Event notifications

S3 can trigger actions when objects are created, deleted, or restored. This is how you build event-driven architectures.

aws s3api put-bucket-notification-configuration --bucket acme-prod-backups \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [{
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:validate-backup",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [
            { "Name": "prefix", "Value": "backups/daily/" },
            { "Name": "suffix", "Value": ".tar.gz" }
          ]
        }
      }
    }],
    "EventBridgeConfiguration": {}
  }'

Targets: Lambda, SQS, SNS, EventBridge. EventBridge is the most flexible — it can filter on object metadata and route to dozens of different targets.

Mental Model: Use event notifications instead of polling with LIST. A bucket with 10 million objects generates 10,000 LIST API calls ($0.05) every time you check for new files. At 100 checks/minute, that is $7,200/month just for LIST operations. An event notification costs nothing — S3 pushes to you when something happens.

Cross-region replication

For disaster recovery or latency reduction, replicate objects to a bucket in another region.

# Both buckets must have versioning enabled
aws s3api put-bucket-replication --bucket acme-prod-backups \
  --replication-configuration '{
    "Role": "arn:aws:iam::123456789012:role/s3-replication-role",
    "Rules": [{
      "ID": "replicate-backups",
      "Status": "Enabled",
      "Filter": { "Prefix": "backups/" },
      "Destination": {
        "Bucket": "arn:aws:s3:::acme-prod-backups-eu",
        "StorageClass": "STANDARD_IA"
      },
      "DeleteMarkerReplication": { "Status": "Enabled" }
    }]
  }'

Gotcha: Replication does not copy existing objects — only new objects written after the rule is enabled. Use S3 Batch Operations to replicate existing data. Replication also does not work with SSE-C encrypted objects.


Part 11: S3 as a Data Lake Foundation

S3 is not just backup storage. It is the foundation of the modern data lake:

                     ┌──────────────┐
                     │   Athena     │ ← SQL queries directly on S3
                     │  (Serverless)│
                     └──────┬───────┘
    ┌───────────┐    ┌──────┴───────┐    ┌───────────┐
    │ Glue ETL  │───→│   S3 Bucket  │←───│  Kinesis   │
    │ (Transform)│    │  (Data Lake) │    │ (Ingest)   │
    └───────────┘    └──────┬───────┘    └───────────┘
                     ┌──────┴───────┐
                     │   Redshift   │ ← Spectrum reads S3 directly
                     │  (Warehouse) │
                     └──────────────┘

The key insight: S3 is cheap enough to store everything, durable enough to trust, and open enough (Parquet, CSV, JSON) that any tool can read it. This is why companies dump everything into S3 first and decide how to process it later.

S3 Inventory gives you a daily or weekly report of all objects — far cheaper than LIST for analytics:

aws s3api put-bucket-inventory-configuration --bucket acme-prod-backups \
  --id weekly-inventory \
  --inventory-configuration '{
    "Id": "weekly-inventory",
    "IsEnabled": true,
    "Destination": {
      "S3BucketDestination": {
        "Bucket": "arn:aws:s3:::acme-inventory",
        "Format": "CSV",
        "AccountId": "123456789012"
      }
    },
    "Schedule": { "Frequency": "Weekly" },
    "IncludedObjectVersions": "Current",
    "OptionalFields": ["Size", "StorageClass", "LastModifiedDate", "EncryptionStatus"]
  }'

Flashcard Check #3

Question Answer
What is a presigned URL? A temporary URL with embedded credentials that grants time-limited access to a private S3 object.
When should you use multipart upload? Required for objects over 5 GB. Recommended for anything over 100 MB (parallel parts, resumability).
Why use S3 event notifications instead of polling with LIST? LIST at scale is expensive ($0.005/1000 calls). Events are free and real-time.
Does cross-region replication copy existing objects? No. Only new objects written after the rule is enabled. Use Batch Operations for existing data.
What does S3 Select do? Runs SQL queries against CSV/JSON/Parquet objects in-place, returning only matching data.

Exercises

Exercise 1: Find the hidden costs (5 minutes)

You have a bucket called staging-artifacts. Run these commands and interpret the output:

# 1. Check versioning status
aws s3api get-bucket-versioning --bucket staging-artifacts

# 2. List incomplete multipart uploads
aws s3api list-multipart-uploads --bucket staging-artifacts

# 3. Check lifecycle rules
aws s3api get-bucket-lifecycle-configuration --bucket staging-artifacts

If versioning is enabled and there are no lifecycle rules, what is the risk?

Answer Old versions accumulate indefinitely, silently increasing storage costs. Every overwrite keeps the old version. Without a `NoncurrentVersionExpiration` rule, you pay for every version forever. This is exactly the $5,000/month problem from the mission. Add a lifecycle rule:
aws s3api put-bucket-lifecycle-configuration --bucket staging-artifacts \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "expire-old-versions",
      "Status": "Enabled",
      "Filter": { "Prefix": "" },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
    }]
  }'

Exercise 2: Secure a bucket (10 minutes)

Write a bucket policy that: 1. Denies any PutObject that is not encrypted with SSE-KMS 2. Allows a specific IAM role to read and list objects 3. Denies all access from outside a specific VPC endpoint

Hint Use three Statement blocks. The encryption check uses a `StringNotEquals` condition on `s3:x-amz-server-side-encryption`. The VPC restriction uses `StringNotEquals` on `aws:sourceVpce`.
Solution
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::staging-artifacts/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "AllowReaderRole",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:role/artifact-reader" },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::staging-artifacts",
        "arn:aws:s3:::staging-artifacts/*"
      ]
    },
    {
      "Sid": "DenyOutsideVPC",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::staging-artifacts",
        "arn:aws:s3:::staging-artifacts/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "vpce-0abc123def456"
        }
      }
    }
  ]
}

Exercise 3: Design a lifecycle strategy (15 minutes)

Your company has three data types in S3:

  • Application logs — useful for 7 days, required for compliance for 1 year
  • Database backups — accessed within first week, rarely after, required for 7 years
  • User uploads — unpredictable access pattern, cannot be deleted

Design lifecycle rules for each. Consider: storage classes, transitions, versioning, expiration. What is the approximate monthly cost per TB for each strategy?

Solution sketch **Application logs:** Standard (7d) → Standard-IA (30d) → Glacier (90d) → Deep Archive (365d) → Expire at 365d. Cost: ~$0.023/GB first month, drops to ~$0.001/GB by month 4. Per TB/year: ~$30. **Database backups:** Standard (7d) → Standard-IA (30d) → Glacier (90d) → Deep Archive (365d). No expiration for 7 years. Enable versioning + MFA Delete. Per TB stored 7 years in Deep Archive: ~$83. **User uploads:** Intelligent-Tiering. No lifecycle rules needed — it auto-tiers based on access. Monitoring fee of $0.0025/1000 objects/month. Never expire. Per TB/month: $0.023 if accessed, auto-drops to $0.004 if not.

Cheat Sheet

┌─────────────────────────────────────────────────────────────────┐
│ S3 OPERATIONS QUICK REFERENCE                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ INVESTIGATE                                                     │
│   aws s3 ls s3://BUCKET/ --recursive --human-readable --summ.  │
│   aws s3api get-bucket-versioning --bucket BUCKET               │
│   aws s3api list-object-versions --bucket BUCKET --prefix PFX   │
│   aws s3api list-multipart-uploads --bucket BUCKET              │
│   aws s3api get-bucket-lifecycle-configuration --bucket BUCKET  │
│   aws s3api get-bucket-policy --bucket BUCKET | jq ...          │
│   aws s3api get-public-access-block --bucket BUCKET             │
│                                                                 │
│ TRANSFER                                                        │
│   aws s3 cp FILE s3://BUCKET/KEY                                │
│   aws s3 sync ./DIR s3://BUCKET/PREFIX/ --delete                │
│   aws s3 presign s3://BUCKET/KEY --expires-in 3600              │
│                                                                 │
│ CONFIGURE                                                       │
│   aws s3api put-bucket-versioning ...                           │
│   aws s3api put-bucket-encryption ...                           │
│   aws s3api put-bucket-lifecycle-configuration ...              │
│   aws s3api put-public-access-block ...                         │
│   aws s3api put-bucket-policy --policy file://policy.json       │
│                                                                 │
│ IDENTITY CHECK                                                  │
│   aws sts get-caller-identity                                   │
│   aws iam simulate-principal-policy ...                         │
│                                                                 │
│ STORAGE CLASSES (us-east-1, per GB/month)                       │
│   Standard:       $0.023   │  Glacier Instant: $0.004           │
│   Standard-IA:    $0.0125  │  Glacier Flex:    $0.0036          │
│   One Zone-IA:    $0.01    │  Deep Archive:    $0.00099         │
│   Intelligent-T:  $0.023*  │  * auto-tiers, monitoring fee      │
│                                                                 │
│ FOUR PRICING DIMENSIONS                                         │
│   1. Storage (GB/month)     3. Data transfer out                │
│   2. Requests (per 1,000)   4. Lifecycle transitions            │
│                                                                 │
│ ENCRYPTION OPTIONS                                              │
│   SSE-S3:  AWS-managed keys, simplest                           │
│   SSE-KMS: KMS keys, audit trail, use BucketKeyEnabled          │
│   SSE-C:   Customer-provided key per request                    │
│   Client:  You encrypt before upload                            │
│                                                                 │
│ ACCESS CONTROL PRIORITY                                         │
│   Explicit Deny > Bucket Policy > IAM Policy > ACL > Deny      │
│                                                                 │
│ DURABILITY: 99.999999999% (11 nines)                            │
│ CONSISTENCY: Strong read-after-write (since Dec 2020)           │
└─────────────────────────────────────────────────────────────────┘

Takeaways

  • S3 is a key-value store, not a filesystem. No directories, no rename, no append. The flat namespace is what makes it infinitely scalable — and what confuses everyone at first.

  • Versioning without lifecycle rules is a cost bomb. Every overwrite keeps the old version. Set NoncurrentVersionExpiration the same day you enable versioning.

  • S3 has four pricing dimensions, not one. Storage, requests, data transfer, and lifecycle transitions. The bill surprise almost always comes from the one you forgot.

  • Block Public Access at the account level. Do not trust bucket policies alone. One "Principal": "*" and your data is on the internet.

  • Use event notifications, not LIST polling. LIST at scale is slow and expensive. S3 can push events to Lambda, SQS, SNS, or EventBridge in near real-time.

  • Strong consistency is the default since December 2020. If you see retry-and-verify patterns in old code, they are no longer necessary. But they are harmless — leave them unless you are actively refactoring.