Development · February 2020

Deploy Previews with AWS and Github Actions

Build Netlify-like Deploy Previews with AWS and Github Actions

Pedro BrandãoFounder & CEO

We love AWS and it's, without a doubt, our go-to choice to build 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).

Main goal

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.

AWS

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 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.

S3 Bucket

  • 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 > 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 > Bucket Policy" and paste this policy (changing the bucket name);

1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "PublicReadGetObject",
6 "Effect": "Allow",
7 "Principal": "*",
8 "Action": "s3:GetObject",
9 "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
10 }
11 ]
12}

Deploy Preview Expiration

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 "prefix/tags filter" add `preview/` as a prefix;
  • Skip transitions and in the Expiration tab tick all the boxes and add the desired values;

Route 53

If you want to route directly to the bucket without HTTPS, you just need to create a new A Record Set to 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;

Cloudfront and HTTPS (Optional)

If you want HTTPS, you first need to 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.

Relative links and assets

Most times, websites are built to be deployed to the root at 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.

1// gatsby.config.js
2module.exports = {
3  pathPrefix: process.env.PATH_PREFIX,
4  ...
5}

For Gatsby to look at this path, we just need to build the website with gatsby build --prefix-paths. 🎉 Perfect!

Github Actions

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 to 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!

1name: Deploy Preview
2
3on: pull_request
4
5jobs:
6 deploy:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v1
10
11 - name: Cache npm
12 uses: actions/cache@v1
13 with:
14 path: ~/.npm
15 key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
16 restore-keys: |
17 ${{ runner.os }}-node-
18
19 - name: Set deployment status
20 uses: pbrandone/create-status-action@master
21 with:
22 token: ${{ secrets.GITHUB_TOKEN }}
23 state: pending
24 description: Preparing deploy preview
25 context: Deploy Preview URL
26
27 - name: Install
28 run: npm ci
29
30 - name: Build
31 run: PATH_PREFIX=preview/${{ github.event.number }} npm run build -- --prefix-paths
32
33 - name: Deploy
34 if: success()
35 uses: jakejarvis/s3-sync-action@master
36 with:
37 args: --delete
38 env:
39 AWS_S3_BUCKET: YOUR_BUCKET_NAME # Could also come from github secrets
40 AWS_ACCESS_KEY_ID: ${{ secrets.DEPLOY_PREVIEW_KEY }}
41 AWS_SECRET_ACCESS_KEY: ${{ secrets.DEPLOY_PREVIEW_SECRET }}
42 AWS_REGION: "eu-west-1"
43 SOURCE_DIR: "public"
44 DEST_DIR: preview/${{ github.event.number }}
45
46 - name: Set success deployment status
47 if: success()
48 uses: pbrandone/create-status-action@master
49 with:
50 token: ${{ secrets.GITHUB_TOKEN }}
51 state: success
52 description: Deploy preview ready!
53 url: https://yourdomain.com/preview/${{ github.event.number }}
54 context: Deploy Preview URL
55
56 - name: Set failed deployment status
57 if: failure()
58 uses: pbrandone/create-status-action@master
59 with:
60 token: ${{ secrets.GITHUB_TOKEN }}
61 state: failure
62 description: Failed to deploy preview
63 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

Update ⚠️

In this tweet, @swyx and @sebastienlorber were kind enough to correctly point out that commenting 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:

1- name: Deploy message
2 if: success()
3 uses: unsplash/comment-on-pr@master
4 env:
5 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6 with:
7 msg: |
8 Deploy preview ready!
9 https://yourdomain.com/preview/${{ github.event.number }}
10 built from ${{ github.sha }}
TutorialAWSContinuous DeploymentGatsby

Pedro Brandão

Founder & CEO @ Significa

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.