Deploying a Serverless Web Application on AWS S3 & CloudFront
A complete engineering guide to hosting high-availability Single-Page Applications (SPA) with Amazon S3, CloudFront Origin Access Control (OAC), Route53, and automated SSL.
1. Architecture Overview
Modern Single-Page Applications (SPAs) built with React, Vue, or Angular do not require heavy, dedicated EC2 compute instances to serve static bundles. By combining Amazon S3 for resilient object storage and Amazon CloudFront as a globally distributed Content Delivery Network (CDN), you achieve sub-50ms latency worldwide, automatic scaling during traffic spikes, and ultra-low hosting costs.
- Amazon S3: Stores compiled HTML, CSS, JS bundles, and static UI assets privately.
- CloudFront (with OAC): Caches content across 400+ worldwide edge locations and securely requests private objects from S3.
- AWS Certificate Manager (ACM): Issues and automatically renews TLS/SSL certificates at zero extra cost.
- Amazon Route53: Resolves DNS queries and routes custom domain requests to CloudFront via Alias (A/AAAA) records.
2. Step 1: Create a Private S3 Storage Bucket
Keep your S3 bucket strictly private by enabling Block All Public Access. External clients must never query S3 directly; traffic is routed exclusively through CloudFront Origin Access Control (OAC).
# Create the private S3 bucket
aws s3api create-bucket \
--bucket cicdclouds-web-app-prod \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
# Enforce Block Public Access settings
aws s3api put-public-access-block \
--bucket cicdclouds-web-app-prod \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
3. Step 2: Build and Upload Production Assets
Compile your frontend production distribution and sync it to S3, applying distinct caching policies for hashed assets versus the entry point:
# Build production bundle
npm run build
# Sync static build assets with long-term immutable caching
aws s3 sync ./dist s3://cicdclouds-web-app-prod/ \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html"
# Upload index.html with no-cache so newly deployed releases load immediately
aws s3 cp ./dist/index.html s3://cicdclouds-web-app-prod/index.html \
--cache-control "no-cache, no-store, must-revalidate"
4. Step 3: Configure CloudFront & Restrictive S3 Bucket Policy
Create a CloudFront distribution with Origin Access Control (OAC) targeting your S3 bucket. Then apply the following Bucket Policy, allowing only your CloudFront distribution ARN to retrieve objects:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::cicdclouds-web-app-prod/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
5. Step 4: SPA Routing Fallbacks & Custom Domains
Single-Page Applications rely on client-side routing (e.g., /catalog/browse). Direct visits or browser refreshes on deep paths cause S3 to return a 403 Forbidden or 404 Not Found error because the corresponding S3 key does not exist.
To solve this, configure a Custom Error Response in your CloudFront distribution settings:
- HTTP Error Code: 403 (and 404)
- Response Page Path:
/index.html - HTTP Response Code:
200: OK
Finally, provision an SSL certificate in the us-east-1 (N. Virginia) region using AWS Certificate Manager, associate your custom domain to CloudFront, and point your Route53 A (Alias) record to the CloudFront distribution domain name.