Upload files → Enable static hosting → Create CloudFront distribution with OAC → Point domain via Route 53 → Request free ACM certificate → Enable HTTPS. That's the whole process.
💡 The OAC gotcha: AWS still shows "Origin Access Identity" in older docs and some console workflows. OAI is legacy. Use Origin Access Control (OAC) instead — it supports SSE-KMS, additional request headers, and POST/PUT to S3. If you pick OAI by accident, CloudFront will still work, but you lose the ability to sign requests with SigV4, which breaks any bucket using default SSE-S3 encryption after Jan 2023. The fix is non-obvious: delete the OAI, create an OAC, update the bucket policy with the new condition key (AWS:SourceArn instead of s3:GetObject with a canonical user ID). Took me two hours to untangle.
- AWS account (Free Tier is fine)
- Static site files (even a single index.html works)
- Domain name (optional — covered below)
No site yet? Use this:
<!DOCTYPE html>
<html>
<head>
<title>My First AWS Site</title>
</head>
<body>
<h1>Hello from S3!</h1>
<p>If you can read this, it's working.</p>
</body>
</html>
Step 1: Create a S3 Bucket
S3 holds your files. Distributed storage, scales to millions of requests, costs almost nothing for static content.
- Open the S3 Console
- Click Create bucket
- Pick a bucket name — globally unique across all of AWS. Use your domain name:
techwilla-site - Select a region near your users
- Keep "Block all public access" ON — CloudFront handles access, not S3
- Click Create bucket
Do not unblock public access. Half the tutorials out there tell you to. They are wrong. CloudFront + OAC is the correct approach.
Upload Your Files
- Open the bucket
- Click Upload
- Drag in everything — HTML, CSS, images, JS
- Click Upload
Files are in S3 now but nobody can reach them. That is by design. CloudFront is next.
Step 2: Create a CloudFront Distribution
CloudFront is the CDN layer. It caches your files at 400+ edge locations. A visitor in Mumbai gets served from Mumbai, not us-east-1.
It also solves the access problem: OAC (Origin Access Control) lets CloudFront read from a private bucket. The bucket stays locked down.
- Open the CloudFront Console
- Click Create distribution
Origin Settings
- Origin domain: Pick your S3 bucket from the dropdown
- It will ask about the S3 website endpoint — say No. OAC is better.
- Origin access: "Origin access control settings (recommended)"
- Click Create new OAC, keep defaults, click Create
- CloudFront will nag about updating the bucket policy. Ignore it for now — we handle that in a minute.
Default Cache Behavior
- Viewer protocol policy: "Redirect HTTP to HTTPS"
- Allowed HTTP methods: GET, HEAD. Static site. Nothing else needed.
- Caching defaults are fine. Tune later if you want.
Settings
- Default root object:
index.html. Skip this and visitors see an XML error. Every time. - Defaults for the rest.
- Click Create distribution
CloudFront shows a banner about the bucket policy. Click Copy policy.
Update Bucket Policy
- Back to S3. Open the bucket.
- Permissions tab
- Bucket policy → Edit
- Paste the policy CloudFront generated
- Save
That policy scopes read access to one specific CloudFront distribution. Nothing else gets in.
Wait for Deployment
5-15 minutes. Status column says "Deploying" until it finishes.
When it flips to "Enabled," grab the Distribution domain name (something like
d1234abcd.cloudfront.net). Open it. Your site should load over HTTPS.
Step 3: Connect Your Domain (Route 53)
The CloudFront URL works but looks terrible. Route 53 maps your actual domain to the distribution. Domain registered elsewhere? Fine — you can either transfer it or just point DNS records at CloudFront.
Option A: Domain on Route 53
Domain registered through Route 53 or already transferred:
- Route 53 → Hosted zones
- Click the domain
- Create record
- Record name: blank for root, or
www - Alias: ON
- Route traffic to: Alias to CloudFront distribution
- Select the distribution
- Create record
Option B: Domain Elsewhere
Domain on Namecheap, GoDaddy, Cloudflare, wherever:
- Go to the registrar's DNS settings
- Add a CNAME pointing to your CloudFront distribution domain
- Root domains need ALIAS/ANAME — not every registrar supports this. Cloudflare does. GoDaddy does not.
DNS propagation: up to 48 hours on paper, usually under 30 minutes.
Update CloudFront with Your Domain
People skip this, then wonder why the custom domain returns a CloudFront error. You have to register the domain inside CloudFront.
- CloudFront → your distribution → Edit (under Settings)
- Alternate domain names (CNAMEs): add
example.comandwww.example.com - Custom SSL certificate: click "Request certificate" — opens ACM
Get a Free SSL Certificate
- ACM → Request a certificate
- Public certificate
- Domain names:
example.comand*.example.com(wildcard covers subdomains) - DNS validation (not email — DNS is faster and auto-renews)
- ACM generates CNAME records. Add them to Route 53 or your registrar.
- Validation takes 5-30 minutes
Certificate shows "Issued"? Go back to CloudFront, edit the distribution, pick the certificate. Save. Done.
What About the Bill?
Actual costs for a low-traffic site (under 10K visitors/month): somewhere between $0.50 and $2.00. The CloudFront free tier (1TB transfer, 10M requests) covers most personal sites entirely for 12 months.
Watch out for logging. S3 access logs generate objects per request. Leave logging on for a busy site and your storage bill balloons from the log files, not the website files.
Common Issues and Fixes
"Access Denied" When Visiting the Site
Bucket policy, 99% of the time. The resource ARN needs the /* wildcard: arn:aws:s3:::your-bucket/*. Without it, CloudFront can see the bucket exists but cannot read any objects inside it. Classic.
Website Shows XML Error Instead of Page
Default root object is missing. CloudFront distribution → Edit → set it to index.html. That is the entire fix.
Changes Not Showing Up
CloudFront is caching stale files. Invalidate:
CloudFront → Your Distribution → Invalidations → Create invalidation
Enter: /*
This clears the entire cache.
Or wait. Default TTL is 24 hours.
HTTPS Not Working
Three things to check: ACM certificate status is "Issued" (not "Pending validation"), the certificate is selected in the CloudFront distribution, and the domain appears in "Alternate domain names." Miss any one and HTTPS fails silently.
The Architecture
Full request path, top to bottom:
User Request
↓
Route 53 (DNS: example.com → CloudFront)
↓
CloudFront (CDN - edge location nearest to user)
↓ (cache miss? fetch from origin)
S3 Bucket (private, only CloudFront can access)
↓
Returns file → CloudFront caches it → Serves to user
🔧 403 Forbidden checklist: Bucket policy includes the /* wildcard? Static website hosting toggled on in bucket properties? Index document spelled index.html exactly — capitalization matters.
S3 is storage. CloudFront is delivery. Route 53 is routing. Each does one thing. The whole stack scales to zero and scales to millions without any config changes.
Automating Deploys
Uploading through the console gets old fast. Here is a deploy script for the AWS CLI:
#!/bin/bash
BUCKET="your-bucket-name"
DISTRIBUTION_ID="your-cloudfront-id"
# Sync files to S3
aws s3 sync ./public s3://$BUCKET --delete
# Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
echo "Deployed!"
./deploy.sh and the site is live in seconds. Wire it into GitHub Actions later for push-to-deploy.
Monthly Cost Breakdown
| Service | Price | Free Tier |
|---|---|---|
| S3 Storage | $0.023/GB/month | 5 GB for 12 months |
| CloudFront Transfer | $0.085/GB | 1 TB/month for 12 months |
| CloudFront Requests | $0.0075/10K HTTPS requests | 10M requests/month for 12 months |
| Route 53 Hosted Zone | $0.50/zone/month | None |
| Route 53 Queries | $0.40/million queries | None |
| ACM (Certificate Manager) | $0.00 | Always free for public certs |
💬 Comments