Developer previews with AWS, Terraform and GitHub Actions

July 12, 2023 0 By addshore

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 to
  • DEPLOY_PREVIEW_KEY: AWS Key ID that can write to the bucket
  • DEPLOY_PREVIEW_SECRET: AWS Key secret needed for deployment
  • DEPLOY_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 previewCode 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