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:
- S3 stores the HTML files and serves them as a static website
- CloudFront sits in front as a CDN, providing HTTPS and caching
- ACM provides the SSL/TLS certificate
- Route53 manages DNS and points the domain to CloudFront
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:
- S3 storage: ~$0.01 (a few MB of HTML)
- S3 requests: ~$0.01 (hundreds of GET requests)
- CloudFront transfer: ~$0.50 (under the free tier most months)
- Route53: $0.50 per hosted zone
- ACM: Free
Total: ~$1–2/month. Hard to beat.
Lessons
- ACM certificates go in us-east-1. Always. No exceptions for CloudFront.
- Use Alias records, not CNAMEs. Especially for the zone apex.
- Disable S3 static hosting when using CloudFront. Let CloudFront be the origin handler.
- Cache invalidation matters. Without it, you'll be looking at stale content and wondering why your deploy "didn't work."
I set up production-hardened static sites on AWS — HTTPS, CDN, DNS, the works. Your site, your domain, ~$1/month.
Work with me →