DEV Community

Cover image for Deploying a Production-Ready React App on AWS using Terraform Module, S3, CloudFront & GitHub Actions
Samir Khanal
Samir Khanal

Posted on

Deploying a Production-Ready React App on AWS using Terraform Module, S3, CloudFront & GitHub Actions

Modern frontend apps need fast, scalable, and cost-efficient hosting.
While AWS S3 is great for static hosting, making it production-ready
requires CloudFront, proper security, and automation.

In this blog, I’ll show how I built a fully automated pipeline.
to deploy a React app using Terraform and GitHub Actions.

πŸ“Œ Architecture Overview

Here’s what we’re building:

  1. React app β†’ built into static files
  2. Terraform provisions:
    • S3 bucket (hosting)
    • CloudFront distribution (CDN)
  3. GitHub Actions:
    • Build React app
    • Upload to S3
    • Invalidate CloudFront cache

πŸ“ Project Structure

βš™οΈ Step 1: Terraform Module Design

Instead of writing everything in one file, I used modular Terraform.

Example modules:

πŸ”— How Modules Are Used (Root Configuration)

This snippet shows how the root module calls the S3 and CloudFront
modules to provision infrastructure.

module "s3_static_site" {
  source      = "./modules/s3-static-site"
  bucket_name = var.bucket_name
}

module "cloudfront" {
  source             = "./modules/cloudfront-distribution"
  bucket_domain_name = module.s3_static_site.bucket_domain_name
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ S3 Module (Static Hosting)

This module creates and configures the S3 bucket for static hosting.

Bucket Creation
This snippet creates the S3 bucket that will host the React static site.

resource "aws_s3_bucket" "skr_bucket" {
  bucket = var.bucket_name
}
Enter fullscreen mode Exit fullscreen mode

Website Configuration
This snippet configures the S3 bucket for static website hosting and sets the index document.

resource "aws_s3_bucket_website_configuration" "skr_bucket" {
  bucket = aws_s3_bucket.skr_bucket.id

  index_document {
    suffix = "index.html"
  }
}
Enter fullscreen mode Exit fullscreen mode

Bucket Policy (for CloudFront)
This snippet sets a bucket policy allowing CloudFront (and other services if needed) to read objects from the S3 bucket.

resource "aws_s3_bucket_policy" "allow_cloudfront" {
  bucket = aws_s3_bucket.skr_bucket.id

  policy = jsonencode({
    Statement = [{
      Effect = "Allow"
      Principal = "*"
      Action = "s3:GetObject"
      Resource = "${aws_s3_bucket.skr_bucket.arn}/*"
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

🌍 CloudFront Module

This module sets up a CDN in front of S3.

Distribution
This snippet creates the CloudFront distribution that serves your S3 bucket globally with HTTPS support.

resource "aws_cloudfront_distribution" "cdn" {
  enabled             = true
  default_root_object = "index.html"
}
Enter fullscreen mode Exit fullscreen mode

React Routing Fix (IMPORTANT)
This snippet ensures single-page React routes return index.html instead of 404 errors.

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

Cache Invalidation
This snippet invalidates the CloudFront cache automatically after deployment, so new changes appear immediately.

resource "null_resource" "cache" {
  provisioner "local-exec" {
    command = "aws cloudfront create-invalidation --distribution-id ${aws_cloudfront_distribution.cdn.id} --paths '/*'"
  }
}
Enter fullscreen mode Exit fullscreen mode

This Terraform module-based architecture makes it easy to reuse infrastructure across multiple environments like dev, staging, and production.

Benefits:

  • Reusable
  • Clean architecture
  • Easier debugging
  • Production-ready structure

☁️ Step 2: S3 Static Website Hosting

The S3 module:

  • Creates a bucket
  • Enables static hosting
  • Configures public access (Via OAC)

Key features:

  • Versioning enabled
  • Proper bucket policy
  • Index + error documents

🌍 Step 3: CloudFront CDN Setup

CloudFront sits in front of S3 to:

  • Improve performance (low latency)
  • Add HTTPS support
  • Cache content globally

Configuration highlights:

  • Origin: S3 bucket
  • Viewer protocol: Redirect HTTP β†’ HTTPS
  • Cache behavior optimized for static assets

⚑ Step 4: GitHub Actions CI/CD

This is where things get powerful.

Workflow does:

  • Install dependencies
  • Build React app
  • Sync build folder to S3
  • Invalidate CloudFront cache

Result:

Every push to main β†’ automatic deployment πŸš€

πŸ”„ CI/CD Workflow Example

name: Deploy to CloudFront

on:
  push:
    branches:
      - main
  workflow_dispatch:

env:
  AWS_REGION: ap-south-1
  S3_BUCKET: samir-module-s3-bucket-hosting

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: static-site-react/package-lock.json

      - name: Install dependencies
        working-directory: ./static-site-react
        run: npm ci

      - name: Build React app
        working-directory: ./static-site-react
        run: npm run build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Upload to S3
        run: |
          aws s3 sync ./static-site-react/dist/ s3://${{ env.S3_BUCKET }} --delete

      - name: Get CloudFront distribution ID
        id: cloudfront
        run: |
          DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Origins.Items[0].DomainName=='${{ env.S3_BUCKET }}.s3.${{ env.AWS_REGION }}.amazonaws.com'].Id" --output text)
          echo "distribution_id=$DISTRIBUTION_ID" >> $GITHUB_OUTPUT

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation --distribution-id ${{ steps.cloudfront.outputs.distribution_id }} --paths "/*"

      - name: Deploy success message
        run: |
          echo "βœ… Deployment completed successfully!"

Enter fullscreen mode Exit fullscreen mode

Why Use Terraform Modules?

Without modules:

  • Messy code
  • Hard to reuse
  • Difficult scaling

With modules:

  • Clean separation
  • Reusable across projects
  • Easier team collaboration

πŸ“Š Key Advantages of This Setup

βœ… Fully automated deployments
βœ… Global CDN performance
βœ… Infrastructure as Code (IaC)
βœ… Scalable & production-ready
βœ… Low cost (S3 + CloudFront)

How to Run This Project

Initialize Terraform

terraform init
Enter fullscreen mode Exit fullscreen mode

Apply Infrastructure

terraform apply -auto-approve
Enter fullscreen mode Exit fullscreen mode

Deploy React App

Push code to GitHub β†’ CI/CD handles the rest

πŸ” Best Practices

  • Use IAM roles instead of access keys
  • Store secrets in GitHub Secrets
  • Enable CloudFront caching strategies

πŸ’‘ Improvements You Can Add

  • Custom domain with Route 53
  • HTTPS with ACM
  • WAF for security
  • Multi-environment Terraform setup

🏁 Conclusion

This project demonstrates how to build a modern frontend deployment pipeline using:

  • Terraform Modules
  • AWS S3 + CloudFront
  • GitHub Actions

πŸ”— Final Output

After deployment, your app will be available via:

πŸ‘‰ CloudFront URL (fast, secure, global)

πŸ“‚ Explore the Code

The full project with Terraform modules, S3, CloudFront setup, and GitHub Actions workflow is available here:

Check it out on GitHub

πŸš€ With this setup, you now have a fully automated, scalable, and production-ready React deployment pipeline. Start building modern frontends with confidence!Happy Building!!

Top comments (0)