Development · February 2020
Deploy Previews with AWS and Github Actions
Build Netlify-like Deploy Previews with AWS and Github Actions


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.js2module.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 Preview23on: pull_request45jobs:6 deploy:7 runs-on: ubuntu-latest8 steps:9 - uses: actions/checkout@v11011 - name: Cache npm12 uses: actions/cache@v113 with:14 path: ~/.npm15 key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}16 restore-keys: |17 ${{ runner.os }}-node-1819 - name: Set deployment status20 uses: pbrandone/create-status-action@master21 with:22 token: ${{ secrets.GITHUB_TOKEN }}23 state: pending24 description: Preparing deploy preview25 context: Deploy Preview URL2627 - name: Install28 run: npm ci2930 - name: Build31 run: PATH_PREFIX=preview/${{ github.event.number }} npm run build -- --prefix-paths3233 - name: Deploy34 if: success()35 uses: jakejarvis/s3-sync-action@master36 with:37 args: --delete38 env:39 AWS_S3_BUCKET: YOUR_BUCKET_NAME # Could also come from github secrets40 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 }}4546 - name: Set success deployment status47 if: success()48 uses: pbrandone/create-status-action@master49 with:50 token: ${{ secrets.GITHUB_TOKEN }}51 state: success52 description: Deploy preview ready!53 url: https://yourdomain.com/preview/${{ github.event.number }}54 context: Deploy Preview URL5556 - name: Set failed deployment status57 if: failure()58 uses: pbrandone/create-status-action@master59 with:60 token: ${{ secrets.GITHUB_TOKEN }}61 state: failure62 description: Failed to deploy preview63 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 message2 if: success()3 uses: unsplash/comment-on-pr@master4 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 }}