Developer previews with AWS, Terraform and GitHub Actions
As a developer, one of the most critical aspects of your workflow is the ability to test and preview your code changes before deploying them to production. This is where developer previews come in.
This post will outline how to create your own simple developer preview system, using Github Actions for building, AWS S3 for hosting, and Terraform to provision it all. Giving you more control, and a lower cost.
Shout out to Pedro Brandão from Significa whose post I read as inspiration for this setup.

What are Developer Previews?
Developer previews, also known as feature branches or pull request previews, allow developers to create isolated environments to test their changes without impacting the main production environment. It enables teams to collaborate, review, and validate code before merging it into the main branch. With developer previews, you can catch bugs, validate new features, and gather feedback early in the development process, ensuring a smoother deployment to production.
Existing services
Many dedicated platforms exist that offer developer preview as a service. These platforms provide a streamlined solution for creating and managing isolated environments for testing code changes. Examples of such services include Netlify’s Deploy Previews, Vercel’s Preview Deployments, and Heroku Review Apps. These platforms integrate seamlessly with popular version control systems and automatically deploy feature branches or pull requests, allowing developers to easily preview their changes. By leveraging these dev-preview services, developers can simplify the process of creating and managing preview environments, enabling faster iteration and effective collaboration within development teams.
However, all these services have a cost, which will always be higher than the do-it-yourself approach.
Deploying AWS Resources with Terraform
To provision and manage your AWS resources efficiently, you can use Terraform—a popular Infrastructure as Code (IaC) tool. Terraform allows you to define your infrastructure in declarative configuration files, making it easy to version, collaborate on, and automate your infrastructure provisioning process.
With Terraform, you can define your developer preview environment as code, including AWS resources, networking, security settings, and any other dependencies. This approach ensures consistency across different developer previews and simplifies the process of creating and tearing down environments.
I have created a Terraform module that you can find in a GitHub gist for download that includes everything you need.
In summary:
- The AWS provider is required to connect to your AWS account
- An S3 bucket is created
- The bucket is set up to allow public reading
- The bucket is set up with a website config, with index.html as the root document
- Files in the bucket are set to delete after 90 days (configurable)
- An IAM user is created
- A key is created for the user, and output
- The user is given access to write to the bucket
- A CloudFront distribution is created for the bucket. Allowing HTTPS access via a domain behind CloudFront
- Caching is disabled
- Access is restricted to some regions
The use of the module is rather simple:
module "dev-preview-0002" {
source = "./modules/dev-preview"
bucket_name = "dev-preview-421984121-002"
}
Code language: JavaScript (javascript)
There is potentially an issue/race condition on creation, and a second apply might be needed.
GitHub Actions
This action is an updated version used in the blog post that I read, making use of new actions, including needed permissions, and tweaking variables for the project I have.
Before using the action, you’ll need to deploy some secrets to the GitHub repository.
DEPLOY_BUCKET_NAME
: The bucket you created above to deploy toDEPLOY_PREVIEW_KEY
: AWS Key ID that can write to the bucketDEPLOY_PREVIEW_SECRET
: AWS Key secret needed for deploymentDEPLOY_REGION
: Region of the bucket
The project is using node 14, can already have a --base-href
passed in at build time to allow for a different URL path. It is output to dist
and once deployed, needs to be loaded at /index.html
.
The deployment will happen to the path admin-portal/${{ github.event.number }}
within the bucket, so multiple PRs can be deployed at a time without stepping on the others toes.
name: Deploy Preview
on:
pull_request:
# https://github.com/ouzi-dev/commit-status-updater/tree/v2/#workflow-permissions
permissions:
contents: read
statuses: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14
- 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: ouzi-dev/commit-status-updater@v2
with:
name: Deploy Preview
status: pending
description: Preparing deploy preview
- name: Install
run: npm ci
- name: Build
run: npm run build -- --configuration production --aot --base-href /admin-portal/${{ github.event.number }}/
- name: Deploy
if: success()
uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --delete
env:
AWS_S3_BUCKET: ${{ secrets.DEPLOY_BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.DEPLOY_PREVIEW_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEPLOY_PREVIEW_SECRET }}
AWS_REGION: ${{ secrets.DEPLOY_REGION }}
SOURCE_DIR: "dist"
DEST_DIR: admin-portal/${{ github.event.number }}
- name: Set success deployment status
if: success()
uses: ouzi-dev/commit-status-updater@v2
with:
name: Deploy Preview
status: success
description: Deploy preview ready!
url: https://${{ secrets.DEPLOY_PREVIEW_DOMAIN }}/admin-portal/${{ github.event.number }}/index.html
- name: Set failed deployment status
if: failure()
uses: ouzi-dev/commit-status-updater@v2
with:
name: Deploy Preview
status: failure
description: Failed to deploy preview
Code language: PHP (php)
Once this is merged into your main
branch, creating a pull request will build the application, and push it to the S3 bucket