S3
03 / 08

Static Hosting & CloudFront

Static Hosting & CloudFront

S3 static website hosting combined with CloudFront is the standard low-cost, high-performance deployment for SPAs and static sites. Using OAC (Origin Access Control) keeps the S3 bucket private while CloudFront serves it globally over HTTPS.

S3 Static Website Hosting

# Enable static website hosting
aws s3 website s3://my-website-bucket \
  --index-document index.html \
  --error-document 404.html

# Or via API (for more control)
aws s3api put-bucket-website \
  --bucket my-website-bucket \
  --website-configuration '{
    "IndexDocument": {"Suffix": "index.html"},
    "ErrorDocument": {"Key": "404.html"},
    "RoutingRules": [
      {
        "Condition": {"HttpErrorCodeReturnedEquals": "404"},
        "Redirect": {"ReplaceKeyWith": "index.html"}
      }
    ]
  }'

# SPA routing fix: redirect 404 to index.html (React Router, Vue Router etc.)
# In routing rules above, redirect 404s to index.html
# OR use CloudFront custom error response (preferred — more control)

# Get the website endpoint
aws s3api get-bucket-website --bucket my-website-bucket
# Endpoint: http://my-website-bucket.s3-website-us-east-1.amazonaws.com
# NOTE: S3 website endpoint does NOT support HTTPS — use CloudFront for HTTPS

# Deploy a React/Vue/Next.js static build
npm run build
aws s3 sync ./dist s3://my-website-bucket/ \
  --delete \
  --cache-control "max-age=31536000,immutable" \
  --exclude "index.html"
# Upload index.html separately with no-cache
aws s3 cp ./dist/index.html s3://my-website-bucket/index.html \
  --cache-control "no-cache,no-store,must-revalidate" \
  --content-type "text/html"

CloudFront Distribution Setup

# Create a CloudFront distribution with S3 origin (using OAC — recommended)
# Step 1: Create OAC (Origin Access Control)
aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "my-website-oac",
    "Description": "OAC for my website bucket",
    "SigningProtocol": "sigv4",
    "SigningBehavior": "always",
    "OriginAccessControlOriginType": "s3"
  }'

# Step 2: Create distribution (simplified — use Console or CloudFormation for full config)
aws cloudfront create-distribution \
  --distribution-config file://cf-config.json

# cf-config.json key fields:
# {
#   "Origins": {
#     "Items": [{
#       "Id": "s3-origin",
#       "DomainName": "my-website-bucket.s3.us-east-1.amazonaws.com",
#       "S3OriginConfig": {"OriginAccessIdentity": ""},
#       "OriginAccessControlId": "<oac-id>"
#     }]
#   },
#   "DefaultCacheBehavior": {
#     "ViewerProtocolPolicy": "redirect-to-https",
#     "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",  // CachingOptimized
#     "Compress": true
#   },
#   "CustomErrorResponses": {
#     "Items": [{"ErrorCode": 404, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 300}]
#   },
#   "Aliases": {"Items": ["www.example.com"]},
#   "ViewerCertificate": {"AcmCertificateArn": "<cert-arn>", "SslSupportMethod": "sni-only"}
# }

# Invalidate CloudFront cache (after deploy)
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/*"

# Targeted invalidation (faster + cheaper — $0.005 per 1k paths after 1k free/mo)
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/index.html" "/assets/app.*.js"

# Route 53 custom domain → CloudFront (A record alias)
# In Route 53: Create A record with "Alias to CloudFront distribution"
# Alias records are free; route to CloudFront distribution domain (xxx.cloudfront.net)

CloudFront Cache Policies & Behaviors

# AWS managed cache policies (use these before creating custom ones)
# CachingOptimized:     TTL 1 day-1 year; compresses; best for immutable assets
# CachingDisabled:      No caching; good for API origins
# CachingOptimizedForUncompressedObjects: like Optimized but no compression

# Managed policy IDs:
# CachingOptimized:    658327ea-f89d-4fab-a63d-7e88639e58f6
# CachingDisabled:     4135ea2d-6df8-44a3-9df3-4b5a84be39ad

# Multiple behaviors — route by path pattern
# Default (*): S3 origin → CachingOptimized → serve static files
# /api/*:       ALB origin → CachingDisabled → proxy to backend (no caching)

# Check cache hit rate in CloudWatch
# Metric: CacheHitRate in namespace AWS/CloudFront
# Low hit rate: check Vary headers, Cache-Control headers, query string forwarding

# Real-time logs → Kinesis Data Streams (for live analysis)
# Standard access logs → S3 (15 min delay, free)

# Geo restriction — block/allow by country
aws cloudfront update-distribution \
  --id EDFDVBD6EXAMPLE \
  --distribution-config file://updated-config.json
# In config: "Restrictions": {"GeoRestriction": {"RestrictionType": "blacklist", "Locations": ["CN","RU"]}}

# Signed URLs vs Signed Cookies:
# Signed URL:    restrict access to individual files (e.g. paid video files)
# Signed Cookie: restrict access to multiple files (e.g. all files in a subscription tier)

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free