Deploy Your React App to AWS S3 and CloudFront

Deploy Your React App to AWS S3 and CloudFront Using a Python Boto3 Script

If you've ever wanted to automate the deployment of your React web app to AWS S3 and serve it securely via Amazon CloudFront, this detailed guide is for you.

In this post, we’ll walk through a powerful Python deployment script that builds your React project, uploads it to S3, configures the bucket, sets up CloudFront with an Origin Access Control (OAC), and verifies your app is live on a secure HTTPS URL — all using Boto3, AWS’s Python SDK.


Overview of the Deployment Workflow

This Python script automates the entire deployment pipeline in 7 major steps:

  1. Build your React app using npm run build.
  2. Create or reuse a private S3 bucket.
  3. Create or reuse a CloudFront Origin Access Control (OAC).
  4. Create or reuse a CloudFront Distribution.
  5. Attach a secure bucket policy that only CloudFront can access.
  6. Upload your React build to S3 and invalidate the CloudFront cache.
  7. Verify that your CloudFront URL is live and serving the React app.

This is ideal for developers who want a repeatable, automated deployment process for single-page apps hosted on AWS.


Prerequisites

  • A working AWS account with permission to create S3 buckets and CloudFront distributions.
  • Boto3 installed: pip install boto3 requests
  • Node.js installed with your React app ready to build.
  • AWS credentials configured locally (via AWS CLI or environment variables).

Full Python Deployment Script

Here’s the full Python script you can use. Copy and save it as deploy_react_to_aws.py.


import boto3
import os
import time
import mimetypes
import json
import subprocess
from botocore.exceptions import ClientError
import requests  # for verifying URL reachability

# -----------------------
#  USER CONFIGURATION
# -----------------------

AWS_REGION = "us-east-1"
REACT_APP_DIR = r"C:\Users\[user]\git\[project]"  # React project root
PROJECT_NAME = "test_project"
BUCKET_NAME = f"{PROJECT_NAME.lower()}-deploy-bucket"
OAC_NAME = f"{PROJECT_NAME}-oac"
DISTRIBUTION_COMMENT = f"CloudFront for {PROJECT_NAME} React App"

# -----------------------
#  AWS CLIENTS
# -----------------------

s3 = boto3.client("s3", region_name=AWS_REGION)
cf = boto3.client("cloudfront", region_name=AWS_REGION)
sts = boto3.client("sts")
account_id = sts.get_caller_identity()["Account"]

# -----------------------
#  STEP 0: Build React App
# -----------------------

def build_react_app(app_dir):
    print(f"šŸ”Ø Building React app in {app_dir}...")
    result = subprocess.run(
        ["npm", "run", "build"],
        cwd=app_dir,
        shell=True,
        capture_output=True,
        text=True
    )
    if result.returncode != 0:
        print("❌ React build failed")
        print(result.stdout)
        print(result.stderr)
        raise RuntimeError("React build failed")
    build_dir = os.path.join(app_dir, "build")
    if not os.path.exists(build_dir):
        raise FileNotFoundError("React build folder not found after build")
    print(" React build complete")
    return build_dir

This section triggers npm run build to compile your React app into a production-ready folder named build.


Step 1: Ensure Private S3 Bucket

The function below checks if the deployment bucket exists. If not, it creates a new private S3 bucket with public access blocked — perfect for hosting a site behind CloudFront.


def ensure_private_s3_bucket(bucket_name):
    try:
        s3.head_bucket(Bucket=bucket_name)
        print(f"ℹ️ Bucket already exists: {bucket_name}")
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code in ('404', 'NoSuchBucket'):
            print(f"🪣 Creating new S3 bucket: {bucket_name}")
            if AWS_REGION == "us-east-1":
                s3.create_bucket(Bucket=bucket_name)
            else:
                s3.create_bucket(
                    Bucket=bucket_name,
                    CreateBucketConfiguration={"LocationConstraint": AWS_REGION}
                )
        else:
            raise

    s3.put_public_access_block(
        Bucket=bucket_name,
        PublicAccessBlockConfiguration={
            "BlockPublicAcls": True,
            "IgnorePublicAcls": True,
            "BlockPublicPolicy": True,
            "RestrictPublicBuckets": True
        }
    )
    print("šŸ”’ S3 bucket is private")
    return bucket_name

Step 2: Create or Reuse Origin Access Control (OAC)

The OAC (Origin Access Control) securely connects CloudFront to your private S3 bucket without exposing it publicly.


def get_or_create_oac(name):
    oacs = cf.list_origin_access_controls()["OriginAccessControlList"].get("Items", [])
    for oac in oacs:
        if oac["Name"] == name:
            print(f"ℹ️ Found existing OAC: {oac['Id']}")
            return oac["Id"]
    print("šŸ” Creating new OAC...")
    response = cf.create_origin_access_control(
        OriginAccessControlConfig={
            'Name': name,
            'Description': 'OAC for secure S3 React hosting',
            'SigningBehavior': 'always',
            'SigningProtocol': 'sigv4',
            'OriginAccessControlOriginType': 's3'
        }
    )
    oac_id = response['OriginAccessControl']['Id']
    print(f"✅ OAC created: {oac_id}")
    return oac_id

šŸŒ Step 3: Create or Reuse CloudFront Distribution

CloudFront serves your app from global edge locations. This function checks if a distribution with your project’s comment exists. If not, it creates one and links it to the OAC.


def get_existing_distribution(comment):
    paginator = cf.get_paginator('list_distributions')
    for page in paginator.paginate():
        for dist in page['DistributionList'].get('Items', []):
            if dist.get('Comment') == comment:
                print(f"ℹ️ Found existing CloudFront distribution: {dist['Id']}")
                return dist['Id'], dist['DomainName']
    return None, None

def create_cloudfront_distribution(bucket_name, oac_id):
    origin_id = f"S3-{bucket_name}"
    response = cf.create_distribution(
        DistributionConfig={
            'CallerReference': str(time.time()),
            'Comment': DISTRIBUTION_COMMENT,
            'Enabled': True,
            'Origins': {
                'Quantity': 1,
                'Items': [{
                    'Id': origin_id,
                    'DomainName': f"{bucket_name}.s3.{AWS_REGION}.amazonaws.com",
                    'OriginAccessControlId': oac_id,
                    'S3OriginConfig': {'OriginAccessIdentity': ''}
                }]
            },
            'DefaultRootObject': 'index.html',
            'DefaultCacheBehavior': {
                'TargetOriginId': origin_id,
                'ViewerProtocolPolicy': 'redirect-to-https',
                'AllowedMethods': {'Quantity': 2, 'Items': ['GET', 'HEAD']},
                'Compress': True,
                'ForwardedValues': {'QueryString': False, 'Cookies': {'Forward': 'none'}}
            },
            'ViewerCertificate': {'CloudFrontDefaultCertificate': True},
            'CustomErrorResponses': {
                'Quantity': 2,
                'Items': [
                    {'ErrorCode': 404, 'ResponsePagePath': '/index.html', 'ResponseCode': '200'},
                    {'ErrorCode': 403, 'ResponsePagePath': '/index.html', 'ResponseCode': '200'}
                ]
            }
        }
    )
    dist_id = response['Distribution']['Id']
    dist_domain = response['Distribution']['DomainName']
    print(f"✅ CloudFront distribution created: {dist_id}")
    return dist_id, dist_domain

šŸ“œ Step 4: Attach Secure S3 Bucket Policy

This policy ensures that only your CloudFront distribution can access the S3 objects.


def attach_bucket_policy(bucket_name, dist_id):
    policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "cloudfront.amazonaws.com"},
            "Action": "s3:GetObject",
            "Resource": f"arn:aws:s3:::{bucket_name}/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": f"arn:aws:cloudfront::{account_id}:distribution/{dist_id}"
                }
            }
        }]
    }
    s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy))
    print("šŸ“œ Attached correct bucket policy for CloudFront")

šŸ“¦ Step 5: Upload the React Build

Uploads every file from the build folder to S3 with the correct MIME types.


def upload_react_build(build_dir, bucket_name):
    print(f"šŸ“¦ Uploading build from: {build_dir}")
    for root, dirs, files in os.walk(build_dir):
        for filename in files:
            local_path = os.path.join(root, filename)
            s3_key = os.path.relpath(local_path, build_dir).replace("\\", "/")
            content_type, _ = mimetypes.guess_type(local_path)
            if not content_type:
                content_type = "application/octet-stream"
            s3.upload_file(
                Filename=local_path,
                Bucket=bucket_name,
                Key=s3_key,
                ExtraArgs={'ContentType': content_type}
            )
            print(f"   ✅ {s3_key}")
    print("šŸš€ Upload complete")

♻️ Step 6: Invalidate CloudFront Cache

Invalidation ensures visitors always see the newest version of your site.


def invalidate_cf_cache(dist_id):
    response = cf.create_invalidation(
        DistributionId=dist_id,
        InvalidationBatch={'Paths': {'Quantity': 1, 'Items': ['/*']}, 'CallerReference': str(time.time())}
    )
    invalidation_id = response['Invalidation']['Id']
    print(f"♻️ CloudFront cache invalidation started: {invalidation_id}")
    return invalidation_id

šŸ” Step 7: Verify CloudFront URL

Finally, the script checks your CloudFront distribution URL to confirm the React app is reachable.


def verify_cloudfront_url(dist_domain):
    url = f"https://{dist_domain}"
    print(f"šŸ” Verifying CloudFront URL: {url}")
    try:
        time.sleep(10)
        response = requests.get(url)
        if response.status_code == 200:
            print("✅ CloudFront URL is accessible! Your app is live.")
        else:
            print(f"⚠️ CloudFront URL returned status {response.status_code}")
    except Exception as e:
        print(f"❌ Failed to access CloudFront URL: {e}")

šŸ Bringing It All Together

The main() function ties everything together in sequence:


def main():
    print("\n=== šŸš€ React App Deployment Starting ===")

    build_dir = build_react_app(REACT_APP_DIR)
    ensure_private_s3_bucket(BUCKET_NAME)
    oac_id = get_or_create_oac(OAC_NAME)

    dist_id, dist_domain = get_existing_distribution(DISTRIBUTION_COMMENT)
    if not dist_id:
        dist_id, dist_domain = create_cloudfront_distribution(BUCKET_NAME, oac_id)
    else:
        print(f"ℹ️ Using existing CloudFront distribution: {dist_id}")

    attach_bucket_policy(BUCKET_NAME, dist_id)
    upload_react_build(build_dir, BUCKET_NAME)

    invalidation_id = invalidate_cf_cache(dist_id)
    print("⏳ Waiting for invalidation to complete...")
    time.sleep(15)

    print("\n" + "="*80)
    print("✅ DEPLOYMENT COMPLETE!")
    print(f"šŸŒ YOUR REACT APP IS LIVE AT:\nšŸ‘‰  https://{dist_domain}")
    print("="*80 + "\n")

    verify_cloudfront_url(dist_domain)

if __name__ == "__main__":
    main()

✨ Final Output Example

=== šŸš€ React App Deployment Starting ===
✅ React build complete
šŸ”’ S3 bucket is private
✅ OAC created: ABCDEFGHIJKL123
✅ CloudFront distribution created: E123ABC456DEF
šŸ“œ Attached correct bucket policy for CloudFront
šŸš€ Upload complete
♻️ CloudFront cache invalidation started: I12345ABCDEF
✅ CloudFront URL is accessible! Your app is live.

Summary

  • Language: Python (using Boto3)
  • Services: S3, CloudFront, STS
  • Use Case: Automate React app deployment
  • Security: Uses OAC for secure private bucket access
  • Result: A globally distributed React app served via HTTPS

With this script, you no longer need to manually upload files or tweak bucket policies. In under a minute, your React app is built, deployed, cached, and distributed securely around the world via AWS CloudFront.

Happy deploying!

Comments

Popular posts from this blog

Building and Deploying a Fargate Container that runs Python and performs CloudWatch Logging

Automate Your API Gateway Setup with Boto3: Rendering HTML from Lambda

Setting up an AWS Cognito User Pool and building a React login component