Featured image of post Static Site Deployment on S3 using Containerized LocalStack

Static Site Deployment on S3 using Containerized LocalStack

Introduction

LocalStack is a local cloud development solution that emulates AWS services in a local environment. This enables developers to build and test various AWS-based solutions on a small scale without requiring an AWS account.

In this post, LocalStack is used to test the deployment of a static website to a S3 bucket. Hugo, a popular static site generator, is used to build the site. The following topics are covered in more detail throughout this post:

  • Creating a containerized environment that includes:
    • LocalStack
    • Hugo
  • S3 bucket creation and configuration for static website hosting
  • Building and deploying the static site to the S3 bucket
  • Automating the deployment process

The entire source code and setup is available on GitHub.

This post does not cover the inner workings of Hugo or how to set up a Hugo static site. Instead, it focuses solely on the deployment aspect. Hugo themes can be found here.

Setup of the Containerized Environment

LocalStack provides a pre-built Docker image that’s available on Docker Hub, offering a fully functional local AWS cloud stack in a containerized environment. However, the image doesn’t include any Hugo-related dependencies to build a static website. For that, we will create a new image based on the LocalStack image and include the additional dependencies.

The Dockerfile below implements the initial basic setup:

1
2
3
4
5
6
7
8
9
FROM localstack/localstack

RUN apt-get update && apt-get install -y --no-install-recommends \
    hugo \
    ca-certificates \
    git \
    golang

EXPOSE 4566

Details for installing Hugo can be found from the official documentation

We can then build the image once the Dockerfile is set up by using the command:

1
podman build . -t localstack-gohugo

Finally, we can run the container by using the command:

1
2
3
4
5
podman run \
  -it \
  -p 4566:4566 \
  -v ./hugo-blog:/opt/code/localstack/hugo-blog \
  --name localstack-gohugo localstack-gohugo 

./hugo-blog can be an empty directory, in which Hugo can be set up within the container, or it can be a directory that already contains a Hugo website.

Accessing the container can be done by opening a new terminal and executing the following command:

1
podman exec -it localstack-gohugo bash

At this point, we have a running localstack-gohugo container that has the following built-in dependencies set up:

  • LocalStack
  • Hugo and its dependencies

However, we do not yet have the actual final build of the website or any deployable, site-related output. In the next sections, we will walk through the deployment process within the container to better understand how the manual process works. Afterward, we will demonstrate a better and enhanced automated way to perform the deployment.

S3 Bucket Setup

Two simple steps are needed to set up the S3 bucket:

  • Create the bucket
  • Apply a bucket policy to allow public access

Bucket creation in LocalStack is done using the command below:

1
awslocal s3 mb s3://testwebsite

awslocal is used instead of aws for all AWS-related APIs when using LocalStack.

Once the bucket has been created, a policy has to be set and applied to the bucket so that the website can be publicly accessible. Therefore, the following bucket policy will be set:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::testwebsite/*"
    }
  ]
}

The bucket IAM policy allows anyone ("Principal": "*") to get read-only access ("Action": "s3:GetObject") to the website contents on the S3 bucket ("Resource": "arn:aws:s3:::testwebsite/*").

The official AWS Documentation contains more thorough details regarding the attributes.

Lastly, the policy is applied to the bucket by using the following command:

1
awslocal s3api put-bucket-policy --bucket testwebsite --policy file://bucket_policy.json

Deploying to S3

Several steps are required before we can deploy to our S3 bucket, namely:

  • Properly set up the hugo.toml configuration file
  • Build the static site using Hugo: hugo --gc --minify
  • Deploy the site to the S3-bucket: awslocal s3 sync ./ s3://testwebsite

Configuring hugo.toml

The hugo.toml config file is already properly set when building the image; however, this section explains how to set the configuration file.

The configuration file is human-readable, and we only need to set four parameters to get started. With LocalStack, the S3 website endpoint accepts several formats; the most common is the virtual-hosted style: http://<BUCKET_NAME>.s3-website.localhost.localstack.cloud:4566. Hence, the configuration file looks as follows:

1
2
3
4
baseURL = 'http://testwebsite.s3-website.localhost.localstack.cloud:4566'
languageCode = 'en-us'
title = 'My Hugo Site'
theme = 'mixedpaper'

It’s possible to set a section for deployments; however, it doesn’t seem to be well integrated with LocalStack, so this will be skipped for the time being.

Building the Website

A single command is needed to build the website:

1
hugo

A single command is needed to build the website:

1
hugo

Hugo will process all the Markdown content files and templates, generating a complete, hostable website in the public directory. The source files (Markdown, partials, etc.) will be converted into HTML/CSS/JS, which will be stored in the public directory. For better visualization, the structure of the public folder is presented below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
tree -L 1
.
├── 404.html
├── archives
├── categories
├── favicon.png
├── index.html
├── index.xml
├── links
├── p
├── page
├── post
├── scss
├── search
├── sitemap.xml
├── tags
└── ts

For additional details, check Hugo’s official documentation.

Once the website has been built and all the content is generated, the index page has to be set using the command:

1
2
# inside the generated public directory
awslocal s3 website s3://testwebsite/ --index-document index.html

Deploying to S3 Bucket

Deploying to the S3 bucket simply means uploading the public directory to the bucket. This can be achieved using the s3 sync command as follows:

1
awslocal s3 sync ./ s3://testwebsite

The site is now hosted on the S3 bucket testwebsite. To test, simply open the browser and paste the base URL: http://testwebsite.s3-website.localhost.localstack.cloud:4566.

It might not work on Chrome, Firefox, or other Chromium-based browsers. Edit the /etc/hosts file on your machine and add the line 127.0.0.1 testwebsite.s3-website.localhost.localstack.cloud for it to work. For more info, check the resolved GitHub issue.

Further Automation

To reduce the setup, a helper script was implemented and the Dockerfile was adjusted as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM localstack/localstack

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       hugo \
       ca-certificates \
       git \
       golang

WORKDIR /opt/code/localstack

COPY . .

RUN chmod +x ./setup.sh \
    && mkdir -p /etc/localstack/init/ready.d \
    && mv ./setup.sh /etc/localstack/init/ready.d/init-aws.sh

EXPOSE 4566

Below is a brief overview of the updated Dockerfile:

  • Copy the entire working directory into the container so that we can avoid using the -v mount option.
  • Set the script to executable and move it to /etc/localstack/init/ready.d/init-aws.sh so that it is automatically executed by LocalStack once the container is running.

Check the LocalStack initialization hooks for more info regarding the lifecycle.

With the newly enhanced and improved Dockerfile, the container is ready to use out of the box, without the need to perform any manual tasks.

Setup Script

Below is the implemented script for automating the setup within the container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# Define variables
BASE_DIR="/opt/code/localstack"
SITE_DIR="$BASE_DIR/hugo-mixedpaper"
BUCKET="testwebsite"
POLICY_FILE="bucket_policy.json"

# Set up the bucket
awslocal s3 mb s3://$BUCKET
awslocal s3api put-bucket-policy --bucket $BUCKET --policy file://$POLICY_FILE

# Build the site
cd $SITE_DIR && hugo

# Deploy to S3
cd "$SITE_DIR/public" && awslocal s3 sync . s3://$BUCKET

# Set the index page
awslocal s3 website s3://$BUCKET/ --index-document index.html

The script above handles:

  • Creation of the bucket.
  • Applying the bucket_policy.json file.
  • Building the website using hugo and generating the public folder.
  • Syncing the public folder to the S3 bucket.
  • Setting the index-document for the bucket.

Once the image is built and the container is running, we can directly access the website at the following endpoint: http://testwebsite.s3-website.localhost.localstack.cloud:4566

Don’t forget to update the /etc/hosts file to add the line 127.0.0.1 testwebsite.s3-website.localhost.localstack.cloud for it to work.

Conclusion

The setup explored in this post is mainly for local development and emulating a production-like environment. In short, we’ve managed to:

  • Extend the LocalStack container image to integrate Hugo and other dependencies.
  • Implement a script to automate the setup inside the container by performing the following tasks:
    • S3 bucket creation and configuration
    • Building and deploying the site to the created S3 bucket

The complete source code used here can be found here on GitHub.