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
- 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.
- CloudFront Distribution: Acts as a content delivery network (CDN), caching and serving requests efficiently.
- OAC: Grants CloudFront permission to fetch content from the private S3 bucket.
-
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 }
}
}
]
})
}
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"
}
}
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",
},
});
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 }}
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)