New static website hosting with Hugo, AWS S3, CloudFront, IAM, OAI and Github Actions

There’s a new way to host a static website in AWS using S3, CloudFront and IAM. This uses a new AWS CloudFront feature called OAI (origin access identity) that means CloudFront handles the access control, and you don’t use the static website hosting feature in S3 anymore.

This guide replaces the older “Deploying a Hugo site with Github Actions, S3, Cloudfront and AWS IAM” guide, which used the static website hosting feature in S3.

Hugo

You don’t need to do anything specifically to set up Hugo to work for static website hosting in AWS, but here’s one tip.

The extended version of Hugo is able to manage your SCSS for you, which means you don’t need to depend on another build system involving Node.js or Ruby.

To have Hugo build and include your SCSS for you, you can use this template code, for example in layouts/_default/baseof.html:

{{ $style := resources.Get "sass/main.scss" | resources.ToCSS | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $style.Permalink }}">

That compiles CSS from a SASS file at assets/main.scss and includes it as a normal stylesheet link element in the HTML.

Hugo can apply more advanced builds to CSS and JS with Babel, but this requires a Node.js dependency. It’s still easier to work with than a separate build tool, though, so I would recommend having Hugo do all of this for you.

S3

Create an S3 bucket. Do not enable static website hosting, and do not grant any public access on the bucket or its objects. CloudFront’s new OAI feature will handle the static website access control.

Note that the S3 bucket name no longer needs to match the domain name you’ll be using for the website, as this was only needed with the S3 static website hosting feature, which we’re not using here.

I seemed to have an issue where CloudFront didn’t like the S3 bucket name containing a dot (.), and was getting vague errors about the bucket not existing. When using an S3 bucket name that only contains ascii letters, I no longer hit this issue.

CloudFront with OAI

Create a CloudFront distribution. The origin should be the S3 bucket you just created. Enable the OAI feature and let CloudFront set up the policy for you.

Add a CNAME for the domain name you want and set whatever other config you want in CloudFront, such as an ACM TLS certificate, redirecting HTTP to HTTPS and so on.

The main thing to get right in the CloudFront config here is the OAI setting to enable serving a static website out of S3 via CloudFront.

IAM

Create an AWS IAM policy.

You can use the visual policy editor, or paste in JSON like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutBucketPolicy",
        "s3:ListBucket",
        "s3:DeleteObject",
        "s3:PutObjectAcl",
        "cloudfront:CreateInvalidation",
        "s3:GetBucketPolicy"
      ],
      "Resource": [
        "arn:aws:cloudfront::{aws-account-id}:distribution/{cloudfront-distribution-id}",
        "arn:aws:s3:::{s3-bucket-name}",
        "arn:aws:s3:::{s3-bucket-name}/*"
      ]
    }
  ]
}

You need to replace three placeholders in that JSON: {aws-account-id}, {s3-bucket-name} and {cloudfront-distribution-id}in that example. It’s probably easiest to paste in the JSON and then use the visual policy editor in the AWS console UI to get the right resource ARNs in place.

Then create a new programmatic-access IAM user and attach the new policy to it directly. This user will be used by the Github Action to deploy your website in S3 and trigger an invalidation in CloudFront.

Copy and save the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for this new user somewhere safe as you’ll need to add them as Github secrets in the next step.

Github

In the Github UI for your repository, go to Settings and then Secrets. Create a secret for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with the values you just got from AWS IAM for the new programmatic-access user.

In your Github repo, create a file at .github/workflows/build.yml. This will control the Github Action that will automatically build and deploy your static site on push.

The content of the Github Action YAML file should be like this:

name: Build and Deploy

on: push

jobs:
  build:
    name: Build and Deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Install Hugo
        run: |
          HUGO_DOWNLOAD=hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz
          wget https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_DOWNLOAD}
          tar xvzf ${HUGO_DOWNLOAD} hugo
          mv hugo $HOME/hugo
        env:
          HUGO_VERSION: 0.85.0
      - name: Hugo Build
        run: $HOME/hugo -v
      - name: Deploy to S3
        if: github.ref == 'refs/heads/master'
        run: aws s3 sync public/ s3://{s3-bucket-name}/ --delete --acl=public-read && aws cloudfront create-invalidation --distribution-id={cloudfront-distribution-id} --paths='/*'
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Again there are some placeholders to replace: {s3-bucket-name} and {cloudfront-distribution-id}. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY will be supplied by the Github secrets you just created, so don’t set those in the YAML.

Commit and push your Github repo. If everything has been set up correctly, it should deploy your static site to AWS and make it available on the web within a few seconds.


Tech mentioned