☁️ 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:
- GitHub repository with Jekyll site, in my case it is https://github.com/fisenkodv/home-page/
- An AWS account
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
- 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
- Create a new IAM User with programmatic access and attach the IAM Policy you just created above.
- Copy the AWS Access Key ID and Secret Access Key to somewhere safe, as we will need these later.
- 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 toAWS_S3_BUCKET_NAME
S3 bucket - Creates a CloudFront invalidation
- Checkouts
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 ResourcesAWS_SECRET_ACCESS_KEY
- Secret Access Key, refer to Step 1. Set Up AWS ResourcesAWS_S3_BUCKET_NAME
- The bucket name from IAM Policy, refer to Step 1. Set Up AWS ResourcesAWS_CLOUDFRONT_DISTRIBUTION_ID
- The CloudFront distribution ID from IAM Policy, refer to Step 1. Set Up AWS Resources
Step 3. Trigger Deployment
- Push any changes to the
main
branch, and GitHub Actions will automatically trigger the deployment workflow. - Check the Actions tab in the GitHub repository to monitor the progress.
- 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.