S3rcel: Deploying PR Previews for React Using AWS S3 & CloudFront

Kartik MehtaKartik Mehta
9 min read

Introduction

If you've used Vercel, you've likely seen how it provides PR Preview Deployments — a feature that generates a unique URL for each pull request, allowing developers to test changes before merging. This is incredibly useful for frontend applications, as it enables quick feedback cycles.

I wanted to replicate this system (as a developer, because why not?) using AWS Infrastructure, so I built my own, S3rcel — A Pull Request Preview / Deployment System for React applications, leveraging:

  • GitHub Actions for automation

  • AWS S3 for static file storage

  • CloudFront for fast global delivery

Image

In this guide, I'll walk you through how to set up AWS, configure GitHub, and explain the workflows powering this deployment system. I will mostly be focusing on setting up AWS, so I'll provide you with everything else you need to get started.

Prerequisites

Before getting started, make sure you have:

  1. An AWS account with root access or privileges to create an IAM user with the necessary permissions.

  1. Fork the Repository: To streamline the process and focus more on setting up AWS, you can start by forking the repository below. The following repository contains three workflows and a basic React application with Tailwind CSS styling. I will discuss the workflows once we are done with the AWS setup.

Once you have forked the repository, you need to set up the following secrets in your repository under Settings → Secrets and Variables → Actions as we continue progressing through the blog.

Secret NameDescription
AWS_ACCESS_KEY_IDYour AWS access key ID with permissions for S3 and CloudFront
AWS_SECRET_ACCESS_KEYYour AWS secret access key
AWS_REGIONThe AWS region where your S3 bucket is located (e.g., ap-south-1)
AWS_S3_BUCKET_NAMEThe name of your S3 bucket for storing the previews
CLOUDFRONT_DISTRIBUTION_IDThe ID of your CloudFront distribution
CLOUDFRONT_DISTRIBUTION_DOMAIN_NAMEThe domain name of your CloudFront distribution

As we have completed the initial setup, let's log in to the AWS Console and start setting up the environment.

Setup - S3 Bucket

  1. Go to the AWS S3 Console.

  2. Click Create bucket.

  3. Set a unique bucket name.

  4. Uncheck "Block all public access".

  5. Leave the other settings as default.

  6. Click Create bucket. You will see something like this: (Your bucket will be empty initially.)

  7. Enable Static Website Hosting: Go to the Properties tab, and scroll down to the bottom.

    1. Click on Edit.

    2. Enable Static website hosting.

  8. Set up the Bucket Policy: Go to the Permissions tab, paste the following policy under Bucket Policy, and save it. (Make sure to replace "your-bucket-name" with the actual name of your bucket.)

     {
         "Version": "2012-10-17",
         "Statement": [
             {
                 "Effect": "Allow",
                 "Principal": "*",
                 "Action": "s3:GetObject",
                 "Resource": "arn:aws:s3:::<your-bucket-name>/*"
             }
         ]
     }
    

    This S3 bucket policy allows public read access to all objects in the specified bucket name. The "Principal": "*" means anyone can access it, while "Action": "s3:GetObject" permits reading objects. The "Resource" specifies the bucket and its contents.

  9. Check the AWS Region: You can find the region in the AWS Management Console at the top-right corner. Make sure you're using the correct region for your setup.

Alright, the S3 setup is complete! Now, you can add the following two secrets in your GitHub repository under:

AWS_REGIONThe AWS region where your S3 bucket is located.
AWS_S3_BUCKET_NAMEThe name of your S3 bucket for storing the previews.

Let's start setting up AWS CloudFront.

Setup - CloudFront

  1. Go to the AWS CloudFront Console.

  2. Click Create Distribution.

  3. Set Origin Domain Name to the S3 bucket URL.

  4. Set Viewer Protocol Policy to Redirect HTTP to HTTPS.

  5. Scroll down to the Settings section and set the Default Root Object to index.html.

  6. Click Create Distribution.

We have completed the CloudFront setup. Now, go to the newly created distribution. You should be able to see the CloudFront Distribution ID and the Distribution Domain Name.

Now, you can add the following two secrets in your GitHub repository under:

CLOUDFRONT_DISTRIBUTION_IDThe ID of your CloudFront distribution
CLOUDFRONT_DISTRIBUTION_DOMAIN_NAMEThe domain name of your CloudFront distribution (without https://)

Now, the only remaining step is to obtain the Access Key ID and Secret Access Key.

Setup - IAM

  1. Go to AWS Console > IAM.

  2. On the left-hand side, under Access Management, click on Users. This is where you can create users and generate their access keys and secrets. Click Create user.

  3. Enter a username and click Next to proceed.

  4. In the Permissions section, select Attach policies directly.

    Scroll down to view the available policies. We only need to attach two policies:

    • CloudFrontFullAccess

    • AmazonS3FullAccess

Select these policies and click Next to proceed.

  1. Click Create user. You have successfully created the user.

  2. Now, to generate the Access Key ID and Secret Access Key for this user, select the user you just created. Navigate to the Security Credentials tab.

  3. Scroll down to the Access Keys section and click Create access key.

  4. Select Command Line Interface (CLI) as the use case, acknowledge the warning at the bottom, and click Next to proceed.

  5. Click Create access key. You can see your Access Key ID and Secret Access Key.

Now, you can add the following two secrets in your GitHub repository under:

AWS_ACCESS_KEY_IDYour AWS access key ID with permissions for S3 and CloudFront
AWS_SECRET_ACCESS_KEYYour AWS secret access key

We have completed all the necessary configurations in the AWS Console.

Now, it's time to set up GitHub and go through the workflows that will handle PR Previews and Deployments automatically.

Setup - GitHub

  1. I assume you have already set up the required secrets under Settings → Secrets and Variables → Actions, so I'll skip that step and move forward with the next part of the setup.

  2. Go to Settings → Actions → General in your GitHub repository and ensure your settings match the following configuration:

Workflow permissionsRead and write permissions. Check - Allow GitHub Actions to create and approve pull requests.
Approval for running fork pull request workflows from contributorsRequire approval for first-time contributors.
Actions permissionsAllow all actions and reusable workflows.

Great! We’ve completed all the setup. Now, we’re ready to create a Pull Request (PR) with changes and watch the workflow in action.

Understanding the Workflows

But wait! Before we dive in, let’s take a moment to understand the workflow that powers and supercharges this project.

PR Preview Deployment

name: Deploy PR Preview

on:
  pull_request:
    branches:
      - main
    types: [opened, synchronize, reopened]

permissions:
  pull-requests: write

jobs:
  deploy-preview:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Install Dependencies
        run: npm install

      - name: Fix React Build Paths
        run: echo "REACT_APP_PUBLIC_URL=/" > .env

      - name: Build React App
        run: npm run build

      - name: Configure AWS Credentials
        run: |
          aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws configure set region ${{ secrets.AWS_REGION }}

      - name: Set PR Deployment Path
        run: echo "DEPLOY_PATH=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV

      - name: Deploy to S3 (Preview)
        run: aws s3 sync ./build s3://${{ secrets.AWS_S3_BUCKET_NAME }}/${{ env.DEPLOY_PATH }} --delete

      - name: Set Correct Content-Type for JS Files
        run: |
          aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/${{ env.DEPLOY_PATH }} s3://${{ secrets.AWS_S3_BUCKET_NAME }}/${{ env.DEPLOY_PATH }} --recursive --exclude "*" --include "*.js" --metadata-directive REPLACE --content-type "application/javascript"

      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"

      - name: Comment on PR with Preview URL
        uses: thollander/actions-comment-pull-request@v2
        with:
          message: "🚀 Preview deployed: [View Preview](https://${{ secrets.CLOUDFRONT_DISTRIBUTION_DOMAIN_NAME }}/${{ env.DEPLOY_PATH }}/index.html)"
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This workflow triggers whenever a PR is created or updated against the main branch. It installs dependencies, builds the React project, and uploads the build files to an S3 bucket under a PR-specific path (pr-<PR_NUMBER>). Once uploaded, CloudFront ensures that the preview is accessible globally, and a comment is posted on the PR with the preview link. This allows reviewers to test the changes before merging. Each time the PR is updated, the deployment is refreshed with the latest changes.

Main Deployment

name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Install Dependencies
        run: npm install

      - name: Fix React Build Paths
        run: echo "REACT_APP_PUBLIC_URL=/" > .env

      - name: Build React App
        run: npm run build   

      - name: Configure AWS Credentials
        run: |
          aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws configure set region ${{ secrets.AWS_REGION }}

      - name: Deploy to S3
        run: aws s3 sync ./build s3://${{ secrets.AWS_S3_BUCKET_NAME }}/main --delete

      - name: Set Correct Content-Type for JS Files
        run: |
          aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/main s3://${{ secrets.AWS_S3_BUCKET_NAME }}/main --recursive --exclude "*" --include "*.js" --metadata-directive REPLACE --content-type "application/javascript"

      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"

Every time code is pushed to main, this workflow kicks in to deploy the latest production build. It follows the same steps as the PR preview workflow but uploads the build to the main directory in S3. Additionally, it invalidates the CloudFront cache to ensure that users receive the latest version of the deployed app without waiting for cache expiry.

Cleanup Workflow

name: Cleanup PR Preview

on:
  pull_request:
    types: [closed]

jobs:
  cleanup-preview:
    runs-on: ubuntu-latest

    steps:

      - name: Remove PR Preview from S3
        run: aws s3 rm s3://${{ secrets.AWS_S3_BUCKET_NAME }}/pr-${{ github.event.pull_request.number }} --recursive
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ secrets.AWS_REGION }}

Once a PR is closed or merged, there is no need to retain the preview deployment. This workflow automatically deletes the PR’s specific deployment folder from S3, keeping the storage clean and avoiding unnecessary clutter. This ensures that old, unused previews do not persist indefinitely.

Give It a Try!

Want to see it in action? Just create a Pull Request (PR) to the main branch in the forked repository, and GitHub Actions will automatically deploy a preview of your changes. A comment will be posted on the PR with the preview link, allowing you to test the deployment before merging. Check it out and experience the seamless PR preview workflow! 💪

Here's an example Pull Request:

Conclusion

This project demonstrates how you can self-host a Vercel-like PR preview system using AWS infrastructure. Instead of relying on third-party services, we leveraged S3 for storage, CloudFront for efficient global distribution, and GitHub Actions for automation. As a developer, building your own tools not only gives you greater control but also deepens your understanding of cloud infrastructure and CI/CD workflows. If you're using AWS in your stack, taking the time to set up custom dev tools like this can save costs and provide a tailored experience. Give it a try, experiment with AWS, and start deploying your own frontend projects seamlessly!

If you ever need help or just want to chat, DM me on Twitter / X or LinkedIn.

Kartik Mehta

X / LinkedIn

9
Subscribe to my newsletter

Read articles from Kartik Mehta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kartik Mehta
Kartik Mehta

A code-dependent life form.