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 "/*"
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)