How to build a Hugo website in AWS Lambda and deploy it to S3

This tutorial explains how to run Hugo in AWS Lambda and deploy a static website to Amazon S3.

Running Hugo in AWS Lambda can be useful if you want to automate your Hugo builds in the cloud and only pay for the build time.

Example use cases:

Build the website when code is pushed to GitHub. GitHub can trigger the Lambda through an API Gateway endpoint using webhooks.

Content authors use a web form to submit website content. The content is uploaded to S3, which triggers the Lambda to build and deploy the website.

Even though getting Hugo to run in Lambda is a fun project, I’ll note that for a simple website like mine nothing beats the AWS Amplify Console linked to a GitHub repo.

Solution

We will create a Python function that downloads website’s files from an S3 bucket, runs Hugo to generate the website, and uploads the static website to another S3 bucket configured to serve web pages.

Here are the high-level components we need to create to make it all work.

Lambda Layer with a Hugo binary that the function will execute.

Lambda Layer with the libstdc++ library, a Hugo’s dependency.

Lambda Layer with AWS CLI to enable easy copying of files to/from S3.

S3 buckets to store files and serve the website.

Lambda function that will build and deploy the website.

IAM role with the necessary permissions for Lambda to read from and write to S3.

SAM Template

If you prefer to use CloudFormation or SAM CLI, I’ve put together a SAM template on GitHub. Look into deploy_template.sh for the list of commands to run.

Note that you’ll still have to obtain all the dependencies described in steps 1-3 and put the zip files into the layers directory, so keep reading.

Step-by-step guide using the AWS Management Console

1. Create a Lambda Layer with the Hugo binary

To run Hugo in Lambda, we will need to make the Hugo binary available to the function as a Lambda Layer. The contents of the layer will be added to the Lambda execution environment and can be used from the function code.

Download the latest Hugo binary for Linux from GitHub. The file name should look like hugo_0.53_Linux-64bit.tar.gz.

Un-tar the archive and zip the hugo binary into hugo-layer.zip. Important: hugo should be in the “root” of the zip archive, do not place it into a subdirectory.

Go to the Lambda console, click on Layers in the left menu, and click Create layer.

In the form, give your layer a name, upload the zip file and select the Python 3.7 runtime.

2. Create a Lambda Layer with the libstdc++ library

Hugo requires libstdc++ in order to run but this library isn’t included in the Lambda execution environment. This means we need to obtain this library compiled specifically for Amazon Linux 2.

3. Create a Lambda Layer with AWS CLI

AWS CLI makes it very easy to get files to and from S3 with the aws s3 sync command. However, it is not part of the Lambda execution environment, so we’ll need to create a Lambda Layer with the AWS CLI.

5. Create the Lambda function

Now the fun part. We’ll create a Lambda function that will make use of all the artifacts we’ve created so far.

Go to the Lambda console and click Create function.

In the form, give the function a name and select Python 3.7 in the Runtime dropdown.

Under Role, select Create a new role from one or more templates, give your role a name, and select Amazon S3 object read-only permissions from the Policy templates dropdown. Remember the role’s name, you’ll need it later.

Click Create function.

In the Designer section, click on Layers (right under the function name in the middle of the screen).

Click Add a layer and select the layer you created in step 1. Repeat for the other two layers.

Click on the function name in Designer and scroll to the Environment variables section.

In Key type SOURCE_PATH and paste the source bucket’s name from step 4a in the Value text box. Note: only paste the bucket name, e.g. example-bucket.

In the next line, add another variable DESTINATION_BUCKET with the website bucket name from step 4b as the value.

Scroll to Basic settings and set Memory to 512 MB and Timeout to 30 seconds.

Scroll to the code editor, paste the following code and click Save.

Note on the code:

This function uses the subprocess module in Python to run shell commands. It’s essentially a shell script packaged as a Python program. Read through the comments to understand how it works.

Lambda Layers are unzipped in the /opt directory.

Binaries must have 755 permissions in order to run. If you see access denied errors in logs when trying to run shell commands, you may need to run chmod u+x on the binaries to make them executable. If you used a Mac or Linux machine to download the files, there should be no issue with permissions.

Lambda functions can write files to the /tmp directory and this is where the function downloads the sources and stores the output of Hugo.

6. Modify the IAM role

You might have noticed that we only gave this function read-only access to S3. When the function attempts to write files to the S3 bucket, it will fail due to lack of permissions to write to S3. We need to manually update the IAM policy for the role created by Lambda.

Go to the IAM console and click on Roles in the left menu.

Locate the role you’ve created with the Lambda function in step 5.3 and click on it.

Click Add inline policy and then go to the JSON tab.

Paste the following statement, replacing example-source-bucket and example-website-bucket with your source and website buckets’ names, click Review and proceed to saving the policy.