1 Feb 2020
Build Netlify-like Deploy Previews with AWS and Github Actions.
We love AWS, and it's, without a doubt, our go-to choice for building our infrastructures. It's flexible, powerful and very capable but – if we're honest – sometimes it's a bit daunting. The ease of use of services like Netlify or Heroku can go a long way to improve the overall developer experience.
One of the things we missed the most from Netlify was its Deploy Previews. For marketing websites, there is no better way to quickly iterate: as soon as there is an open Pull Request against our master branch, we can gather feedback, do proper Quality Assurance sessions and simply keep on improving the current branch with all-team collaboration instead of the usual "code review first > merge to a staging branch > gather feedback > new branch > rinse and repeat".
That's why we needed to bring it to AWS. This quick tutorial is an example of how to achieve this behaviour with Github Actions and AWS for a Gatsby website (although it could easily be adapted to something else).
When a Pull Request is opened, or someone commits to it, we should make a new build, deploy it to somedomain.com/preview/<pr-number>
and post back the URL as a comment.
This tutorial is not about setting up AWS to host websites, so we'll just go through it as quickly as possible. We recommend automating these steps with a CloudFormation template, but for the sake of simplicity, we'll just go over it in the console.
You're going to need at least an S3 Bucket for public web hosting and an IAM user with the necessary permissions. We recommend at least setting up Route 53 for a custom domain and - if you want HTTPS - Cloudfront with a certificate from Certificate Manager.
Feel free to skip this part if you already have what you need.
Go to S3 and click on + Create Bucket. If you're not planning on adding HTTPS, make sure the DNS-compliant name of the bucket is the domain you wish to use (e.g., somedomain.com). This way, we can create a record directly to this bucket on Route 53.
Go over to step 3 of the modal and unselect Block all public access.
After the bucket is created, go over to Properties, then to Static website hosting and select Use this bucket to host a website. Your index and error documents probably are index.html and 404.html, respectively.
Go over to Permissions, then to Bucket Policy, and paste this policy (changing the bucket name).
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
}
]
}
Optionally, you can also make S3 automatically delete your Deploy Previews after a while:
Go to Management > Lifecycle and click + Add lifecycle rule;
Give it a name (e.g., Deploy Preview Expire);
In the Prefix/tags filter, add preview/
as a prefix;
Skip transitions and in the Expiration tab, tick all the boxes and add the desired values.
If you want to route directly to the bucket without HTTPS, you just need to create a new A Record Set for your bucket:
Go over to Route53 and open your hosted zone;
Add a new A Record Set;
Select Alias;
Select your bucket from the S3 list.
If you want HTTPS, you must first request a Certificate from Certificate Manager.
Then, go over to Cloudfront, create a new Web Distribution and:
Paste your bucket public URL in Origin Domain Name;
Select Redirect HTTP to HTTPS;
Given we're just using CloudFront for the HTTPS, select Customize in Object Caching and make them all 0;
Select Yes for Compress Objects Automatically;
Add your desired CNAMEs and select your newly created Certificate.
Finally, go over to Route 53 and change the value of the recently created A Record to point to CloudFront instead of S3.
Most times, websites are built to be deployed to the root of the domain but here we have a problem: given we are not deploying to /
, our relative links and links to resources (e.g., images) won't work.
The first thing we thought of was using the HTML5 base tag. Luckily we didn't have to (feel free to try it anyway and let us know your results!)
Instead of having to conditionally add some markup, Gatsby provides a very helpful Path Prefix feature that does exactly what we were looking for. Our path prefix needs to change with every PR, so we just leveraged environment variables to define it.
// gatsby.config.js
module.exports = {
pathPrefix: process.env.PATH_PREFIX,
...
}
For Gatsby to look at this path, we just need to build the website with gatsby build --prefix-paths
. Perfect! 🎉
We'll use jakejarvis/s3-sync-action to deploy and pbrandone/create-status-action to add the deployed URL as a commit status.
The configuration for the workflow is very straightforward. We just have remember to build with PATH_PREFIX=preview/${{ github.event.number }} npm run build -- --prefix-paths
Just make sure you have the necessary environment variables in your repo's Settings > Secrets and you are good to go!
name: Deploy Preview
on: pull_request
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Cache npm
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Set deployment status
uses: pbrandone/create-status-action@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
state: pending
description: Preparing deploy preview
context: Deploy Preview URL
- name: Install
run: npm ci
- name: Build
run: PATH_PREFIX=preview/${{ github.event.number }} npm run build -- --prefix-paths
- name: Deploy
if: success()
uses: jakejarvis/s3-sync-action@master
with:
args: --delete
env:
AWS_S3_BUCKET: YOUR_BUCKET_NAME # Could also come from github secrets
AWS_ACCESS_KEY_ID: ${{ secrets.DEPLOY_PREVIEW_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEPLOY_PREVIEW_SECRET }}
AWS_REGION: "eu-west-1"
SOURCE_DIR: "public"
DEST_DIR: preview/${{ github.event.number }}
- name: Set success deployment status
if: success()
uses: pbrandone/create-status-action@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
state: success
description: Deploy preview ready!
url: https://yourdomain.com/preview/${{ github.event.number }}
context: Deploy Preview URL
- name: Set failed deployment status
if: failure()
uses: pbrandone/create-status-action@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
state: failure
description: Failed to deploy preview
context: Deploy Preview URL
Feel free to check our website's open-source repo and see it in the wild:
https://github.com/significa/significa.co
In this tweet, @swyx and @sebastienlorber were kind enough to correctly point out that commenting on the deployed URL per commit will easily clutter the Pull Request's discussion.
The snippet above was updated to use GitHub Status instead of commenting.
If you still want to go with the comments approach with unsplash/comment-on-pr to post the URL comment back to the PR, this is what we had:
- name: Deploy message
if: success()
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: |
Deploy preview ready!
https://yourdomain.com/preview/${{ github.event.number }}
built from ${{ github.sha }}
Pedro Brandão
Managing Partner
Pedro el patron Brandão is the founder and CEO at Significa. Pedro’s playlist is made entirely of songs no one has ever listened to.
Nuno Polónia
Front-End Developer
18 October 2024
Master JavaScript web animations with requestAnimationFrame.Significa
Team
30 September 2024
Optimise your e-commerce website for better performance.Significa
Team