Skip to main content

Hosting my website on AWS with Hugo

·1099 words·6 mins
Justin van den Anker
Author
Justin van den Anker
A curious george at heart

Introduction
#

For the past six years I’ve been working with Azure (at some jobs more than others), but for a while now I’ve been wanting to try a different cloud provider. I want to update my blog and add infrastructure as code. So I came up with some new requirements:

  1. Host the website on AWS

  2. Provision the website using Pulumi

  3. Capture all the infrastructure & settings in code

The plan
#

My website is built using Hugo, an open-source static site generator. This means that I don’t need anything fancy to host it. Something that is capable of serving static files will do nicely. For these things there are a couple of options available in AWS. One of these options is to have an AWS Amplify instance host my website. But that takes away part of the challenge because it hides and manages some basic services I want to manage myself. Since I want to stick with basic infrastructure offerings, I end up with S3 buckets. They can serve static files and even have settings that support hosting a static website (it comes with a special endpoint! Which we won’t be using…).

In case you’re wondering if you’re getting the bucket website endpoint or the regular bucket endpoint served by the CDN. Look at the error page! If it’s in XML you’re staring at the regular REST endpoint of the bucket. Otherwise it’s the website endpoint.

One thing about working with S3 buckets is deciding whether you want to have it publicly accessible. If you do this you have to make sure that you set all the actions & identities correctly. This can be quite tricky and it’s easy to miss things. Luckily for me; there is another solution. We can create an Amazon CloudFront distribution that serves the content of the bucket. This makes my website have less latency, higher security and better availability. The CDN will need a certificate to secure things via HTTPS but that’s about it.

Side note: The certificate for the CDN needs to be in the us-east-1 AWS region for it to work.

So that’s the second piece of our little puzzle. The only thing still missing is making sure that something points to our CloudFront Distribution and we should be good to go. For this I need to create some DNS records in the website’s hosted zone. I already have the hosted zone set up for my e-mail provider’s custom domain functionality. So the Pulumi program will create two pairs of A records in the hosted zone. One pair is for the www subdomain and the other for the root domain. Each pair consists of a validation record that proves the domain is mine and a record that points to the CDN endpoint.

Now that the plan is formed I need to come up with a way to provision this with Pulumi. Thanks to their amazing documentation we can find an example of this scenario on their Github.

The execution
#

If I clone the repository and set all the variables and configuration correctly I should be able to deploy it using the pulumi up command in the terminal. After having done this, all resources are created and the configuration should be correct. But I didn’t check if the domain variable in the configuration needs the www prefix. It turns out it doesn’t and Pulumi now creates two A records in my hosted zone resolving www.justinvandenanker.nl & www.www.justinvandenanker.nl. At this point both those subdomains resolve, so most of the setup is functioning. Except when you click a link, that will get you a solid 403: access denied. But first: lets fix the subdomain situation.

Simply changing the configuration to the correct value without the www prefix gets me into trouble quickly. The pulumi up command now gets lost in the forest because there’s overlap between something that exists and something that needs to be created.

They are linked with the two domain verification records and it returns an error along the lines of resource already created. It turns out the only option to mitigate this was to destroy the whole stack and redeploy. Make sure that before you destroy the stack you empty out the log S3 bucket that comes with this example as that doesn’t store it’s objects as Bucket resources managed by Pulumi.

After destroying and redeploying the stack everything works as intended. Now, back to the issue of the links on the website not working. As it turns out the CDN only has rights to perform a GET request on the bucket and every request that is the non-root request needs to resolve to a file.

With Hugo all my paths are folders and in these folders there is another index.html file. This is however not the request made by the CDN. That requests the folder and therefore it returns a 403. So we need something that manipulates the incoming requests before forwarding it. This brings me to an unexpected puzzle piece: CloudFront Functions. After looking this up in the Pulumi documentation I concluded that they are in fact quite easy to set up and add to the CDN's cache behavior. This results in the following piece of code:


const rewriteFunctionString =

`function handler(event) {

var request = event.request;

var uri = request.uri;

  

if(uri.endsWith('/')) {

request.uri += 'index.html';

}

  

else if(!uri.includes('.')) {

request.uri += '/index.html'

}

  

return request;

}`;

  

const rewriteFunction = new aws.cloudfront.Function("awsFunctionResource", {

code: rewriteFunctionString,

runtime: "cloudfront-js-2.0",

comment: "Function te rewrite incoming requests to reference the index.html file in non-root directories.",

keyValueStoreAssociations: [],

name: "HugoIndexRewriter",

publish: true,

});

which can be added to the default cache behavior like so:


defaultCacheBehavior: {

targetOriginId: contentBucketWebsite.websiteEndpoint,

viewerProtocolPolicy: "redirect-to-https",

allowedMethods: ["GET", "HEAD", "OPTIONS"],

cachedMethods: ["GET", "HEAD", "OPTIONS"],

functionAssociations: [{eventType: "viewer-request", functionArn: rewriteFunction.arn}],

forwardedValues: {

cookies: { forward: "none" },

queryString: true,

},

And after running another pulumi up I now have a fully functional website! Hooray!

The conclusion
#

All in all I’m quite satisfied. The whole endeavor took half a Sunday with some nice lessons along the way.

The lessons:

  1. Read the code and understand the constraints on variables and parameters

  2. You can always adjust the code to make the constraints more forgiving so it’s harder to make mistakes and easier to do the right thing

  3. Trial and error is the best way forward

Now obviously even though I’m happy with the setup, there’s always improvements to be made.

So for the next project:

  • Move the build & deployment of the website to GitHub Actions.`
  • Invalidate the CloudFront CDN after deployment

Thanks for reading and see you next time!