TL;DR
Learn how to host your static website on Amazon S3, configure permissions, and set up custom domains with CloudFront.
Hosting Static Websites on AWS S3 with CloudFront: A Comprehensive Guide
$1
This guide provides a detailed walkthrough for hosting static websites on AWS S3, leveraging CloudFront for global content delivery, HTTPS, custom domains, and performance optimization. It covers S3 bucket configuration, content uploading, CloudFront distribution setup, Route 53 DNS management, security best practices, cost considerations, and common troubleshooting scenarios. The recommended architecture prioritizes scalability, security, and cost-efficiency for modern static web applications.
---
$1
This document outlines the process of deploying and managing a static website using Amazon S3 for storage and CloudFront as a Content Delivery Network (CDN). This approach offers significant advantages in terms of availability, scalability, performance, and cost-effectiveness compared to traditional web hosting methods. We will cover the entire lifecycle, from initial setup to advanced configurations and ongoing maintenance.
$1
$1
| Requirement | Purpose | Notes |
| :---------------- | :------------------------------------ | :------------------------------------------ |
| AWS Account | Access to AWS services | Free tier eligibility applies for many services |
| AWS CLI v2 | Command-line interface for AWS | Recommended for automation and scripting |
| Domain Name | Custom URL for your website | Can be managed via Route 53 or external registrars |
| Static Website | HTML, CSS, JavaScript, image files | Minification and optimization are key |
| Index Document | index.html or equivalent | Required for S3 static website hosting |
| Error Document | error.html or equivalent | For handling 404s and other errors |
$1
| File Type | Required | Best Practice |
| :------------- | :------- | :------------------------------------------------------ |
| HTML | Yes | Minified, compressed (e.g., Gzip, Brotli) |
| CSS | Yes | Bundled, minified, compressed |
| JavaScript | Yes | Bundled, minified, compressed, consider code splitting |
| Images | Yes | Optimized (e.g., WebP, JPEG), compressed |
| Fonts | Optional | WOFF2 format for modern browsers, consider self-hosting |
---
$1
This section details the creation and configuration of an S3 bucket to serve static website content.
$1
AWS CLI:
`` aws s3api create-bucket \
--bucket your-unique-website-bucket-name \
--region us-east-1 \
--create-bucket-configuration LocationConstraint=us-east-1 # Omit for us-east-1
bash
`
AWS Management Console:
Navigate to S3, click "Create bucket," provide a globally unique name, select a region, and choose to block all public access (this will be reconfigured via bucket policy for website hosting).
$1
Configure the bucket to serve files as a website.
AWS CLI:
` aws s3api put-bucket-website \
--bucket your-unique-website-bucket-name \
--website-configuration '{
"IndexDocument": {"Suffix": "index.html"},
"ErrorDocument": {"Key": "error.html"}
}'
bash
`
AWS Management Console:
In the bucket's properties, find "Static website hosting," enable it, specify index.html as the Index document, and error.html as the Error document. Note the Endpoint URL provided by S3; this is crucial for initial testing but will be superseded by CloudFront.
$1
To allow unauthenticated access to your website files, apply a bucket policy.
AWS CLI:
` aws s3api put-bucket-policy \
--bucket your-unique-website-bucket-name \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-unique-website-bucket-name/*"
}
]
}'
bash
`
Important: This policy makes objects publicly readable. For production environments using CloudFront, it's recommended to restrict direct S3 access and use CloudFront Origin Access Identity (OAI) or Origin Access Control (OAC) for more secure access. This public policy is for simplicity in the initial S3 website endpoint testing phase.
$1
If your website needs to access resources from different domains (e.g., APIs), configure Cross-Origin Resource Sharing (CORS).
AWS CLI:
` aws s3api put-bucket-cors \
--bucket your-unique-website-bucket-name \
--cors-configuration '{
"CORSRules": [
{
"AllowedOrigins": ["http://localhost:3000", "https://your-domain.com"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000,
"AllowedHeaders": ["*"]
}
]
}'
bash
`
Note: Adjust AllowedOrigins and AllowedMethods to your specific security requirements.
---
$1
Transfer your static assets to the configured S3 bucket.
$1
A typical project structure:
` your-website-root/
├── index.html
├── error.html
├── assets/
│ ├── css/
│ │ └── styles.min.css
│ ├── js/
│ │ └── main.bundle.js
│ └── images/
│ └── logo.webp
├── about/
│ └── index.html # Accessible at /about/
└── contact/
└── index.html # Accessible at /contact/
`
Key Principle: S3 treats objects as files. The directory structure in your upload maps directly to URL paths. For example, about/index.html becomes accessible at your-bucket-url/about/.
$1
The aws s3 sync command is efficient for uploading. It synchronizes the local directory with the S3 bucket, uploading only new or modified files.
` cd your-website-root/ aws s3 sync . s3://your-unique-website-bucket-name \
--acl public-read \ # If using S3 website endpoint directly (not recommended with CloudFront OAC/OAI)
--exclude "*.DS_Store" \
--exclude "node_modules/*" \
--cache-control "max-age=3600" \ # Default cache for most files
--content-type "text/html" --exclude "" --include ".html" \
--content-type "text/css" --exclude "" --include ".css" \
--content-type "application/javascript" --exclude "" --include ".js" \
--content-type "image/webp" --exclude "" --include ".webp" \
--content-type "image/jpeg" --exclude "" --include "*.jpg" --include ".jpeg" \
--content-type "image/png" --exclude "" --include ".png"
bash
`Navigate to your website's root directory in the terminal
Explanation of Flags:
* . * * * * * : Sync from the current local directory.
s3://your-unique-website-bucket-name: The target S3 bucket.
--acl public-read: Grants public read access to uploaded objects. Deprecated in favor of bucket policies and CloudFront OAC/OAI.
--exclude / --include: Fine-tune which files are uploaded.
--cache-control: Sets the Cache-Control HTTP header. Essential for performance.
--content-type: Explicitly sets the Content-Type header, crucial for browsers to render files correctly. Multiple --include and --content-type pairs are used to apply specific types.
Note on public-read ACL: While common for direct S3 website hosting, it's generally discouraged when using CloudFront. CloudFront's Origin Access Control (OAC) or Origin Access Identity (OAI) allows CloudFront to access S3 objects privately, enhancing security. The bucket policy should be adjusted to grant GetObject permissions to the OAI/OAC principal.
$1
| File Extension | Content-Type | :------------- | :---------------------------- | :------------------------------------ | :---------------------------------------- |
| | | | | | | | Font files | Header | Cache-Control Header | Notes |
.html | text/html | no-cache, max-age=0 | Revalidate frequently to ensure freshness |
.css | text/css | public, max-age=604800, immutable | Cache for 1 week, immutable for cache busting |
.js | application/javascript | public, max-age=604800, immutable | Cache for 1 week, immutable for cache busting |
.jpg, .jpeg| image/jpeg | public, max-age=31536000, immutable | Cache for 1 year |
.png | image/png | public, max-age=31536000, immutable | Cache for 1 year |
.webp | image/webp | public, max-age=31536000, immutable | Cache for 1 year |
.svg | image/svg+xml | public, max-age=31536000, immutable | Cache for 1 year |
font/woff2, font/ttf etc. | public, max-age=31536000, immutable | Cache for 1 year |
Cache Busting: For immutable assets (CSS, JS, images), append a version hash to the filename (e.g., styles.a1b2c3d4.css). This ensures that when the file content changes, the URL changes, forcing browsers and CDNs to fetch the new version.
---
$1
CloudFront acts as the global front-end, caching content at edge locations and serving it to users with low latency.
$1
1. Origin Domain:* Select your S3 bucket from the dropdown. *Do NOT use the S3 website endpoint URL here if you plan to use OAC/OAI. CloudFront will automatically populate it.
2. Origin Access:
* Recommended: Choose "Origin access control settings (recommended)." Create a new OAC. This will generate a policy statement that you'll need to apply to your S3 bucket policy, granting CloudFront read access.
* Legacy: Use "Legacy access identities" and create an OAI if necessary.
3. Viewer Protocol Policy: Select "Redirect HTTP to HTTPS."
4. Allowed HTTP Methods: Select "GET, HEAD, OPTIONS."
5. Cache Policy: Use CachingOptimized 6. Price Class: "Use all edge locations" for maximum performance and availability globally.
7. Alternate Domain Names (CNAMEs): Add your custom domain(s) (e.g., 8. Custom SSL Certificate: Select an ACM certificate for your custom domain (see Section 4).
9. Default Root Object: Enter 10. Security Headers: Consider adding security headers via CloudFront Functions or Lambda@Edge (more on this in Advanced Configuration). AWS CLI (Conceptual - requires for most static sites. You can create a custom policy for finer control.
example.com, www.example.com).
index.html. This ensures requests to / are served by index.html.
aws cloudfront create-distribution with a JSON configuration file):
` // distribution-config.json (simplified example)
{
"CallerReference": "your-unique-ref-id",
"DistributionConfig": {
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "S3-your-unique-website-bucket-name",
"DomainName": "your-unique-website-bucket-name.s3.amazonaws.com", // Use S3 API endpoint, not website endpoint
"S3OriginConfig": {
"OriginAccessIdentity": "origin-access-identity/cloudfront/E1111222333444" // Replace with your OAI if using legacy
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3-your-unique-website-bucket-name",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
}
},
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88632e7e9f" // Managed-CachingOptimized
},
"Aliases": {
"Quantity": 2,
"Items": ["example.com", "www.example.com"]
},
"DefaultRootObject": "index.html",
"Enabled": true,
"ViewerCertificate": {
"ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
},
"HttpVersion": "http2"
}
}
json
`
After creating the distribution, CloudFront will provision it globally, which can take several minutes. Note the Distribution Domain Name (e.g., d123abc.cloudfront.net).
$1
If you chose OAC or OAI, update your S3 bucket policy to grant CloudFront access:
Example OAC Policy:
` {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GrantCloudFrontRead",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-unique-website-bucket-name/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/YOUR_CLOUDFRONT_DISTRIBUTION_ID"
}
}
}
]
}
json
`
Replace 123456789012 with your AWS Account ID and YOUR_CLOUDFRONT_DISTRIBUTION_ID with your CloudFront distribution ID.
---
$1
This section covers mapping your custom domain to the CloudFront distribution and enabling HTTPS.
$1
1. Region: Request the certificate in the us-east-1 2. Domain Names: Enter your root domain (e.g., 3. Validation Method: Choose "DNS validation" for automated validation via Route 53.
4. Create Records in Route 53: ACM will provide CNAME records. If your domain is managed by Route 53, ACM can often create these records for you automatically. Otherwise, manually add them to your DNS provider. Once the certificate status is "Issued," it's ready to be used with CloudFront. region for CloudFront.
example.com) and any subdomains (e.g., www.example.com).
$1
If you are using Route 53 as your DNS provider:
1. Hosted Zone: Ensure you have a hosted zone for your domain (e.g., example.com 2. Create Records:
* For the root domain ( * Record Type: * Alias: Yes
* Route traffic to: Alias to CloudFront distribution.
* Choose Distribution: Select your CloudFront distribution from the dropdown.
* Routing Policy: Simple routing (or weighted/latency if applicable).
* For the * Record Type: * Alias: Yes
* Route traffic to: Alias to CloudFront distribution.
* Choose Distribution: Select your CloudFront distribution. If using an external DNS provider: * Root Domain: Use an * ).
example.com):
A
www subdomain (www.example.com):
A
ALIAS or ANAME record if supported by your provider, pointing to the CloudFront Distribution Domain Name. If not supported, you cannot easily point the root domain directly.
www Subdomain: Create a CNAME record for www pointing to your CloudFront Distribution Domain Name (e.g., d123abc.cloudfront.net).
Note on DNS Propagation: DNS changes can take time to propagate globally (from minutes to 48 hours).
---
$1
Enhance your static website's security and functionality.
$1
Security headers help mitigate common web vulnerabilities. These can be implemented using CloudFront Functions (preferred for performance and cost) or Lambda@Edge.
Example CloudFront Function (Viewer Request):
` // viewer-response.js
function handler(event) {
const response = event.response;
const headers = response.headers; // Add Security Headers
headers['strict-transport-security'] = { value: 'max-age=31536000; includeSubdomains; preload' };
headers['content-security-policy'] = { value: "default-src 'self'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self';" }; // Adjust directives as needed
headers['x-content-type-options'] = { value: 'nosniff' };
headers['x-frame-options'] = { value: 'DENY' };
headers['x-xss-protection'] = { value: '1; mode=block' };
headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' }; return response;
}
javascript
`
Associate this function with your CloudFront distribution's cache behavior for "Viewer Response" events.
$1
Configure CloudFront to serve custom error pages for specific HTTP status codes.
1. In your CloudFront distribution settings, go to "Error Pages."
2. Click "Create custom error response."
3. HTTP Error Code: Select 403 4. Customize Error Response: Yes.
5. Response Page Path: Enter the path to your custom error file (e.g., 6. HTTP Response Code: Keep the original code or set it to (Forbidden) or 404 (Not Found).
/error.html or /404.html).
200 if you want the error page to appear as a successful load (use with caution, can impact SEO and analytics). Usually, keep the original error code.
$1
For advanced protection against common web exploits (SQL injection, cross-site scripting), integrate AWS WAF with your CloudFront distribution. Create WAF Web ACLs and associate them with your CloudFront distribution.
---
$1
Maximize website speed and responsiveness.
$1
Create multiple cache behaviors to apply different caching rules based on path patterns.
| Path Pattern | Origin Request Policy | Cache Policy | TTL (Min/Max/Default) | Compression | Notes |
| :------------------ | :-------------------- | :------------------- | :-------------------- | :---------- | :--------------------------------------------------- |
| /assets/images/* | | | | | AllViewer | CachingOptimized | 31536000/31536000/31536000 | No | Cache images for 1 year, immutable asset |
/assets/css/* | AllViewer | CachingOptimized | 604800/604800/604800 | Yes | Cache CSS for 1 week, immutable asset |
/assets/js/* | AllViewer | CachingOptimized | 604800/604800/604800 | Yes | Cache JS for 1 week, immutable asset |
*.html | AllViewer | Managed-BypassCache| 0/0/0 | Yes | Revalidate HTMLs to ensure freshness, compress them |
* | AllViewer | Managed-BypassCache| 0/0/0 | Yes | Fallback for any other file type, compress |
Origin Request Policy:
* AllViewer * * Custom: Define precisely what to forward. For static sites, often minimal forwarding is required for cache hits. Compression: Enable compression (Gzip and Brotli) within CloudFront cache policies for text-based assets (: Forward all headers, cookies, and query strings.
CacheOptimized (managed policy): Only forwards necessary headers/cookies for caching. Use this unless specific dynamic behavior is needed.
.html, .css, .js).
$1
* Format Conversion: Use modern formats like WebP for smaller file sizes with comparable quality.
* Compression: Employ tools like imagemin * Responsive Images: Use the , jpegoptim, pngquant, or cloud-based image optimization services.
element or srcset attribute in HTML to serve appropriately sized images based on the viewport.
$1
Implement lazy loading for images and other media that are below the fold to improve initial page load time.
` // Example using Intersection Observer API
document.addEventListener("DOMContentLoaded", function() {
var lazyImages = document.querySelectorAll("img.lazy");
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
}); lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// Fallback for older browsers
lazyImages.forEach(function(lazyImage) {
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
});
}
});
javascript
`
`html
`
---
$1
Understand your website's performance and usage.
$1
CloudFront automatically sends metrics to CloudWatch. Key metrics to monitor:
| Metric Name | Description | Threshold/Alerting |
| :----------------- | :--------------------------------------------------- | :------------------------------------------------ |
| Requests | | | | | Total number of viewer requests | Monitor trends, set alerts for sudden spikes |
BytesDownloaded | Total bytes transferred to viewers | Monitor for cost estimation, set alerts for anomalies |
4xxErrorRate | Percentage of requests resulting in 4xx errors | Set alert if > 1% |
5xxErrorRate | Percentage of requests resulting in 5xx errors | Set alert if > 0.1% |
CacheHitRate | Percentage of requests served from CloudFront cache | Aim for > 90% for static sites |
$1
Enable CloudFront access logs to capture detailed information about each request. These logs are delivered to an S3 bucket.
1. In your CloudFront distribution settings, under "Logging," enable logging.
2. Specify an S3 bucket for log delivery.
3. Choose a log prefix (e.g., cloudfront-logs/).
Analyzing these logs can provide insights into traffic sources, user agents, referrers, and more. Tools like Amazon Athena can query these logs directly from S3.
$1
Integrate a web analytics service (e.g., Google Analytics, Plausible, Matomo) by adding their tracking script to your index.html` or a common layout file.
---
$1
Optimize AWS costs for your static website.
$1
1. S3 Storage Class: Use S3 Standard for frequently accessed assets. Consider S3 Intelligent-Tiering if access patterns are unpredictable.
2. S3 Lifecycle Policies: Implement policies to transition older, less accessed data to cheaper storage classes (e.g., S3 Glacier) or expire it.
3. CloudFront Price Class: "Use all edge locations" provides the best performance but can be more expensive than regional options. Evaluate based on your target audience's geographic
Why This Matters
Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.
Trade-offs to Consider
Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.
When NOT to Use This
Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.
Decision Framework
Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.