Part 5 of 1228 min read · 6 diagramsAI-assisted

Storage: S3, EBS & EFS

Table of Contents#

  1. Three Storage Shapes, Three AWS Services
  2. S3 — The Foundational Object Store
  3. S3's Consistency and Durability Guarantees
  4. S3 Storage Classes
  5. S3 Lifecycle Policies — Automating Storage-Class Transitions
  6. S3 Versioning
  7. S3 Bucket Policies and Block Public Access
  8. S3 Encryption — At Rest and In Transit
  9. S3 Replication — Cross-Region and Same-Region
  10. S3 Performance — Prefixes, Multipart Upload, and Transfer Acceleration
  11. EBS — Block Storage for EC2
  12. EBS Volume Types
  13. EBS Snapshots — Backup, Revisited
  14. EBS Multi-Attach and the "One Instance" Rule
  15. EFS — Shared File Storage
  16. EFS Performance and Throughput Modes
  17. S3 vs EBS vs EFS — The Decision Framework
  18. FSx — A Brief, Practical Note
  19. S3 Object Lock — Write Once, Read Many (WORM)
  20. S3 Access Points — Simplifying Access at Scale
  21. S3 Batch Operations — Bulk Actions at Scale
  22. S3 Storage Lens — Organization-Wide Storage Visibility
  23. EBS Encryption and Fast Snapshot Restore
  24. Part 5 CLI Cheat Sheet
  25. Storage Best Practices — The Consolidated Checklist
  26. A Full Worked Example: Designing Storage for a Media Upload Platform
  27. Common Mistakes
  28. Worked Practice Problems
  29. Summary and What's Next

Three Storage Shapes, Three AWS Services#

Directly extending the general storage-scaling ideas from the Capacity Planning series and the storage-class discussion implicit in the Databases series — AWS offers three fundamentally different storage shapes, and picking the right one for the job is the entire point of this part.

Diagram

S3 — The Foundational Object Store#

Simple Storage Service (S3) stores objects (essentially, whole files plus metadata) inside buckets (a flat, globally-unique-named container) — genuinely one of AWS's oldest, most heavily used, and most reliable services.

# Create a bucket (bucket names are GLOBALLY unique across ALL of AWS)
aws s3api create-bucket --bucket my-unique-app-bucket-2026 --region us-east-1

# Upload and download objects
aws s3 cp ./report.csv s3://my-unique-app-bucket-2026/reports/report.csv
aws s3 cp s3://my-unique-app-bucket-2026/reports/report.csv ./report.csv

# List objects under a "prefix" (S3 has no real directories —
# the "/" is just a convention in the object key name)
aws s3 ls s3://my-unique-app-bucket-2026/reports/

# Sync an entire local directory to a bucket
aws s3 sync ./dist s3://my-unique-app-bucket-2026/static/ --delete

Why "S3 has no real directories" is worth stating precisely, a common point of confusion: an object's key (e.g. reports/2026/report.csv) is just a single string containing slashes — the Console displays it as a folder structure for convenience, but there is no actual directory object anywhere; this has real performance implications covered in the "Prefixes" section later in this part.


S3's Consistency and Durability Guarantees#

Directly connects to the consistency-model discussion already covered in depth in the Reliability & Architecture Patterns series (Part 3, CAP theorem) — S3 is a genuinely excellent real-world example to cite.

Durability: "11 nines" (99.999999999%) — S3 automatically stores redundant copies of every object across multiple Availability Zones within a region. A commonly-cited, memorable way to state this: if you stored 10 million objects in S3, you'd statistically expect to lose one object roughly every 10,000 years.

Consistency: strong read-after-write consistency for ALL operations, as of a 2020 AWS improvement — worth knowing precisely, since it used to be eventually consistent for overwrite/delete operations, and this history still shows up in older documentation and some interview answers. The current, correct answer: a PUT followed immediately by a GET of the same object always returns the latest version, with no read-after-write consistency gap to reason about anymore — a genuinely rare case of a distributed system successfully offering strong consistency without a meaningful availability tradeoff most users ever notice.


S3 Storage Classes#

S3 offers multiple storage classes, all with the same API and durability guarantee, trading retrieval speed/availability for cost.

Storage classRetrieval timeBest fit
S3 StandardMillisecondsFrequently accessed data
S3 Intelligent-TieringMilliseconds (automatic)Unknown or changing access patterns — AWS automatically moves objects between tiers based on actual usage
S3 Standard-IA (Infrequent Access)MillisecondsAccessed less than monthly, but needs millisecond access when it IS accessed
S3 One Zone-IAMillisecondsSame as Standard-IA, but stored in only ONE AZ — cheaper, less durable against an AZ loss
S3 Glacier Instant RetrievalMillisecondsArchive data still needing occasional instant access
S3 Glacier Flexible RetrievalMinutes to hoursTrue archives, retrieved rarely
S3 Glacier Deep ArchiveUp to 12 hoursThe cheapest possible storage — long-term compliance retention, almost never retrieved
# Upload directly into a specific storage class
aws s3 cp big-archive.tar.gz s3://my-bucket/archives/ --storage-class GLACIER

# Check the storage class of an existing object
aws s3api head-object --bucket my-bucket --key archives/big-archive.tar.gz --query StorageClass

S3 Lifecycle Policies — Automating Storage-Class Transitions#

Manually moving old objects to cheaper storage classes is exactly the kind of repetitive, well-defined process the Toil discussion (SRE Fundamentals series) flags as an automation target — S3 Lifecycle policies automate it entirely.

cat <<'LIFECYCLE'
{
  "Rules": [{
    "ID": "archive-old-logs",
    "Filter": { "Prefix": "logs/" },
    "Status": "Enabled",
    "Transitions": [
      { "Days": 30, "StorageClass": "STANDARD_IA" },
      { "Days": 90, "StorageClass": "GLACIER" }
    ],
    "Expiration": { "Days": 2555 }
  }]
}
LIFECYCLE

aws s3api put-bucket-lifecycle-configuration \
  --bucket my-bucket --lifecycle-configuration file://lifecycle.json

A concrete, worked cost example worth internalizing: log files accessed daily for the first month, rarely after, and legally required to be kept for 7 years — a lifecycle policy transitioning Standard → Standard-IA at 30 days → Glacier at 90 days → permanent deletion at day 2,555 (7 years) automates the ENTIRE cost-optimized lifecycle with zero ongoing manual work, directly connecting to the FinOps/cost-optimization theme this whole series carries.


S3 Versioning#

Once enabled on a bucket, every PUT to the same key creates a NEW version rather than overwriting the old one — the old version remains retrievable.

aws s3api put-bucket-versioning --bucket my-bucket --versioning-configuration Status=Enabled

# A "delete" with versioning enabled doesn't actually erase data —
# it adds a "delete marker" as the new latest version
aws s3api delete-object --bucket my-bucket --key important-file.txt

# The OLD version is still retrievable by its version ID
aws s3api list-object-versions --bucket my-bucket --prefix important-file.txt
aws s3api get-object --bucket my-bucket --key important-file.txt \
  --version-id abc123def456 restored-file.txt

Why this is a genuinely strong, real protection against both accidental deletion AND ransomware-style malicious deletion, worth stating explicitly: even a full DeleteObject call doesn't destroy the underlying data with versioning enabled — it only adds a marker hiding it from the default "latest" view, directly connecting to the backup/durability discipline from the Databases series (Part 3). Combined with MFA Delete (requiring a physical MFA code to permanently delete a version, not just hide it), this is a strong, defense-in-depth data-protection pattern worth knowing by name.


S3 Bucket Policies and Block Public Access#

A resource-based policy (Part 2) attached directly to a bucket, controlling who can access it — and the single most consequential source of real-world S3 security incidents when misconfigured.

# A bucket policy granting read access to a specific IAM role
# from another account — the cross-account pattern from Part 2,
# applied to S3 specifically
cat <<'POLICY'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::999988887777:role/ReportingRole" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-bucket/reports/*"
  }]
}
POLICY

aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json

# Block Public Access — a bucket-level (or account-level) SETTING
# that overrides even a permissive bucket policy, as a safety net
aws s3api put-public-access-block \
  --bucket my-bucket \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Why Block Public Access is worth enabling as an account-wide default, worth stating explicitly, and why so many real, widely-publicized data breaches trace back to exactly this gap: a huge number of real-world S3 data exposures happened because a bucket policy or ACL was accidentally made public — Block Public Access is a hard, structural safety net that prevents public access EVEN IF a future bucket policy mistake would otherwise allow it, the same "guardrail beyond individual permission grants" idea already covered for SCPs in Part 1.


S3 Encryption — At Rest and In Transit#

# Enable default encryption at rest (SSE-KMS) for a bucket —
# every new object is automatically encrypted, even without the
# uploader specifying encryption explicitly
aws s3api put-bucket-encryption \
  --bucket my-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms", "KMSMasterKeyID": "alias/my-key"}}]
  }'

# Enforce encryption IN TRANSIT (reject any non-HTTPS request)
# via a bucket policy Deny statement
cat <<'ENFORCE_TLS'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:*",
    "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
    "Condition": { "Bool": { "aws:SecureTransport": "false" } }
  }]
}
ENFORCE_TLS

A deeper treatment of KMS and envelope encryption specifically is covered in Part 9 (Security), since it applies identically across nearly every AWS service, not just S3.


S3 Replication — Cross-Region and Same-Region#

Diagram
aws s3api put-bucket-replication \
  --bucket my-source-bucket \
  --replication-configuration '{
    "Role": "arn:aws:iam::123456789012:role/S3ReplicationRole",
    "Rules": [{
      "Status": "Enabled",
      "Priority": 1,
      "Filter": {},
      "Destination": {"Bucket": "arn:aws:s3:::my-destination-bucket", "StorageClass": "STANDARD"}
    }]
  }'

Why Cross-Region Replication is the direct, concrete S3 implementation of the multi-region DR strategies already covered in depth in the Disaster Recovery series (Part 1), worth stating explicitly: it's fundamentally the same asynchronous replication pattern already covered generically for databases in the Databases series (Part 1) — new/updated objects replicate to the destination bucket automatically, but asynchronously, meaning there's a real (usually small) RPO gap to account for, exactly the same replication-lag consideration already covered there.


S3 Performance — Prefixes, Multipart Upload, and Transfer Acceleration#

A genuinely important, historically significant performance note worth knowing precisely: modern S3 (since a 2018 improvement) automatically scales request rate horizontally across prefixes — there is NO LONGER a hard need to manually "randomize" key prefixes for raw throughput the way older S3 guidance recommended. It's still worth knowing this history, since it appears in older material and occasionally in interview questions about "how would you design S3 keys for high throughput" — the honest, current answer is that S3 now handles this automatically for the vast majority of workloads.

# Multipart upload — required for objects over 5GB, and
# recommended for anything over ~100MB for resumability
# and parallel-upload speed
aws s3 cp huge-file.tar.gz s3://my-bucket/ --expected-size 10737418240

# S3 Transfer Acceleration — routes uploads through
# CloudFront's global edge network for faster long-distance
# uploads (e.g. a user far from the bucket's region)
aws s3api put-bucket-accelerate-configuration \
  --bucket my-bucket --accelerate-configuration Status=Enabled

EBS — Block Storage for EC2#

Elastic Block Store (EBS) provides network-attached block-storage volumes, functioning like a raw disk attached to exactly one EC2 instance (Part 3) at a time.

# Create and attach an EBS volume
aws ec2 create-volume --availability-zone us-east-1a --size 100 --volume-type gp3 \
  --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=app-data}]'

aws ec2 attach-volume --volume-id vol-0123456789abcdef0 \
  --instance-id i-0123456789abcdef0 --device /dev/sdf

A critical, precise fact worth stating explicitly: an EBS volume must be in the SAME Availability Zone as the instance it's attached to — this is a direct, physical constraint (the volume is genuinely a network-attached disk within that specific AZ's infrastructure), and it's exactly why a database's data on EBS doesn't automatically survive an AZ failure the way S3 (which spans multiple AZs by design) does — a real, important distinction covered further in Part 6's database HA discussion.


EBS Volume Types#

TypeBest fitKey characteristic
gp3 (General Purpose SSD)The default choice for most workloadsBaseline 3,000 IOPS / 125 MB/s, independently scalable up to 16,000 IOPS without needing a larger volume
io2 Block ExpressHigh-performance databases needing consistent, very high IOPSUp to 256,000 IOPS, sub-millisecond latency, 99.999% durability
st1 (Throughput Optimized HDD)Large, sequential-access workloads (big data, log processing)Optimized for throughput (MB/s), not IOPS — cheaper for this access pattern
sc1 (Cold HDD)Rarely-accessed, large volumesThe cheapest EBS option

Why gp3 specifically decoupling IOPS/throughput from volume SIZE matters, worth explaining precisely, since it's a genuine improvement over the older gp2 type: gp2's performance scaled WITH the volume's size (a small gp2 volume was performance-limited purely by being small) — gp3 lets you provision exactly the IOPS and throughput a workload needs independently of how many GB you actually need, avoiding the old "over-provision capacity just to get more speed" waste.

# Modify an existing gp3 volume's IOPS/throughput WITHOUT downtime
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 --iops 6000 --throughput 250

EBS Snapshots — Backup, Revisited#

A snapshot is a point-in-time, incremental backup of an EBS volume, stored in S3 (transparently, behind the scenes) — directly extending the incremental-backup concepts already covered in depth in the Databases series (Part 3).

aws ec2 create-snapshot --volume-id vol-0123456789abcdef0 \
  --description "pre-migration backup" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=pre-migration}]'

# Create a NEW volume from a snapshot (e.g. to restore, or to
# clone data into a different AZ)
aws ec2 create-volume --snapshot-id snap-0123456789abcdef0 --availability-zone us-east-1b

# Automate snapshots on a schedule with Data Lifecycle Manager,
# instead of remembering to run this by hand
aws dlm create-lifecycle-policy \
  --description "daily-ebs-snapshots" \
  --state ENABLED \
  --execution-role-arn arn:aws:iam::123456789012:role/DLMRole \
  --policy-details '{
    "ResourceTypes": ["VOLUME"],
    "TargetTags": [{"Key": "Backup", "Value": "true"}],
    "Schedules": [{"Name": "daily", "CreateRule": {"Interval": 24, "IntervalUnit": "HOURS", "Times": ["03:00"]}, "RetainRule": {"Count": 7}}]
  }'

Why snapshots are "incremental" specifically, worth stating precisely: only the blocks that CHANGED since the last snapshot are actually stored — the first snapshot of a volume is a full copy, but every subsequent snapshot only captures the delta, dramatically reducing both storage cost and snapshot creation time, exactly the same incremental-backup mechanics already covered generically in the Databases series.


EBS Multi-Attach and the "One Instance" Rule#

The default, standard rule: an EBS volume can only be attached to ONE EC2 instance at a time. A specific, more advanced feature — EBS Multi-Attach — relaxes this for io2/io1 volumes, allowing attachment to up to 16 instances simultaneously within the same AZ.

Why Multi-Attach is a narrow, specialized feature rather than a general-purpose solution, worth stating explicitly: it provides shared BLOCK-level access, but does NOT provide any filesystem-level coordination — the application itself (e.g. a cluster-aware filesystem, or a database engine specifically designed for shared storage) must handle concurrent write coordination; it is emphatically not a drop-in way to let multiple ordinary instances safely read/write the same files at once. For that genuinely common need — many instances needing shared, ordinary file access — EFS (next section) is almost always the right answer instead.


EFS — Shared File Storage#

Elastic File System (EFS) is a fully managed NFS (Network File System) that many EC2 instances — even across multiple AZs — can mount and use concurrently, with normal file-level semantics.

Diagram
# Create an EFS filesystem
aws efs create-file-system --creation-token my-app-efs --encrypted --performance-mode generalPurpose

# Create a mount target in each AZ you need to mount from
aws efs create-mount-target --file-system-id fs-0123456789abcdef0 \
  --subnet-id subnet-private-1a --security-groups sg-efs123

# Mount it from within an EC2 instance, exactly like any
# other network filesystem
sudo mount -t efs fs-0123456789abcdef0:/ /mnt/shared-data

Why EFS is the right tool specifically when MANY instances need genuinely shared, concurrent file access with real filesystem semantics, worth stating explicitly as the key differentiator from EBS: unlike EBS's one-instance (or narrowly-Multi-Attach) model, EFS is designed from the ground up for exactly this — a content management system's uploaded media, shared configuration files, or a home-directory-style shared workspace across a fleet. It automatically scales storage capacity with usage — there's no volume size to provision or resize, unlike EBS.


EFS Performance and Throughput Modes#

ModeHow throughput scales
Bursting (default)Throughput scales with the AMOUNT of data stored — a small filesystem gets a small baseline, with burst credits for spikes
ProvisionedYou specify a fixed throughput level directly, independent of how much data is stored — for high-throughput needs on a small dataset
ElasticAutomatically scales throughput up/down based on actual workload, with no capacity planning needed at all
aws efs create-file-system --creation-token my-app-efs \
  --throughput-mode elastic

A genuinely important, common gotcha worth naming explicitly, connecting back to the Bursting mode: a SMALL filesystem on Bursting mode can genuinely run out of burst credits under sustained load and get throttled — this is directly analogous to the T-series "burstable" EC2 instance CPU-credit mechanism from Part 3, just applied to storage throughput instead of compute. Provisioned or Elastic throughput mode avoids this entirely, at a corresponding cost.


S3 vs EBS vs EFS — The Decision Framework#

Consolidating this entire part into one practical, interview-ready decision tool.

Diagram
S3EBSEFS
Storage shapeObjectBlockFile (NFS)
Attachable toAccessed via HTTP API from anywhereONE EC2 instance (or up to 16 with Multi-Attach, same AZ)MANY instances, across AZs, concurrently
Scales toEffectively unlimitedFixed size (resizable)Grows/shrinks automatically
Typical useStatic assets, backups, data lakesDatabase data files, boot volumesShared config, CMS uploads, shared workspaces
Durability11 nines, multi-AZ by designTied to a single AZ (snapshot to S3 for durability)Multi-AZ by design

FSx — A Brief, Practical Note#

Worth knowing by name even without full depth: Amazon FSx provides fully managed versions of specific, specialized third-party filesystems — FSx for Windows File Server (native SMB, for Windows-based workloads), FSx for Lustre (a high-performance filesystem for HPC/ML workloads needing extreme throughput), and FSx for NetApp ONTAP / FSx for OpenZFS (for organizations standardizing on those specific storage platforms). Reach for FSx specifically when a workload has a hard dependency on one of these particular filesystem technologies that EFS's standard NFS interface doesn't provide.


S3 Object Lock — Write Once, Read Many (WORM)#

A genuinely important compliance feature worth knowing precisely, directly extending the Versioning discussion earlier in this part. S3 Object Lock prevents an object version from being deleted or overwritten for a defined retention period — or indefinitely — even by an account administrator.

Diagram
# Enable Object Lock at bucket creation (cannot be added to
# an existing bucket after the fact — a genuinely important
# constraint worth knowing)
aws s3api create-bucket --bucket compliance-records-bucket \
  --object-lock-enabled-for-bucket

# Apply a COMPLIANCE-mode retention to a specific object version
aws s3api put-object-retention \
  --bucket compliance-records-bucket --key contracts/2026-agreement.pdf \
  --retention '{"Mode":"COMPLIANCE","RetainUntilDate":"2033-01-01T00:00:00Z"}'

Why "cannot be added to an existing bucket" is worth knowing precisely, a genuinely common real-world gotcha: Object Lock must be enabled at BUCKET CREATION time — a team that later realizes they need this compliance guarantee cannot simply enable it on their existing bucket; they must create a new Object-Lock-enabled bucket and migrate data into it, directly reinforcing the general "plan for compliance requirements before they become urgent" lesson already touched on in the DevSecOps series.


S3 Access Points — Simplifying Access at Scale#

As a single bucket accumulates many different consumers (different applications, different teams, each needing a different scoped view), managing one increasingly complex bucket policy becomes genuinely unwieldy. S3 Access Points create named, dedicated entry points into a bucket, each with its OWN policy.

# Create an access point scoped to only one prefix of the bucket,
# for one specific consuming application
aws s3control create-access-point \
  --account-id 123456789012 \
  --name reporting-app-access \
  --bucket shared-data-bucket \
  --vpc-configuration VpcId=vpc-0123456789abcdef0

aws s3control put-access-point-policy \
  --account-id 123456789012 --name reporting-app-access \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/ReportingRole"},"Action":"s3:GetObject","Resource":"arn:aws:s3:us-east-1:123456789012:accesspoint/reporting-app-access/object/reports/*"}]}'

Why this is worth adopting once a bucket serves more than a handful of distinct consumers, worth stating explicitly: instead of one increasingly complex, hard-to-audit bucket policy trying to express every consumer's distinct access needs in a single document, each Access Point gets its OWN focused, independently auditable policy — directly applying the same "separate concerns instead of one tangled document" principle already covered generically across this series' IAM discussion, just applied at the S3 resource-policy layer.


S3 Batch Operations — Bulk Actions at Scale#

For operations that need to run against millions of existing objects (re-encrypting, copying to a new storage class, applying a new tag), doing it object-by-object via the CLI would be impractically slow. S3 Batch Operations runs a specified action against every object in a manifest, at scale, fully managed.

aws s3control create-job \
  --account-id 123456789012 \
  --operation '{"S3PutObjectTagging":{"TagSet":[{"Key":"Reviewed","Value":"true"}]}}' \
  --manifest '{"Spec":{"Format":"S3BatchOperations_CSV_20180820","Fields":["Bucket","Key"]},"Location":{"ObjectArn":"arn:aws:s3:::my-bucket/manifest.csv","ETag":"..."}}' \
  --priority 1 --role-arn arn:aws:iam::123456789012:role/S3BatchRole \
  --report '{"Bucket":"arn:aws:s3:::my-bucket","Prefix":"batch-reports","Format":"Report_CSV_20180820","Enabled":true,"ReportScope":"AllTasks"}'

A genuinely realistic use case worth stating explicitly: rotating encryption keys across every object in a multi-million-object bucket, or retroactively applying the tagging discipline from Part 1 to a bucket that predates the org's tagging policy — both are exactly the kind of bulk, well-defined, repetitive operation Batch Operations is built for, with built-in progress tracking and a completion report, rather than a fragile custom script looping through list-objects pages one at a time.


S3 Storage Lens — Organization-Wide Storage Visibility#

A dashboard providing storage usage and activity metrics across an entire Organization's S3 usage — genuinely useful for answering "where is our S3 cost actually going" at scale, directly connecting to the cost-optimization/FinOps theme carried throughout this series.

aws s3control put-storage-lens-configuration \
  --account-id 123456789012 --config-id org-wide-lens \
  --storage-lens-configuration '{"Id":"org-wide-lens","AccountLevel":{"BucketLevel":{}},"IsEnabled":true,"DataExport":{"S3BucketDestination":{"AccountId":"123456789012","Arn":"arn:aws:s3:::storage-lens-reports","Format":"CSV","OutputSchemaVersion":"V_1"}}}'

Why this matters at real organizational scale, worth stating explicitly: without Storage Lens, answering "which team's buckets are actually driving our S3 bill, and are they using appropriate storage classes" requires manually aggregating across every account and bucket — Storage Lens provides this org-wide, cross-account visibility natively, directly feeding into the cost-allocation and tagging discipline already covered in Part 1.


EBS Encryption and Fast Snapshot Restore#

Two genuinely important, practical EBS details worth knowing precisely for production use.

# Enable EBS encryption BY DEFAULT for the entire account/region —
# every new volume is automatically encrypted, with zero
# per-volume effort required going forward
aws ec2 enable-ebs-encryption-by-default

# Fast Snapshot Restore — eliminates the normal "lazy loading"
# latency penalty a volume created from a snapshot would
# otherwise have on its FIRST access to each block
aws ec2 enable-fast-snapshot-restores \
  --availability-zones us-east-1a us-east-1b \
  --source-snapshot-ids snap-0123456789abcdef0

Why Fast Snapshot Restore matters specifically for Auto Scaling Groups launching from a golden-AMI-backed snapshot (Part 3), worth stating explicitly: without it, a newly launched volume's blocks are lazily fetched from S3 (where the snapshot data actually lives) on FIRST access — meaning an instance's very first few moments of disk I/O can be meaningfully slower than steady-state, directly compounding the cold-start problem already discussed for Auto Scaling in Part 3. Fast Snapshot Restore pre-warms the volume, eliminating this first-access penalty entirely, at an additional hourly cost per AZ/snapshot combination.


Part 5 CLI Cheat Sheet#

AreaCommandPurpose
S3 basicsaws s3 cp / aws s3 syncUpload/download/sync objects
S3 basicsaws s3api create-bucketCreate a bucket
S3 lifecycleaws s3api put-bucket-lifecycle-configurationAutomate storage-class transitions and expiration
S3 versioningaws s3api put-bucket-versioningEnable/suspend versioning
S3 securityaws s3api put-public-access-blockEnforce no public access, as a safety net
S3 securityaws s3api put-bucket-encryptionSet default encryption
S3 complianceaws s3api put-object-retentionApply Object Lock retention
S3 accessaws s3control create-access-pointCreate a scoped, named access point
S3 bulk opsaws s3control create-jobRun a Batch Operations job
EBSaws ec2 create-volume / attach-volumeCreate and attach a volume
EBSaws ec2 create-snapshotCreate an incremental backup
EBSaws ec2 enable-ebs-encryption-by-defaultEncrypt every new volume automatically
EFSaws efs create-file-system / create-mount-targetCreate a shared filesystem and mount targets

Storage Best Practices — The Consolidated Checklist#

  • Enable Block Public Access at the account level, not just per-bucket — a structural safety net against future policy mistakes.
  • Enable Versioning on any bucket holding data that must survive accidental or malicious deletion, and consider Object Lock in COMPLIANCE mode for hard regulatory retention requirements.
  • Use Lifecycle policies to automate storage-class transitions, rather than manual, easily-forgotten cleanup scripts.
  • Enable EBS encryption by default at the account level — zero ongoing effort once turned on.
  • Take EBS snapshots on a schedule (via Data Lifecycle Manager), not ad hoc — and remember snapshots are the ONLY thing giving EBS data cross-AZ durability.
  • Use EFS, not EBS Multi-Attach, for genuine multi-instance shared file access — Multi-Attach provides no file-level write coordination.
  • Adopt S3 Access Points once a bucket serves more than a handful of distinct consumers, instead of one increasingly tangled bucket policy.
  • Use Fast Snapshot Restore for AMI-backed snapshots feeding a latency-sensitive Auto Scaling Group, to avoid lazy-loading latency on newly launched instances.

A Full Worked Example: Designing Storage for a Media Upload Platform#

Bringing this entire part together into one concrete, realistic design — genuinely worth walking through end to end.

Scenario: a platform where users upload photos/videos, which get processed (thumbnails, transcoding) and served globally, with a database tracking metadata.

Diagram

Walking through each storage decision and its explicit reasoning:

  1. raw-uploads bucket uses S3 (not EBS/EFS) because uploads are whole-object, accessed via HTTP, and need to scale to effectively unlimited storage with zero capacity planning — exactly S3's core strength. Versioning protects against an accidental overwrite during a re-upload; a Lifecycle policy transitions old raw uploads to Glacier once processing is confirmed complete and the raw original is rarely needed again.
  2. processed-media bucket is a separate bucket (not a prefix in the same bucket) specifically so its own, simpler bucket policy (public read, served through CloudFront in Part 8) never risks accidentally applying to the raw-uploads bucket, which should never be public — a direct application of the least-exposure principle from this series' security theme.
  3. EFS is used specifically for the processing fleet's SHARED temporary workspace during a multi-instance transcoding job that needs several instances to read/write the same intermediate files concurrently — exactly EFS's core differentiator from EBS.
  4. EBS gp3 handles the app servers' own root volumes and any purely local, single-instance working storage — the default, appropriate choice per this part's decision framework, needing no special justification since it's simply the right tool for single-instance block storage.

Why walking through a design this way — bucket by bucket, volume by volume, each with an explicit reason — is worth practicing as a habit, directly echoing the same lesson from Part 2's IAM design walkthrough: naming the SPECIFIC property of each storage service that makes it the right fit (not just "S3 for files, EBS for disks") is what distinguishes a considered architecture from one assembled by habit.


Common Mistakes#

MistakeWhy It's WrongFix
Manually randomizing S3 key prefixes for "performance," based on outdated guidanceModern S3 auto-scales request rate per prefix — this is no longer necessary and adds needless key-naming complexityUse natural, logical key naming; only worry about prefix design for genuinely extreme, sustained request rates
Leaving a bucket without Block Public Access enabled "just in case it's needed later"A single future bucket-policy mistake can then expose data publicly with no structural safety netEnable Block Public Access by default; explicitly disable only for buckets genuinely intended to be public (e.g. static website hosting)
Expecting an EBS volume to survive its Availability Zone failingEBS volumes are tied to a single AZ by physical designTake regular snapshots (stored durably in S3) for AZ-level durability, or use EFS/S3 for data that must survive an AZ loss
Trying to share ordinary EBS volumes across many instances for shared file accessEBS Multi-Attach provides only shared BLOCK access, with no built-in file-level write coordinationUse EFS for genuine multi-instance shared file access
Running a small EFS filesystem under sustained heavy load on the default Bursting throughput modeBurst credits run out, causing throttling under sustained (not just spiky) loadSwitch to Provisioned or Elastic throughput mode for sustained high-throughput needs
Never enabling S3 Versioning on buckets holding genuinely important dataA single accidental (or malicious) delete/overwrite is permanent and unrecoverableEnable Versioning (and consider MFA Delete) on any bucket holding data that must survive accidental loss

Worked Practice Problems#

Problem 1: A team stores application logs in S3, accessed frequently for the first week, occasionally for the first month, and essentially never after that, but must be retained for 3 years for compliance. Design a cost-effective S3 configuration for this.

Answer: An S3 Lifecycle policy on the bucket (or a specific logs/ prefix) with staged transitions: keep objects in S3 Standard for the first ~7 days (frequent access), transition to S3 Standard-IA at day 7 (occasional access, still millisecond retrieval when needed), transition to S3 Glacier Flexible Retrieval or Glacier Deep Archive around day 30-90 once access becomes essentially never, and set an Expiration rule at day 1,095 (3 years) to automatically delete objects once the compliance retention period ends. This fully automates the cost-optimal lifecycle with no ongoing manual intervention, directly matching the access pattern described.

Problem 2: An application team provisions a single, large EBS gp3 volume for a self-hosted database on one EC2 instance, and separately wants three other EC2 instances (in different AZs, for redundancy) to be able to read shared configuration files that change periodically. What storage choice fits each need, and why is EBS not the right fit for the second requirement?

Answer: The database's data files correctly belong on EBS — it needs a low-latency, block-level disk for one specific instance, exactly EBS's core use case. The shared configuration files, however, need genuine multi-instance, cross-AZ concurrent file access with normal file semantics — EBS fundamentally cannot satisfy this (a standard EBS volume attaches to only one instance, and even Multi-Attach requires the same AZ and provides no file-level write coordination). EFS is the correct fit here: a single EFS filesystem with mount targets in each of the three AZs lets all three instances mount and read the same files concurrently, with EFS itself handling the distributed, multi-AZ availability.

Problem 3: A security audit finds that an S3 bucket containing sensitive customer data has no Block Public Access setting enabled, and a bucket policy was recently modified (accidentally, during a deployment change) to grant s3:GetObject to Principal: "*". The team wants to understand both what happened and how to structurally prevent a recurrence, not just fix this one instance.

Answer: What happened is a straightforward, single-point-of-failure security misconfiguration: because Block Public Access wasn't enabled as a safety net, the accidental Principal: "*" bucket policy change took immediate, full effect, making the bucket's contents publicly readable. The immediate fix is correcting the bucket policy and confirming no unauthorized access occurred (via S3 access logs or CloudTrail, Part 9/10). The structural prevention is enabling Block Public Access at the ACCOUNT level (not just per-bucket), which overrides even a future accidental public-granting bucket policy — this converts a single point of failure (one policy document being correct) into a defense-in-depth setup requiring TWO independent mistakes (both the account-level block AND the bucket policy) to actually expose data.

Problem 4: A financial services company needs to retain transaction records for 7 years, with a hard regulatory requirement that NO employee — including administrators — can delete or modify a record before its retention period expires, even accidentally or under insider-threat conditions. Standard S3 Versioning is already enabled. Is Versioning alone sufficient to satisfy this requirement, and if not, what additional feature closes the gap?

Answer: Versioning alone is not sufficient — while it protects against accidental overwrites and provides a recovery path after a delete, a sufficiently privileged administrator can still permanently delete a specific object VERSION (not just add a delete marker) if they have the right IAM permissions, which doesn't satisfy a "structurally impossible for anyone, including admins" requirement. S3 Object Lock in COMPLIANCE mode closes this gap precisely: once applied with a retention date, NO principal — including the root user — can delete or overwrite that object version until the retention period expires, with no override mechanism existing at all. The one operational catch worth flagging: Object Lock must be enabled at bucket CREATION time, so this decision needs to be made before the bucket (or its replacement, if retrofitting) is created, not after.

Problem 5: An organization's Auto Scaling Group launches new instances from a golden AMI backed by a large EBS snapshot, and the team notices new instances perform noticeably slower on disk-heavy operations for their first few minutes after launch, before settling into normal performance. What's the underlying mechanism causing this, and what EBS feature directly addresses it?

Answer: This is the EBS snapshot "lazy loading" behavior — a volume created from a snapshot doesn't have all its data physically present on the new volume immediately; blocks are fetched from the snapshot's underlying S3 storage on first access, meaning the very first read of any given block is measurably slower than a normal, already-resident read, exactly matching the described symptom of early sluggishness that settles down over time. Fast Snapshot Restore directly addresses this by pre-warming the volume ahead of time, ensuring all blocks are already present and fast from the very first read — a genuinely worthwhile feature to enable on the specific AZs and snapshot an Auto Scaling Group's launch template depends on, directly reducing the same cold-start latency problem already covered for EC2 more broadly in Part 3.


Summary and What's Next#

  • AWS offers three distinct storage shapes — S3 (object), EBS (block, single-instance), and EFS (file, multi-instance shared) — matching the right shape to the actual access pattern is the core decision.
  • S3 provides 11 nines of durability and strong read-after-write consistency, with storage classes and Lifecycle policies automating cost-optimal data aging, and Versioning protecting against accidental/malicious deletion.
  • Block Public Access is a structural, account-level safety net that should be enabled by default — the majority of real-world S3 data exposure incidents trace back to a missing version of exactly this control.
  • EBS volumes are tied to a single Availability Zone, requiring snapshots (stored durably in S3) for genuine cross-AZ durability; gp3 is the sensible modern default, with independently scalable IOPS/throughput.
  • EFS provides genuine multi-instance, cross-AZ shared file access with normal POSIX semantics — the right tool whenever multiple instances need true concurrent file access, which EBS structurally cannot provide.
  • FSx fills specialized needs (Windows SMB, Lustre for HPC, ONTAP/OpenZFS) where EFS's standard NFS interface isn't the right fit.

Continue to Part 6 (06-managed-databases-and-data-services.md) to see how these storage primitives underpin AWS's managed database services — RDS, Aurora, and the caching and data warehousing layers built on top of them.