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