Post

AWS S3 Mastery: Buckets, Objects, Versioning, and Lifecycle Policies

AWS S3 Mastery: Buckets, Objects, Versioning, and Lifecycle Policies

Introduction

Your users are uploading profile pictures, your application is generating daily reports, your backups are piling up, and your log files are consuming gigabytes every hour. Traditional file systems struggle with this scale—running out of disk space, slow network transfers, no built-in redundancy, and manual backup management.

Amazon Simple Storage Service (S3) fundamentally changes how we think about storage. It’s not a file system mounted on a server—it’s an infinitely scalable object store accessible via HTTP, with 99.999999999% (11 nines) durability, automatic redundancy across multiple facilities, and pricing that starts at pennies per gigabyte.

But S3 is more than just “cloud storage.” It’s about understanding buckets and objects, designing access policies, implementing versioning for data protection, automating lifecycle transitions to optimize costs, enabling static website hosting, and integrating with CloudFront for global content delivery.

In this guide, we’ll master S3 from first principles: creating buckets, managing objects, securing access, implementing versioning, automating lifecycle policies, and building production architectures that serve millions of files reliably.

What Is Amazon S3?

S3 is object storage built to store and retrieve any amount of data from anywhere on the web.

Key S3 Characteristics

Feature Description
Object Storage Store files (objects) with metadata, not block/file system
Scalability Unlimited storage capacity
Durability 99.999999999% (11 nines) - data stored redundantly
Availability 99.99% uptime SLA
Global Buckets in specific regions, accessible worldwide

S3 vs Traditional Storage

Aspect Traditional Storage S3
Capacity Fixed (buy more disks) Unlimited
Redundancy RAID, manual backups Automatic across 3+ facilities
Access File system (mount) HTTP API (RESTful)
Scalability Vertical (bigger disks) Horizontal (infinite)
Cost Fixed CapEx Variable OpEx ($0.023/GB/month)

Core S3 Concepts

Buckets

Buckets are containers for objects (like top-level folders).

Bucket rules:

  • Globally unique names: my-app-uploads must be unique across all AWS accounts
  • Region-specific: Created in a specific region
  • Flat structure: No bucket nesting (but objects can have key prefixes simulating folders)

Naming constraints:

  • 3-63 characters
  • Lowercase letters, numbers, hyphens
  • Must start with letter or number
  • No uppercase, spaces, underscores

Objects

Objects are files stored in buckets.

Object anatomy:

1
2
3
4
5
6
7
8
9
Object:
├── Key: "uploads/users/123/profile.jpg"
├── Value: <binary data>
├── Version ID: "abc123" (if versioning enabled)
├── Metadata:
│   ├── Content-Type: image/jpeg
│   ├── Content-Length: 524288
│   └── x-amz-meta-user-id: 123
└── ACL: private (access control)

Object size:

  • Single PUT: Up to 5GB
  • Multipart upload: Up to 5TB

S3 URIs

Objects are accessed via:

  • S3 URI: s3://my-bucket/uploads/file.pdf
  • HTTP URL: https://my-bucket.s3.amazonaws.com/uploads/file.pdf
  • Virtual-hosted style: https://my-bucket.s3.us-east-1.amazonaws.com/uploads/file.pdf

Creating and Managing Buckets

Create Bucket

1
2
3
4
5
6
7
8
9
10
# Create bucket in us-east-1
aws s3api create-bucket \
  --bucket my-app-uploads \
  --region us-east-1

# Create bucket in other regions (requires location constraint)
aws s3api create-bucket \
  --bucket my-app-eu \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1

Enable Versioning

1
2
3
aws s3api put-bucket-versioning \
  --bucket my-app-uploads \
  --versioning-configuration Status=Enabled

Why versioning?

  • Protect against accidental deletions
  • Recover from unintended overwrites
  • Audit history of changes

Enable Server-Side Encryption

1
2
3
4
5
6
7
8
9
10
11
# Encrypt with S3-managed keys (SSE-S3)
aws s3api put-bucket-encryption \
  --bucket my-app-uploads \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      },
      "BucketKeyEnabled": true
    }]
  }'

Block Public Access

1
2
3
4
5
# Block all public access (recommended)
aws s3api put-public-access-block \
  --bucket my-app-uploads \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Uploading and Downloading Objects

Upload Object (CLI)

1
2
3
4
5
6
7
8
9
10
11
12
# Upload file
aws s3 cp local-file.jpg s3://my-app-uploads/uploads/file.jpg

# Upload with metadata
aws s3 cp document.pdf s3://my-app-uploads/docs/document.pdf \
  --metadata user-id=123,uploaded-by=john

# Upload directory recursively
aws s3 cp ./images/ s3://my-app-uploads/images/ --recursive

# Sync directory (only changed files)
aws s3 sync ./backups/ s3://my-app-backups/daily/

Upload Object (SDK - Ruby)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require 'aws-sdk-s3'

s3 = Aws::S3::Client.new(region: 'us-east-1')

# Upload file
s3.put_object(
  bucket: 'my-app-uploads',
  key: 'uploads/profile.jpg',
  body: File.read('/path/to/profile.jpg'),
  content_type: 'image/jpeg',
  metadata: {
    'user-id' => '123',
    'uploaded-at' => Time.now.iso8601
  }
)

# Generate presigned URL (temporary upload URL)
signer = Aws::S3::Presigner.new
url = signer.presigned_url(
  :put_object,
  bucket: 'my-app-uploads',
  key: 'uploads/user-photo.jpg',
  expires_in: 3600  # 1 hour
)

# User uploads directly to S3 using this URL
# No traffic through your server!

Download Object

1
2
3
4
5
6
7
8
9
# Download file
aws s3 cp s3://my-app-uploads/reports/report.pdf ./report.pdf

# Download specific version
aws s3api get-object \
  --bucket my-app-uploads \
  --key reports/report.pdf \
  --version-id abc123 \
  report-v1.pdf

List Objects

1
2
3
4
5
6
7
8
# List all objects
aws s3 ls s3://my-app-uploads/ --recursive

# List with prefix (simulating folder)
aws s3 ls s3://my-app-uploads/uploads/users/

# List with human-readable sizes
aws s3 ls s3://my-app-uploads/ --recursive --human-readable --summarize

S3 Storage Classes

S3 offers multiple storage classes optimized for different access patterns and costs.

Storage Class Use Case Durability Availability Retrieval Cost/GB/month
S3 Standard Frequently accessed 11 nines 99.99% Instant $0.023
S3 Intelligent-Tiering Unknown/changing patterns 11 nines 99.9% Instant $0.023 + monitoring
S3 Standard-IA Infrequent access 11 nines 99.9% Instant $0.0125 + retrieval fee
S3 One Zone-IA Infrequent, non-critical 11 nines* 99.5% Instant $0.01 + retrieval fee
S3 Glacier Instant Archive, instant access 11 nines 99.9% Instant $0.004 + retrieval fee
S3 Glacier Flexible Archive, rare access 11 nines 99.99% Minutes-hours $0.0036 + retrieval fee
S3 Glacier Deep Archive Long-term archive 11 nines 99.99% 12 hours $0.00099

*One Zone-IA stores data in single AZ (not 3+), lower durability if AZ fails

When to Use Each Class

S3 Standard:

  • User-uploaded content (profile photos, documents)
  • Active application assets
  • Frequently accessed data

S3 Standard-IA:

  • Backups accessed occasionally
  • Disaster recovery files
  • Data accessed < once/month

S3 Glacier Flexible:

  • Compliance archives (7-year retention)
  • Old logs/backups
  • Data accessed < once/year

S3 Glacier Deep Archive:

  • Legal hold data
  • Regulatory archives (10+ year retention)
  • Lowest cost long-term storage

Change Storage Class

1
2
3
4
5
6
# Move object to Standard-IA
aws s3api copy-object \
  --bucket my-app-uploads \
  --copy-source my-app-uploads/old-file.pdf \
  --key old-file.pdf \
  --storage-class STANDARD_IA

Lifecycle Policies

Lifecycle policies automatically transition objects between storage classes or delete them.

Example: Optimize Costs

Goal: Reduce storage costs for user uploads

  • Keep recent uploads (< 30 days) in Standard
  • Move older uploads (30-90 days) to Standard-IA
  • Move old uploads (> 90 days) to Glacier
  • Delete after 7 years
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "Rules": [
    {
      "Id": "UserUploadsLifecycle",
      "Status": "Enabled",
      "Prefix": "uploads/",
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ],
      "Expiration": {
        "Days": 2555
      }
    }
  ]
}

Apply lifecycle policy:

1
2
3
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-app-uploads \
  --lifecycle-configuration file://lifecycle.json

Example: Delete Old Logs

1
2
3
4
5
6
7
8
9
10
{
  "Rules": [{
    "Id": "DeleteOldLogs",
    "Status": "Enabled",
    "Prefix": "logs/",
    "Expiration": {
      "Days": 30
    }
  }]
}

Result: Logs automatically deleted after 30 days, no manual cleanup.

Example: Clean Up Incomplete Multipart Uploads

1
2
3
4
5
6
7
8
9
{
  "Rules": [{
    "Id": "CleanupIncompleteUploads",
    "Status": "Enabled",
    "AbortIncompleteMultipartUpload": {
      "DaysAfterInitiation": 7
    }
  }]
}

Why? Incomplete multipart uploads consume storage and incur costs.

Versioning

Versioning keeps multiple variants of an object in the same bucket.

Enable Versioning

1
2
3
aws s3api put-bucket-versioning \
  --bucket my-app-uploads \
  --versioning-configuration Status=Enabled

How Versioning Works

1
2
3
4
5
6
7
8
9
10
# Upload file (version 1)
aws s3 cp file.txt s3://my-bucket/file.txt
# Version ID: abc123

# Upload file again (version 2)
aws s3 cp file.txt s3://my-bucket/file.txt
# Version ID: def456

# List all versions
aws s3api list-object-versions --bucket my-bucket --prefix file.txt

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "Versions": [
    {
      "Key": "file.txt",
      "VersionId": "def456",
      "IsLatest": true,
      "LastModified": "2024-01-15T10:00:00Z"
    },
    {
      "Key": "file.txt",
      "VersionId": "abc123",
      "IsLatest": false,
      "LastModified": "2024-01-10T10:00:00Z"
    }
  ]
}

Recover Deleted File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# "Delete" file (creates delete marker)
aws s3 rm s3://my-bucket/file.txt

# File appears deleted
aws s3 ls s3://my-bucket/
# (empty)

# But versions still exist!
aws s3api list-object-versions --bucket my-bucket --prefix file.txt

# Restore by downloading specific version
aws s3api get-object \
  --bucket my-bucket \
  --key file.txt \
  --version-id def456 \
  restored-file.txt

# Or delete the delete marker
aws s3api delete-object \
  --bucket my-bucket \
  --key file.txt \
  --version-id <delete-marker-id>

Lifecycle with Versioning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "Rules": [{
    "Id": "ArchiveOldVersions",
    "Status": "Enabled",
    "NoncurrentVersionTransitions": [
      {
        "NoncurrentDays": 30,
        "StorageClass": "STANDARD_IA"
      }
    ],
    "NoncurrentVersionExpiration": {
      "NoncurrentDays": 90
    }
  }]
}

Result: Old versions transition to IA after 30 days, deleted after 90 days.

S3 Access Control

Bucket Policies

Bucket policies are JSON-based access policies attached to buckets.

Example: Public read access for website assets

1
2
3
4
5
6
7
8
9
10
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PublicReadGetObject",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-website-bucket/public/*"
  }]
}
1
2
3
aws s3api put-bucket-policy \
  --bucket my-website-bucket \
  --policy file://policy.json

Example: Allow CloudFront access only

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": "cloudfront.amazonaws.com"
    },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-app-assets/*",
    "Condition": {
      "StringEquals": {
        "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
      }
    }
  }]
}

Presigned URLs

Presigned URLs grant temporary access to private objects.

Ruby example:

1
2
3
4
5
6
7
8
9
10
require 'aws-sdk-s3'

s3 = Aws::S3::Resource.new(region: 'us-east-1')
obj = s3.bucket('my-app-uploads').object('private/document.pdf')

# Generate URL valid for 1 hour
url = obj.presigned_url(:get, expires_in: 3600)

# Share URL with user
# URL expires after 1 hour

Use cases:

  • Temporary download links for paid content
  • Time-limited file sharing
  • Direct uploads from browser (upload presigned URL)

S3 Static Website Hosting

Host static websites directly from S3.

Enable Website Hosting

1
2
3
aws s3 website s3://my-website-bucket/ \
  --index-document index.html \
  --error-document error.html

Upload Website Files

1
2
3
4
5
# Upload with correct content types
aws s3 cp ./public/ s3://my-website-bucket/ \
  --recursive \
  --exclude "*.DS_Store" \
  --cache-control "max-age=3600"

Bucket Policy for Public Access

1
2
3
4
5
6
7
8
9
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-website-bucket/*"
  }]
}

Website endpoint:

1
http://my-website-bucket.s3-website-us-east-1.amazonaws.com

Pro tip: Use CloudFront + custom domain for production (HTTPS, caching, performance).

S3 Event Notifications

Trigger actions when objects are created, deleted, or accessed.

Example: Process Uploaded Images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Configure S3 to send events to Lambda
aws s3api put-bucket-notification-configuration \
  --bucket my-app-uploads \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [{
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ProcessImage",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [{
            "Name": "prefix",
            "Value": "uploads/images/"
          }]
        }
      }
    }]
  }'

Workflow:

1
User uploads image → S3 → Lambda function → Resize image → Save thumbnail to S3

Other targets:

  • SNS (send notification)
  • SQS (queue for processing)
  • EventBridge (complex routing)

Multipart Upload

For files > 100MB, use multipart upload for reliability and speed.

Ruby Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require 'aws-sdk-s3'

s3 = Aws::S3::Client.new(region: 'us-east-1')

# Initiate multipart upload
resp = s3.create_multipart_upload(
  bucket: 'my-app-uploads',
  key: 'large-file.zip'
)
upload_id = resp.upload_id

# Upload parts (in parallel)
parts = []
file_size = File.size('large-file.zip')
part_size = 10 * 1024 * 1024  # 10MB per part

File.open('large-file.zip', 'rb') do |file|
  part_number = 1
  while (chunk = file.read(part_size))
    part_resp = s3.upload_part(
      bucket: 'my-app-uploads',
      key: 'large-file.zip',
      part_number: part_number,
      upload_id: upload_id,
      body: chunk
    )
    parts << { etag: part_resp.etag, part_number: part_number }
    part_number += 1
  end
end

# Complete multipart upload
s3.complete_multipart_upload(
  bucket: 'my-app-uploads',
  key: 'large-file.zip',
  upload_id: upload_id,
  multipart_upload: { parts: parts }
)

Benefits:

  • Resume failed uploads
  • Parallel uploads (faster)
  • Upload files > 5GB (required for > 5GB)

S3 Performance Optimization

Request Rate

S3 supports 3,500 PUT/POST/DELETE and 5,500 GET/HEAD requests per second per prefix.

Prefix examples:

  • s3://bucket/uploads/2024/01/file.jpg → prefix: uploads/2024/01/
  • s3://bucket/logs/app-1/log.txt → prefix: logs/app-1/

Strategy: Distribute objects across multiple prefixes for higher throughput.

Bad (single prefix):

1
2
3
4
uploads/file1.jpg
uploads/file2.jpg
uploads/file3.jpg
→ Limited to 3,500 PUT/s

Good (distributed prefixes):

1
2
3
4
uploads/2024-01-15/file1.jpg
uploads/2024-01-16/file2.jpg
uploads/2024-01-17/file3.jpg
→ 3,500 PUT/s per date prefix = 10,500 PUT/s

Transfer Acceleration

S3 Transfer Acceleration uses CloudFront edge locations for faster uploads.

1
2
3
4
5
6
7
8
9
# Enable on bucket
aws s3api put-bucket-accelerate-configuration \
  --bucket my-app-uploads \
  --accelerate-configuration Status=Enabled

# Upload using accelerated endpoint
aws s3 cp large-file.zip \
  s3://my-app-uploads/large-file.zip \
  --endpoint-url https://my-app-uploads.s3-accelerate.amazonaws.com

Speed improvement: 50-500% faster for long-distance transfers.

Real-World Architecture: User-Generated Content Platform

Requirements

  • Users upload photos/videos
  • Generate thumbnails automatically
  • Store originals long-term
  • Serve via CDN
  • Optimize costs

Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
User Upload
  │
  ▼
S3 Bucket (uploads/)
  │
  ├─→ S3 Event → Lambda → Resize → S3 (thumbnails/)
  │
  ├─→ Lifecycle Policy:
  │   ├─ Day 0-30: Standard
  │   ├─ Day 30-365: Standard-IA
  │   └─ Day 365+: Glacier
  │
  └─→ CloudFront CDN (global delivery)

Implementation

1. S3 bucket with versioning:

1
2
3
aws s3api create-bucket --bucket user-content-platform
aws s3api put-bucket-versioning --bucket user-content-platform \
  --versioning-configuration Status=Enabled

2. Lifecycle policy:

1
2
3
4
5
6
7
8
9
10
11
{
  "Rules": [{
    "Id": "OptimizeStorageCosts",
    "Status": "Enabled",
    "Prefix": "uploads/originals/",
    "Transitions": [
      {"Days": 30, "StorageClass": "STANDARD_IA"},
      {"Days": 365, "StorageClass": "GLACIER"}
    ]
  }]
}

3. Event notification for processing:

1
2
3
aws s3api put-bucket-notification-configuration \
  --bucket user-content-platform \
  --notification-configuration file://notification.json

4. CloudFront distribution (covered in CloudFront guide)

Cost savings:

  • Standard (0-30 days): $0.023/GB
  • Standard-IA (30-365 days): $0.0125/GB
  • Glacier (365+ days): $0.004/GB

For 100TB of user content:

  • All Standard: $2,300/month
  • With lifecycle: ~$600/month (74% savings)

Security Best Practices

1. Block Public Access by Default

1
2
3
4
aws s3api put-public-access-block \
  --bucket my-bucket \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

2. Enable Encryption

1
2
3
4
5
6
7
8
9
10
# SSE-S3 (S3-managed keys - free)
aws s3api put-bucket-encryption \
  --bucket my-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }]
  }'

3. Enable Access Logging

1
2
3
4
5
6
7
8
aws s3api put-bucket-logging \
  --bucket my-bucket \
  --bucket-logging-status '{
    "LoggingEnabled": {
      "TargetBucket": "my-logs-bucket",
      "TargetPrefix": "s3-access-logs/"
    }
  }'

4. Use IAM Roles, Not Access Keys

For EC2/Lambda accessing S3, use IAM roles (no hardcoded credentials).

Conclusion

AWS S3 transforms storage from a capacity-constrained, manually managed resource into an infinitely scalable, automatically replicated, cost-optimized utility. By mastering buckets, objects, versioning, lifecycle policies, and access control, you architect storage solutions that scale from gigabytes to petabytes without infrastructure changes.

The shift from fixed storage to elastic object storage, from manual backups to automatic versioning, and from flat-rate pricing to lifecycle-optimized costs transforms storage from a constraint into an enabler.

Start simple: create a bucket, upload files, secure access. Then evolve: implement versioning, automate lifecycle transitions, integrate with Lambda for processing, distribute via CloudFront. Every iteration makes your storage more resilient, more cost-effective, and more integrated with your application architecture.

Master S3, and you master cloud storage.

Suggested Reading

This post is licensed under CC BY 4.0 by the author.