AWS 📖 18 min read
📅 Published: 🔄 Updated:

Host a Static Website on AWS (The Right Way)

Before: $12/month on a VPS for a static site. After: $0.53/month on S3 + CloudFront.
Same files, same traffic, 96% cost reduction.

🛠️ Before You Start

💻
Hardware Any computer with a web browser
📦
Software AWS account (Free Tier works), AWS CLI optional
⏱️
Estimated Time 30-60 minutes

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.

No site yet? Use this:

index.HTML
<!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.

  1. Open the S3 Console
  2. Click Create bucket
  3. Pick a bucket name — globally unique across all of AWS. Use your domain name: techwilla-site
  4. Select a region near your users
  5. Keep "Block all public access" ON — CloudFront handles access, not S3
  6. 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

  1. Open the bucket
  2. Click Upload
  3. Drag in everything — HTML, CSS, images, JS
  4. 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.

  1. Open the CloudFront Console
  2. Click Create distribution

Origin Settings

Default Cache Behavior

Settings

CloudFront shows a banner about the bucket policy. Click Copy policy.

Update Bucket Policy

  1. Back to S3. Open the bucket.
  2. Permissions tab
  3. Bucket policy → Edit
  4. Paste the policy CloudFront generated
  5. 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)

Terminal: SSH login to server
Terminal: SSH login to server

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:

  1. Route 53 → Hosted zones
  2. Click the domain
  3. Create record
  4. Record name: blank for root, or www
  5. Alias: ON
  6. Route traffic to: Alias to CloudFront distribution
  7. Select the distribution
  8. Create record

Option B: Domain Elsewhere

Domain on Namecheap, GoDaddy, Cloudflare, wherever:

  1. Go to the registrar's DNS settings
  2. Add a CNAME pointing to your CloudFront distribution domain
  3. 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.

  1. CloudFront → your distribution → Edit (under Settings)
  2. Alternate domain names (CNAMEs): add example.com and www.example.com
  3. Custom SSL certificate: click "Request certificate" — opens ACM

Get a Free SSL Certificate

  1. ACM → Request a certificate
  2. Public certificate
  3. Domain names: example.com and *.example.com (wildcard covers subdomains)
  4. DNS validation (not email — DNS is faster and auto-renews)
  5. ACM generates CNAME records. Add them to Route 53 or your registrar.
  6. 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:

AWS Console
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:

Architecture
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:

deploy.sh
#!/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