Post

☁️ Deploy Jekyll site to AWS using GitHub Actions

Harnessing the power of GitHub Actions can significantly streamline and automate the building and deployment procedures for your repositories.

In the forthcoming blog post, I will guide you through the steps to effortlessly build and deploy your Jekyll static site onto AWS S3, seamlessly integrated with CloudFront, all made possible through the seamless orchestration of GitHub Actions.

Prerequisites

Before we dive into the deployment process, make sure you have the following prerequisites in place:

Desired Workflow

The desired workflow should align with the following sequence:

  • Upon pushing changes to the repository’s main branch or when manually triggering the GitHub action
  • Build the main branch
  • Deploy the generated static site files to AWS S3
  • Create an AWS CloudFront invalidation

Step 1. Set Up AWS Resources

We’ll need to create the following AWS resources:

S3 Bucket

Create <YOUR-BUCKET-NAME> S3 bucket where future generated static site files be stored later.

CloudFront Distribution

Create a CloudFront distribution with the origin pointed to <YOUR-BUCKET-NAME>. If Origin access is set to Legacy access identities then need to make sure that Origin access identity has been created.

Due to Jekyll’s default behavior of generating index.html files and formatting links without appending index.html to URLs (although this can be configured differently), CloudFront may not automatically recognize the necessity to retrieve the index.html file from the S3 bucket.

To help CloudFront with this, we can create a function in the CloudFront like below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handler(event) {
  const request = event.request;
  const uri = request.uri;

  // Check whether the URI is missing a file name.
  if (uri.endsWith("/")) {
    request.uri += "index.html";
  }
  // Check whether the URI is missing a file extension.
  else if (!uri.includes(".")) {
    request.uri += "/index.html";
  }

  return request;
}

And then specify in Function associations in the CloudFront distribution behavior. See more details in Tutorial: Creating a simple function with CloudFront Functions.

Configuring Alternate domain names is out of the scope of this post, but feel free to read Using custom URLs by adding alternate domain names (CNAMEs).

IAM User

  1. Create a new IAM Policy. Which will grant restricted access to deploy to <YOUR-BUCKET-NAME> S3 bucket and create an invalidation on our CloudFront distribution. Below is the AWS IAM Policy you’ll need to create. You must modify it by replacing a couple of the items below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Resource": [
        "arn:aws:s3:::<YOUR-BUCKET-NAME>",
        "arn:aws:s3:::<YOUR-BUCKET-NAME>/*"
      ],
      "Effect": "Allow",
      "Action": ["s3:*"]
    },
    {
      "Effect": "Allow",
      "Action": ["cloudfront:CreateInvalidation"],
      "Resource": "arn:aws:cloudfront::<YOUR-AWS-ACCOUNT-NUMBER>:distribution/<YOUR-BUCKET-NAME>"
    }
  ]
}

Where:

  • <YOUR-BUCKET-NAME> - Your S3 bucket name (ex: www.fisenko.net)
  • <YOUR-AWS-ACCOUNT-NUMBER> - The 12 numeric characters of your AWS account.
  • <YOUR-BUCKET-NAME> - The alphanumeric 14 characters of your associated CloudFront distribution
  1. Create a new IAM User with programmatic access​ and attach the IAM Policy you just created above.
  2. Copy the AWS Access Key ID and Secret Access Key to somewhere safe, as we will need these later.
  3. Make sure your S3 bucket permission policy has required permissions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::<YOUR-BUCKET-NAME>",
        "arn:aws:s3:::<YOUR-BUCKET-NAME>/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <OAI_ID>"
      },
      "Action": ["s3:GetObject*", "s3:GetBucket*", "s3:List*"],
      "Resource": [
        "arn:aws:s3:::<YOUR-BUCKET-NAME>",
        "arn:aws:s3:::<YOUR-BUCKET-NAME>/*"
      ]
    }
  ]
}

Step 2. Add a GitHub Action Workflow

Configure The GitHub Action

GitHub Actions definitions are stored in a dedicated directory within the repository (<repo>/.github/workflows/). Within this directory, all the workflow files are housed.

Workflows will trigger off events that happen in GitHub. In this particular scenario, our emphasis will be on events such as push and workflow_dispatch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
name: "Build and Deploy"
on:
  push:
    branches:
      - main
      - master
    paths-ignore:
      - .gitignore
      - README.md
      - LICENSE

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID  }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY  }}
  AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION  }}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          # submodules: true
          # If using the 'assets' git submodule from Chirpy Starter, uncomment above
          # (See: https://github.com/cotes2020/chirpy-starter/tree/main/assets)

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3 # reads from a '.ruby-version' or '.tools-version' file if 'ruby-version' is omitted
          bundler-cache: true

      - name: Build site
        run: bundle exec jekyll b -d "_site$"
        env:
          JEKYLL_ENV: "production"

      - name: Test site
        run: |
          bundle exec htmlproofer _site \
            \-\-disable-external=true \
            \-\-ignore-urls "/^http:\/\/127.0.0.1/,/^http:\/\/0.0.0.0/,/^http:\/\/localhost/"

      - name: "Deploy to AWS S3"
        run: aws s3 sync ./_site/ s3://$ --delete --cache-control max-age=604800

      - name: "Create AWS Cloudfront Invalidation"
        run: aws cloudfront create-invalidation --distribution-id $ --paths "/*"

It’s pretty self-explanatory, but let’s go through the process:

  • When a push is made into the main branch (or manual button click in GitHub), this workflow runs
  • The job uses the latest Ubuntu virtual environment and runs the following steps
    • Checkouts main branch
    • Setups Ruby environment
      • Installs specified Ruby version and runs ‘bundle install’
    • Builds the site for the production environment
    • Runs tests
    • Uploads output files from our \_site directory to AWS_S3_BUCKET_NAME S3 bucket
    • Creates a CloudFront invalidation

Pretty straightforward, but we still need to create a few resources in AWS and configure secrets in our GitHub repository.

Configure The GitHub Action Secrets

To use the special variables like ${{ secrets.AWS_ACCESS_KEY_ID }} we’ll need to configure them in the GitHub Actions Secrets. To do this:

  • In GitHub, navigate to Repository > Settings > Secrets and variables > Actions
  • For each secret below, click the New repository secret button, fill out the form, and click Add Secret
    • AWS_ACCESS_KEY_ID - AWS Access Key ID, refer to Step 1. Set Up AWS Resources
    • AWS_SECRET_ACCESS_KEY - Secret Access Key, refer to Step 1. Set Up AWS Resources
    • AWS_S3_BUCKET_NAME - The bucket name from IAM Policy, refer to Step 1. Set Up AWS Resources
    • AWS_CLOUDFRONT_DISTRIBUTION_ID - The CloudFront distribution ID from IAM Policy, refer to Step 1. Set Up AWS Resources

Step 3. Trigger Deployment

  1. Push any changes to the main branch, and GitHub Actions will automatically trigger the deployment workflow.
  2. Check the Actions tab in the GitHub repository to monitor the progress.
  3. Navigate to the website.

Conclusion 🎉

Great job! The deployment of your Jekyll site to AWS through GitHub Actions, S3, and CloudFront has been successfully automated. This efficient workflow guarantees that your website remains current and operates at its best for users. As you progress with site development, this deployment pipeline will be an indispensable tool in sustaining a seamless and effective workflow.

This post is licensed under CC BY 4.0 by the author.