Publishing a Website the Modern Way - Part 2

Jordan Patterson
Jordan Patterson

This is part two of my journey to getting this blog online. You can read part one here.

It works! It only took another two hours. (That was sarcasm).

In my previous post I made two solid attempts at deploying a static website built with Hugo - this blog in fact - to AWS. It ended with me giving up and going to bed. I finally got it working the day after but I really don’t like how it turned out and in this post I’ll explain why.

My second attempt was very close, but I had to tear it all down again, fork the repository, make a few tweaks, and learn how to build and deploy CloudFormation scripts that include assets. Once that was finished I had fixed my CSP errors and everything was working beautifully… almost.

The last remaining issue appeared when I attempted to navigate away from the home page to a blog post.

Access Denied (with a huge hash after it)

So I added /index.html to the end of the URL and sure enough, it worked. Sigh. This is the issue I was trying to avoid in my first attempt.

Static Website Hosting with S3

AWS S3 has allows you to serve a static website directly from and S3 bucket. Two of the settings that are required when you enable this are “Index document” and “Error document”.

"Index document" tells S3 which file to serve if no specific file is requested. This is typically set to index.html. So if you request yourdomain.com, S3 will show /index.html from the root of your bucket if it exists. If you request yourdomain.com/posts/, S3 will show /posts/index.html if it exists.

"Error document" is the file that gets served if the file that was requested doesn’t exist. For example, if you request yourdomain.com/posts/first-post.html and /posts/first-post.html does not exist in your bucket, it will show the error document instead. This also applies to missing index documents. If you request yourdomain.com/posts/second-post/, S3 will look for /posts/second-post/index.html. If it does not exist, it will show the error document.

This functionality is pretty standard for a web server. This is essentially would be accomplished with something like this in nginx.

error_page 404 /error.html;
try_files $uri $uri/index.html =404;

S3 Website Endpoint

S3 only behaves this way if you access the contents of the bucket via the S3 website endpoint. This has a major drawback though; it doesn't support https. If you are willing to go without https in 2020, you can point a CNAME record to the endpoint and be done.

However, since it is 2020 and https is required for good SEO rankings, some browser and javascript features, not to mention security, this probably isn't a good place to compromise. Amazon's suggested solution is to use the S3 website endpoint as the origin for a CloudFront distribution. Which is what I tried in attempt number one in my first post. The problem is that this leaves the content open and accessible via the S3 website endpoint as well as the CloudFront endpoint.

The Solution

In addition to what I implemented in attempt number two I added a Lambda@Edge function to the origin request trigger on the CloudFront distribution to handle default directory indexes. I believe the only thing that is still missing is the error document functionality.

This is a very complicated solution. There are a lot of moving parts. It involves two S3 buckets, two SSL certificates, two CloudFront distributions, and four Lambda functions. These are wrapped up in a set of CloudFormation stack scripts that total 655 lines of configuration.

I realize that I am putting myself through this pain. I could accomplish this much more simply using Netlify or Vercel. But where is the fun in that.