DEV Community

Sohana Akbar
Sohana Akbar

Posted on

Deploying a Static Site to S3 + CloudFront with GitHub Actions

Deploying a static website shouldn't require manual FTP uploads or complex server management. With AWS S3, CloudFront, and GitHub Actions, you can create a fully automated pipeline that delivers your site globally with HTTPS, caching, and zero server maintenance.

This guide walks you through the complete setup, from AWS configuration to a production-ready CI/CD workflow.

Why This Stack Works
Amazon S3 provides cheap, reliable storage for your static files . CloudFront acts as a global CDN, caching content at edge locations worldwide and automatically providing HTTPS . GitHub Actions ties it all together, automatically deploying your site whenever you push changes .

The result: a fast, secure, cost-effective website that updates itself.

Architecture Overview
text
User → CloudFront (HTTPS + CDN) → S3 Bucket (Static Files)

GitHub Actions (CI/CD)

Push to main branch
Every push to your main branch triggers the workflow: build the site, sync to S3, and invalidate CloudFront's cache so users see the latest version immediately .

Step 1: Set Up Your AWS Infrastructure
Create an S3 Bucket
Log in to AWS Console → S3 → Create Bucket

Choose a unique bucket name (globally unique, like my-site-username)

Uncheck "Block all public access" (we'll use CloudFront for security)

Enable Static Website Hosting:

Properties tab → Static website hosting → Enable

Index document: index.html

Error document: index.html (for SPAs)

Apply the Bucket Policy
Your bucket needs public read access via CloudFront. Add this policy (replace BUCKET_NAME):

json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/
"
}
]
}
Enable Versioning (Properties tab → Versioning → Enable) for rollback capability .

Create a CloudFront Distribution
CloudFront → Create Distribution

Origin: Your S3 bucket's website endpoint

Viewer protocol policy: Redirect HTTP to HTTPS

Default root object: index.html

Cache policy: Managed-CachingOptimized

After creation, copy your CloudFront domain (e.g., d123abcd.cloudfront.net)—this is your live site URL .

Step 2: Configure GitHub Secrets
Go to your repository → Settings → Secrets and variables → Actions and add:

Secret Name Description
AWS_ACCESS_KEY_ID Your IAM user access key
AWS_SECRET_ACCESS_KEY Your IAM user secret
AWS_REGION Your bucket region (e.g., us-east-1)
S3_BUCKET Your bucket name
CLOUDFRONT_DISTRIBUTION_ID Your CloudFront distribution ID
IAM Permissions Required
Your IAM user needs:

s3:PutObject, s3:DeleteObject, s3:ListBucket

cloudfront:CreateInvalidation

Step 3: Create the GitHub Actions Workflow
Create .github/workflows/deploy.yml in your repository:

yaml
name: Deploy to AWS S3 and CloudFront

on:
push:
branches: [ main ]
workflow_dispatch: # Allow manual trigger

jobs:
deploy:
runs-on: ubuntu-latest

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

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

- name: Install dependencies
  run: npm ci

- name: Build project
  run: npm run build  # or your build command

- 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: ${{ secrets.AWS_REGION || 'us-east-1' }}

- name: Sync files to S3
  run: |
    # Upload non-HTML files with 1-hour cache
    aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} \
      --delete \
      --cache-control "public, max-age=3600" \
      --exclude "*.html" \
      --exclude "*.xml"

    # Upload HTML/XML with 5-minute cache for faster updates
    aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} \
      --delete \
      --cache-control "public, max-age=300, must-revalidate" \
      --exclude "*" \
      --include "*.html" \
      --include "*.xml"

- name: Create CloudFront invalidation
  run: |
    aws cloudfront create-invalidation \
      --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
      --paths "/*"
Enter fullscreen mode Exit fullscreen mode

This workflow uses differential caching: assets get 1-hour cache, while HTML gets 5-minute cache for quicker updates .

Step 4: Test Your Deployment
Push to main: git push origin main

Monitor progress: Actions tab in GitHub

Visit your site: https://your-cloudfront-domain.cloudfront.net

Advanced Optimizations
Custom Domain + HTTPS
Register a domain in Route 53

Request SSL certificate in ACM (us-east-1)

Add alternate domain to CloudFront

Create Route 53 A record pointing to CloudFront

Rollback Strategy
S3 versioning lets you restore previous versions:

S3 Console → Bucket → Show versions

Select older version → Restore

Re-run the workflow to clear cache

Troubleshooting Guide
403 Access Denied: Check bucket policy and public access block settings. CloudFront should have read access .

404 Not Found: Verify files exist in the bucket root and index.html is uploaded .

Stale Content: CloudFront invalidation takes a few minutes. Re-run the workflow or manually invalidate /* .

Build Fails: Check your build command and ensure dependencies install correctly in the CI environment.

Cost Considerations
This setup is extremely cost-effective:

S3: ~$0.023/GB storage, $0.0004/10,000 GET requests

CloudFront: First 1TB/month free for new accounts

Estimated cost: Under $1/month for a low-traffic site

Conclusion
You now have a fully automated deployment pipeline using S3, CloudFront, and GitHub Actions. Every push delivers your site globally with HTTPS, CDN caching, and instant invalidation. This setup scales from personal projects to enterprise applications and costs pennies to run.

Deployed your site? Share your experience in the comments below!

Top comments (0)