You know that feeling when you visit a website and it loads so fast you question whether your internet is suddenly working properly for once?
That's the CloudFront effect.
Imagine this: Your website's files sitting in one place (S3 bucket in, say, us-east-1), but being served from locations near your users all over the world - Nairobi, London, Tokyo, São Paulo. Someone in Mombasa hits your site, and CloudFront serves it from the nearest edge location in milliseconds.
It's like having a kiosk in every matatu stage in the world, all selling your product. Customer walks up, grabs what they need, pays the same price, gets it instantly. That's CloudFront - a content delivery network (CDN) that makes your website stupid fast globally.
Today we're building a complete production setup: static website on S3, distributed via CloudFront, with custom domain and HTTPS. This is how the pros do it.
What We're Building
A static website hosting setup with:
- S3 bucket for storage
- CloudFront distribution for global delivery
- Custom domain (optional) with Route 53
- HTTPS/SSL certificate (free via AWS Certificate Manager)
- Cache optimization for performance
- Security best practices with Origin Access Control (OAC)
This builds on S3 object storage fundamentals - read that first if you're new to S3.
Why S3 + CloudFront Instead of Regular Hosting?
Speed: CloudFront has 400+ edge locations worldwide. Your site is cached close to users, reducing latency from hundreds of milliseconds to single-digit milliseconds.
Scalability: Your site goes viral? CloudFront handles it. No server crashes, no capacity planning. AWS scales automatically.
Cost: S3 storage is dirt cheap ($0.023/GB/month). CloudFront free tier gives you 1TB data transfer/month free for a year. After that, it's still cheap compared to traditional hosting.
Security: Built-in DDoS protection, HTTPS by default, and you can restrict S3 access so only CloudFront can serve files.
Reliability: AWS's 99.99% uptime SLA. Your site stays up even if an entire AWS region goes down.
Prerequisites
- AWS account
- Domain name (optional, but recommended)
- Static website files (HTML, CSS, JS, images)
- AWS CLI installed and configured
# Verify AWS CLI is configured
aws sts get-caller-identity
Step 1: Create and Configure S3 Bucket
Create an S3 bucket for your website:
# Replace 'my-awesome-website' with your unique bucket name
BUCKET_NAME="my-awesome-website"
REGION="us-east-1"
aws s3api create-bucket \
--bucket $BUCKET_NAME \
--region $REGION
Important: Bucket names must be globally unique. If you get an error, try a different name.
Enable versioning (optional, but useful for rollbacks):
aws s3api put-bucket-versioning \
--bucket $BUCKET_NAME \
--versioning-configuration Status=Enabled
Step 2: Upload Website Files
Upload your static website files:
# If your website is in a 'dist' or 'build' folder
aws s3 sync ./dist s3://$BUCKET_NAME/ \
--delete \
--cache-control "max-age=31536000" \
--exclude "*.html" \
--exclude "*.xml"
# Upload HTML files with shorter cache (they change more often)
aws s3 sync ./dist s3://$BUCKET_NAME/ \
--exclude "*" \
--include "*.html" \
--include "*.xml" \
--cache-control "max-age=3600" \
--content-type "text/html"
Why different cache settings?
- Static assets (CSS, JS, images) rarely change - cache for 1 year
- HTML files change frequently - cache for 1 hour
- This gives you speed without serving stale content
Set up a simple test website if you don't have one:
# Create a simple HTML file
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Awesome Website</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
text-align: center;
padding: 2rem;
}
h1 { font-size: 3rem; margin: 0; }
p { font-size: 1.5rem; opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 It Works!</h1>
<p>Your S3 + CloudFront setup is live</p>
<p><small>Served via AWS CloudFront CDN</small></p>
</div>
</body>
</html>
EOF
# Upload to S3
aws s3 cp index.html s3://$BUCKET_NAME/index.html \
--content-type "text/html" \
--cache-control "max-age=3600"
Step 3: Create CloudFront Distribution
This is where the magic happens. We'll create a CloudFront distribution that serves your S3 content globally.
First, create an Origin Access Control (OAC) - this is the secure way to allow CloudFront to access your S3 bucket:
# Create OAC configuration
OAC_CONFIG=$(aws cloudfront create-origin-access-control \
--origin-access-control-config \
Name="${BUCKET_NAME}-oac",\
Description="OAC for ${BUCKET_NAME}",\
SigningProtocol=sigv4,\
SigningBehavior=always,\
OriginAccessControlOriginType=s3 \
--query 'OriginAccessControl.Id' \
--output text)
echo "OAC ID: $OAC_CONFIG"
Create the CloudFront distribution:
# Create distribution configuration
cat > distribution-config.json << EOF
{
"CallerReference": "$(date +%s)",
"Comment": "CDN for $BUCKET_NAME",
"Enabled": true,
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "S3-$BUCKET_NAME",
"DomainName": "$BUCKET_NAME.s3.${REGION}.amazonaws.com",
"OriginAccessControlId": "$OAC_CONFIG",
"S3OriginConfig": {
"OriginAccessIdentity": ""
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3-$BUCKET_NAME",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
}
},
"Compress": true,
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"MinTTL": 0,
"DefaultTTL": 86400,
"MaxTTL": 31536000
},
"DefaultRootObject": "index.html",
"CustomErrorResponses": {
"Quantity": 2,
"Items": [
{
"ErrorCode": 403,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
},
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
}
]
},
"PriceClass": "PriceClass_100"
}
EOF
# Create the distribution
DISTRIBUTION_ID=$(aws cloudfront create-distribution \
--distribution-config file://distribution-config.json \
--query 'Distribution.Id' \
--output text)
echo "Distribution ID: $DISTRIBUTION_ID"
# Get the CloudFront domain name
CLOUDFRONT_DOMAIN=$(aws cloudfront get-distribution \
--id $DISTRIBUTION_ID \
--query 'Distribution.DomainName' \
--output text)
echo "Your CloudFront URL: https://$CLOUDFRONT_DOMAIN"
What's happening here?
redirect-to-https: Automatically redirects HTTP to HTTPSCompress: true: Enables gzip/brotli compressionDefaultRootObject: Serves index.html for root requestsCustomErrorResponses: Enables client-side routing for SPAsPriceClass_100: Uses North America and Europe edge locations (cheapest)
Step 4: Update S3 Bucket Policy
CloudFront needs permission to access your S3 bucket. Get the policy from CloudFront:
# Get the bucket policy statement from CloudFront
aws cloudfront get-distribution-config --id $DISTRIBUTION_ID \
--query 'DistributionConfig.Origins.Items[0].S3OriginConfig' \
--output json
Create and apply the bucket policy:
cat > bucket-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::${BUCKET_NAME}/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::$(aws sts get-caller-identity --query Account --output text):distribution/${DISTRIBUTION_ID}"
}
}
}
]
}
EOF
aws s3api put-bucket-policy \
--bucket $BUCKET_NAME \
--policy file://bucket-policy.json
Security win: Your S3 bucket is now only accessible via CloudFront, not directly. This prevents bandwidth theft and ensures all traffic goes through your CDN.
Step 5: Configure Custom Domain and HTTPS (Optional)
Want to use your own domain instead of the CloudFront URL? Here's how:
Request SSL certificate:
# Replace with your domain
DOMAIN="yourdomain.com"
# Request certificate (must be in us-east-1 for CloudFront)
CERT_ARN=$(aws acm request-certificate \
--domain-name $DOMAIN \
--subject-alternative-names "www.$DOMAIN" \
--validation-method DNS \
--region us-east-1 \
--query 'CertificateArn' \
--output text)
echo "Certificate ARN: $CERT_ARN"
# Get DNS validation records
aws acm describe-certificate \
--certificate-arn $CERT_ARN \
--region us-east-1 \
--query 'Certificate.DomainValidationOptions' \
--output table
Add the DNS validation records to your domain's DNS settings. Once validated (can take 5-30 minutes), update your CloudFront distribution:
# Update distribution with custom domain
aws cloudfront update-distribution \
--id $DISTRIBUTION_ID \
--distribution-config '{
"Aliases": {
"Quantity": 2,
"Items": ["'$DOMAIN'", "www.'$DOMAIN'"]
},
"ViewerCertificate": {
"ACMCertificateArn": "'$CERT_ARN'",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
}
}'
Update DNS records:
If using Route 53:
# Get hosted zone ID
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name \
--dns-name $DOMAIN \
--query "HostedZones[0].Id" \
--output text)
# Create Route 53 record
cat > dns-record.json << EOF
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "$DOMAIN",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "$CLOUDFRONT_DOMAIN",
"EvaluateTargetHealth": false
}
}
}
]
}
EOF
aws route53 change-resource-record-sets \
--hosted-zone-id $HOSTED_ZONE_ID \
--change-batch file://dns-record.json
Step 6: Optimize Performance
Enable HTTP/2 and HTTP/3:
CloudFront automatically supports HTTP/2. For HTTP/3 (QUIC):
aws cloudfront update-distribution \
--id $DISTRIBUTION_ID \
--distribution-config '{
"HttpVersion": "http2and3"
}'
Set up cache invalidation for deployments:
When you update your site, invalidate the cache:
# Invalidate everything (costs $0 for first 1000 paths/month)
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
# Invalidate specific files
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/index.html" "/css/*" "/js/*"
Automated deployment script:
#!/bin/bash
# deploy.sh - Deploy and invalidate cache
set -e
BUCKET_NAME="my-awesome-website"
DISTRIBUTION_ID="YOUR_DISTRIBUTION_ID"
echo "Building website..."
npm run build # or your build command
echo "Uploading to S3..."
aws s3 sync ./dist s3://$BUCKET_NAME/ --delete
echo "Invalidating CloudFront cache..."
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
echo "✅ Deployment complete!"
echo "🌍 Site will update globally in 1-2 minutes"
Performance Metrics
After setup, you should see:
Before (regular hosting):
- TTFB (Time to First Byte): 300-800ms
- Full page load: 2-4 seconds
- Global latency: varies wildly by location
After (S3 + CloudFront):
- TTFB: 20-100ms (edge location)
- Full page load: 0.5-1.5 seconds
- Global latency: consistent worldwide
Test your site's performance:
- WebPageTest.org
- GTmetrix
- Chrome DevTools Network tab
Cost Breakdown (2025 Pricing)
S3 Storage:
- $0.023 per GB/month
- For 1GB website: $0.023/month
S3 Data Transfer:
- First 1GB/month: Free
- Data transfer to CloudFront: Free (zero cost)
CloudFront:
- First 1TB/month (12 months free tier): $0
- After free tier: $0.085 per GB
- 10TB/month: $850 (but you'd be making serious money at that traffic)
Example: Small to Medium Site
- 500MB website size
- 50,000 pageviews/month
- Average 2MB per pageview (after caching)
- Total: ~100GB transferred
Monthly cost:
- S3 storage: $0.01
- S3 requests: $0.02
- CloudFront (after free tier): $8.50
- Total: ~$8.53/month for 50K pageviews
Compare that to most hosting providers charging $10-30/month for far worse performance.
Learn more about cloud cost optimization strategies to minimize CDN costs.
Common Issues and Fixes
Problem: "Access Denied" errors Solution: Check bucket policy. Ensure CloudFront OAC has proper permissions.
Problem: Old content still showing after upload Solution: CloudFront is caching. Create cache invalidation.
Problem: Custom domain not working Solution: Verify DNS propagation (can take 24-48 hours), check certificate validation, ensure distribution has correct aliases.
Problem: Slow invalidations Solution: Invalidations take 5-15 minutes. For instant updates, use versioned filenames (style.v123.css) instead of invalidating.
Problem: 403 on SPA routes Solution: Configure custom error responses to redirect 403/404 to index.html (already in our setup).
This setup is perfect for hosting Progressive Web Apps with offline capabilities and app-like experiences.
Best Practices
1. Use versioned filenames: Instead of app.js, use app.v1234.js. Update HTML to reference new version. No cache invalidation needed.
2. Separate cache policies: Long cache for assets (1 year), short cache for HTML (1 hour).
3. Enable compression: Gzip/Brotli compression (already enabled in our setup).
4. Use security headers: Add security headers via Lambda@Edge or CloudFront Functions.
5. Monitor costs: Set up billing alerts in AWS.
6. Use Route 53: Seamless integration with CloudFront for domains.
Next Steps
Add CI/CD: Integrate deployment with GitHub Actions or AWS CodePipeline for automatic deploys on git push.
Implement preview environments: Deploy feature branches to separate CloudFront distributions for testing.
Add Lambda@Edge: Run code at CloudFront edge locations for dynamic behavior (A/B testing, authentication, URL rewrites). Pair your static frontend with a serverless API backend using Lambda.
Set up monitoring: CloudWatch alarms for 4xx/5xx errors, cache hit rate monitoring.
Optimize images: Use CloudFront's automatic image optimization or integrate with services like Cloudinary.
The Bottom Line
You now have a professional static website hosting setup that:
- Loads in milliseconds globally
- Scales to millions of visitors automatically
- Costs pennies for small sites
- Has enterprise-grade security and reliability
- Uses HTTPS by default
This is the same setup used by companies serving billions of requests. It's production-ready, battle-tested, and ridiculously fast.
Your users in Nairobi, New York, and New Delhi will all get the same lightning-fast experience. That's the power of CloudFront.
Now go deploy something beautiful.
Sources: