As you probably know, this website is based on an amazing static website generator Hugo. I’ve been more than happy ever since I switched to it more than two years ago.

In the beginning, I deployed website directly from my local machine to the S3 and then invalidated Cloudfront cache as described in the article. This has been working well, but I had to automate it.

Website is in Git and sometimes when I’d do something to it I would build and deploy the website, but sometimes forget to push to Git (as we all do, right? :-) ). This was especially tricky when certain edits were done on the desktop, and I realized I have a typo, misspelling or could have phrased something better while at my laptop and not at home. Who wants that kind of hassle…

Anyhow, I decided to force myself to build and deploy only once things are pushed to Git, and to ensure that I had to have build and deploy setup configured there.

Github Actions

There’s nothing much fancy here, quite simple, monolith build and deploy process:

name: Deploy website to AWS S3

on:
  push:
    branches:
      - main
  workflow_dispatch:

# Required for Auth to AWS
permissions:
  contents: read
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these deployments to complete.
concurrency:
  group: "aws"
  cancel-in-progress: false

defaults:
  run:
    shell: bash

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: SOMEVERSION
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
          && sudo dpkg -i ${{ runner.temp }}/hugo.deb          

      - name: Checkout
        uses: actions/checkout@v3
        with:
          submodules: recursive
          fetch-depth: 0

      - name: Build with Hugo
        run: |
          hugo          

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-region: MYREGION
          role-to-assume: arn:aws:iam::MYAWSID:role/MYROLETOPUBLISHTOS3ANDINVALIDATECLOUDFRONTCACHE
          role-session-name: Github_to_AWS_via_OIDC

      - name: Deploy
        run: |
          hugo deploy          

So basically process consists of:

  • spin up Ubuntu container
  • download hugo.deb and install it
  • checkout repository
  • hugo
  • login to AWS
  • hugo deploy

Authenticating to AWS

Interesting part to you might be how I authenticate to AWS. Instead of using static credentials I’m using OIDC provider in AWS. Once I created token.actions.githubusercontent.com provider under Identity providers I created the role which we’re using in Github Actions above.

Role is created with following trust policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::MYAWSACCOUNT:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:ivantomica/MYREPOSITORY:*"
                }
            }
        }
    ]
}

I could have restricted actions further, and specify that only main branch can authenticate to the AWS, but I wanted to have some freedom there for further experimentation.

This Role also has 2 policies attached to it:

  • policy to invalidate cloudfront distribution cache
  • policy to write to s3 bucket

And policies are as follows.

Cloudfront policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "arn:aws:cloudfront::MYAWSACCOUNT:distribution/MYCLOUDFRONTDISTRIBUTIONID"
        }
    ]
}

S3 policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObjectVersion",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::mys3bucket",
                "arn:aws:s3:::mys3bucket/*"
            ]
        }
    ]
}

Conclusion

Voila, that’s it, nothing else required to automatically publish new version of the webiste every time I change something and push to Git. Whole Github Actions build/deploy process takes around 20 seconds which is quite fast by my standards.