DEV Community

Cover image for Deploying a React + Vite SPA to a Private S3 Bucket with CloudFront and OAC
Pablo Eliseo for One Beyond

Posted on

Deploying a React + Vite SPA to a Private S3 Bucket with CloudFront and OAC

Deploying a React + Vite single-page application (SPA) to a private AWS S3 bucket while using CloudFront with Origin Access Control (OAC) requires careful setup, especially to handle client-side routing and asset paths properly. This guide will walk you through the complete process, highlighting critical aspects such as custom_error_response in CloudFront and the base config in vite.config.js.

1. Understanding How OAC, CloudFront, and S3 Work Together

What is Origin Access Control (OAC)?

OAC is a mechanism that allows CloudFront to securely access private content stored in an S3 bucket without exposing the bucket to the public internet. It replaces the older Origin Access Identity (OAI) with better security and fine-grained access control.

How CloudFront Works with S3 and OAC

  1. S3 Bucket: Stores your static site files (HTML, JS, CSS, images, etc.). Since it's private, it cannot be accessed directly via a public URL.
  2. CloudFront Distribution: Acts as a content delivery network (CDN), caching and serving requests efficiently.
  3. OAC: Grants CloudFront permission to fetch content from the private S3 bucket.
  4. Custom Error Handling: Ensures deep links and client-side routing work properly by redirecting 404 errors for non-existing objects to index.html.

With this setup, users access CloudFront, which in turn retrieves the content from the private S3 bucket, ensuring security and performance.

2. Setting Up the S3 Bucket

Since the S3 bucket is private, it must be configured to allow CloudFront access via OAC.

Terraform Configuration:

resource "aws_s3_bucket" "spa_bucket" {
  bucket = "react-vite-app"
  acl    = "private"
}

resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.spa_bucket.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowCloudFrontAccess"
        Effect    = "Allow"
        Principal = { Service = "cloudfront.amazonaws.com" }
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.spa_bucket.arn}/*"
        Condition = {
          StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.spa_distribution.arn }
        }
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

3. Creating a CloudFront Distribution with Custom Error Handling

A major challenge when deploying an SPA is handling client-side routing. When a user directly visits /dashboard or /profile, CloudFront will look for dashboard/index.html or profile/index.html in S3, which doesn't exist. To fix this, we use custom_error_response to serve index.html whenever CloudFront encounters a 404 error.

Why Use custom_error_response?

  • SPAs rely on client-side routing. The React app should handle the routes, not CloudFront.
  • Prevents 404 errors. Instead of returning a 404 from S3, CloudFront serves index.html, allowing React to handle the routing.

Terraform Configuration:

resource "aws_cloudfront_distribution" "spa_distribution" {
  enabled = true

  origin {
    domain_name = aws_s3_bucket.spa_bucket.bucket_regional_domain_name
    origin_id   = "spa-s3-origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "spa-s3-origin"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = true
      cookies { forward = "none" }
    }
  }

  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }
}
Enter fullscreen mode Exit fullscreen mode

It is very important here not to use the origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # AllViewer - Default ID as the AllViewer request policy forwards the host to the S3 bucket which will be configured to only accept requests from Cloudfront.

4. Why Use Vite Instead of Webpack?

Advantages of Vite

  • Faster Development: Vite uses native ES modules, significantly improving development speed compared to Webpack.
  • Better Performance: It only rebuilds changed files instead of bundling everything together.
  • Optimized Build Output: Vite produces highly optimized static assets, reducing bundle size and improving load times.

For SPAs, especially with React, Vite provides a better developer experience and faster deployments.

5. When to Use This Approach vs. Other Alternatives

Use This Approach When:

  • You need a pure client-side application without server-side rendering (SSR).
  • You want cost-effective static hosting with high security.
  • You want full control over AWS infrastructure.

Consider Other Options When:

  • Next.js: If you need SSR or static site generation (SSG), Next.js on Vercel or AWS Lambda may be a better choice.
  • Gatsby: If your site is primarily static content with pre-built pages.
  • AWS Amplify: If you prefer a managed service that simplifies the deployment process.

6. Configuring Vite for Correct Asset Paths

Why the base Option in vite.config.js?

Vite's base configuration determines how URLs are resolved in the generated output. If not set correctly, assets (JavaScript, CSS, favicon) may try to load from incorrect paths when navigating to deeper routes.

Why Use base: '/'?

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  base: "/", // Ensures all assets use absolute paths from the root
  plugins: [react()],
  build: {
    outDir: "dist",
  },
});
Enter fullscreen mode Exit fullscreen mode

7. Deploying with GitHub Actions

To automate deployment, we use GitHub Actions to build and upload the React app to the S3 bucket. I am using Github Actions Secrets and Variables for configuring the workflow.

name: Deploy to S3
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Install Dependencies
        run: npm install
      - name: Build Application
        run: npm run build
      - name: Configure
        uses: aws-actions/configure-aws-credentials@v4.0.2
        with:
          aws-region: "${{ vars.AWS_REGION }}"
          role-to-assume: "arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-actions_role"
          role-session-name: "GitHub-Actions_Publish_Static_Site"
      - name: Deploy static site to S3 bucket
        run: aws s3 sync ./dist/ s3://${{ vars.AWS_S3_BUCKET_NAME }} --delete --region ${{ vars.AWS_REGION }}
Enter fullscreen mode Exit fullscreen mode

Also, I am configuring the AWS credentials by using a IAM role. See this guide for more information on the topic.

Conclusion

This guide covered deploying a React + Vite SPA securely on AWS using S3, CloudFront, and OAC. By setting up CloudFront with custom_error_response, using Vite for efficient builds, and automating deployment with GitHub Actions, you ensure a smooth, performant, and cost-effective solution for hosting your SPA.
You can find the full code (with some more improvements) in this GitHub repository

Top comments (0)