Preview URL for Every Pull Request with GitHub & AWS

April 20, 2025 (6mo ago)

In a robust development workflow, the ability to reliably preview changes before they're merged into production is critical. One powerful approach to achieving this is creating a unique, isolated environment for each Pull Request (PR). This allows your team to thoroughly test, review, and validate new features or bug fixes without affecting your production environment.

In this guide, we'll explore a structured approach to setting up automated preview URLs for your PRs using GitHub Actions and AWS. We'll cover essential considerations, architectural decisions, and practical implementation details.

Why Preview Environments Matter#

Preview environments solve several critical challenges faced by development teams:

Now, let's dig into how you can set this up in a real-world scenario using GitHub and AWS.

Understanding the Architecture#

Here’s the typical production environment we're referencing:

Our goal is clear: every PR should have its own isolated environment with:

Implementing Preview Environments#

To automate this process effectively, GitHub Actions is a natural fit, providing the following capabilities:

  1. Automatic triggers on PR lifecycle events.
  2. Docker image builds for frontend and backend components.
  3. Automated deployments to AWS services (ECS for backend, S3/CloudFront for frontend).
  4. Dynamic routing and URL management.
  5. Resource cleanup post-PR closure.

Before diving into code examples, let's first discuss some key architectural decisions.

Architectural Considerations#

DNS & Subdomain Structure#

Instead of relying on a complex DNS setup with Route 53, you can leverage existing AWS infrastructure efficiently by combining your Application Load Balancer (ALB) and CloudFront with Lambda@Edge:

Backend Routing with ALB#

Your existing ALB can dynamically route backend requests to the appropriate ECS tasks using host-based routing rules. For example:

Frontend Routing with CloudFront & Lambda@Edge#

For frontend applications, leverage CloudFront combined with Lambda@Edge functions:

This approach simplifies DNS management significantly, avoiding the overhead and complexity of creating numerous individual DNS records for each preview environment.

Database Strategy#

Managing databases for previews presents two primary strategies:

Option 1: Shared Database#

A single RDS database shared among all preview environments, with isolation achieved through schemas or table prefixes.

Pros:

Cons:

Option 2: Dedicated Database per PR#

Deploy a dedicated PostgreSQL instance per PR using Docker containers on ECS, optionally with persistent volume mounts and data seeding.

Pros:

Cons:

For most professional teams, particularly those working on sensitive features or requiring thorough integration testing, a dedicated ECS-hosted Docker database per PR is recommended. For smaller teams or simpler setups, a shared database with strict conventions could suffice.

Data Seeding Strategy#

Effective data seeding is crucial to making preview environments useful and reliable. Consider the following guidelines:

Automating the Setup#

With these decisions made, here's how the automation flow looks using GitHub Actions:

Enhancing Security and Cost Efficiency#

To maintain secure and cost-efficient operations:

GitHub Actions Workflow (Example)#

Here's a simplified example of a GitHub Actions workflow:

on:
  pull_request:
    types: [opened, synchronize, closed]
 
jobs:
  deploy_preview:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: echo "PR_ID=pr${{ github.event.pull_request.number }}" >> $GITHUB_ENV
      - run: |
          docker build -t frontend:preview ./apps/web-app
          docker build -t backend:preview ./apps/backend
      - run: ./scripts/deploy-preview.sh $PR_ID
      - uses: marocchino/sticky-pull-request-comment@v2
        with:
          message: |
            Preview environment ready:
            - Frontend: https://$PR_ID.preview.app.example.com
            - Backend: https://$PR_ID.preview.api.example.com
 
  cleanup_preview:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: echo "PR_ID=pr${{ github.event.pull_request.number }}" >> $GITHUB_ENV
      - run: ./scripts/cleanup-preview.sh $PR_ID

Final Thoughts#

Setting up automated preview environments isn't just about enhancing your CI/CD process—it's a fundamental shift toward higher quality and safer deployments. While it requires upfront investment in infrastructure and automation, the payoff is significant: improved code quality, streamlined reviews, and increased confidence across your team.

Taking the time to implement this workflow properly will transform your development process, setting a solid foundation for continued growth and stability.

Start small, iterate, and scale the solution as your team and needs evolve.