Blog · May 2026 · 9 min read

Static Sites on AWS: From Cuddle-O-Tron to a Real Portfolio

My first website on lhiggins.cloud was a Pet Cuddle-O-Tron. Not a metaphor — it was literally an HTML page with a button that said "cuddle" and showed a picture of a cat. I built it to learn AWS, and it served its purpose. But eventually I wanted something real.

The Architecture

Static sites on AWS follow a well-trodden path:

The total cost for all of this is roughly $1–2/month for a low-traffic personal site. The free tier covers most of it.

The S3 Bucket Setup

Creating an S3 bucket for static hosting is straightforward, but there's a catch: you should not enable "Static Website Hosting" on the bucket if you're using CloudFront. Instead, use the S3 REST API endpoint as the CloudFront origin. This lets CloudFront handle the access control via an Origin Access Control (OAC) policy, which is more secure than making the bucket publicly accessible.

aws s3 website s3://lhiggins.cloud/ \
  --index-document index.html \
  --error-document index.html

Actually, skip that. Use CloudFront with OAC instead. The bucket policy should only allow the CloudFront distribution to read from it:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::lhiggins.cloud/*",
    "Condition": {
      "StringEquals": {
        "aws:SourceArn": "arn:aws:cloudfront::ACCOUNT:distribution/DIST_ID"
      }
    }
  }]
}

The ACM Certificate Gotcha

ACM certificates must be created in us-east-1 (N. Virginia) to work with CloudFront. This is documented, but it's easy to miss if you typically work in another region. I created my first certificate in us-west-2 and couldn't figure out why CloudFront couldn't see it.

The certificate validation happens via DNS. ACM gives you a CNAME record to add to Route53, and once it validates (usually within minutes), you can attach it to your CloudFront distribution.

The Route53 Alias vs CNAME Trap

Here's the one that cost me an hour. When pointing your domain to CloudFront, you need to use an Alias record, not a CNAME. Alias records are Route53-specific — they resolve to AWS resources internally without additional DNS lookups. CNAMEs won't work for the zone apex (the bare domain like lhiggins.cloud).

The CloudFront distribution gives you a domain like d1234abcdef.cloudfront.net. In Route53, create an A-record with "Alias" set to "Yes" and point it to that CloudFront domain. Do the same for www.lhiggins.cloud if you want it.

The Redirect Loop

My first deployment had a redirect loop: lhiggins.cloud → CloudFront → S3 → redirect to lhiggins.cloud → repeat. The problem was that I had both S3 static website hosting and CloudFront configured, and the S3 redirect rules were conflicting with CloudFront's default root object.

The fix: disable S3 static website hosting entirely. Let CloudFront handle everything. Set the default root object to index.html in the CloudFront distribution settings and point the origin to the S3 REST API endpoint (not the website endpoint).

Deploying Updates

My deploy process is a one-liner:

aws s3 sync ./site/ s3://lhiggins.cloud/ \
  --delete \
  --cache-control "no-cache" && \
aws cloudfront create-invalidistribution-id DIST_ID \
  --paths "/*"

Sync to S3, invalidate the CloudFront cache. Changes are live within 30 seconds.

Cost Breakdown

For a personal site with low traffic, the monthly bill looks like this:

Total: ~$1–2/month. Hard to beat.

Lessons

Need help with this?

I set up production-hardened static sites on AWS — HTTPS, CDN, DNS, the works. Your site, your domain, ~$1/month.

Work with me →
← Back to blog