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:
- Build your React app using
npm run build
. - Create or reuse a private S3 bucket.
- Create or reuse a CloudFront Origin Access Control (OAC).
- Create or reuse a CloudFront Distribution.
- Attach a secure bucket policy that only CloudFront can access.
- Upload your React build to S3 and invalidate the CloudFront cache.
- 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
Post a Comment