These are notes for my own reference, not a comprehensive post, I’ll leave out most explanations and won’t explore various options. This is just a set of steps that worked for my needs, hopefully they’re useful to someone else. Getting this to work today took far too long!
I was deploying a Vue app to a subdomain, but the steps should be the same or similar for any static site.
Create and configure an S3 bucket
- Create an S3 bucket in
us-east-1
(CloudFront requiresus-east-1
) with the domain name to be hosted (e.g.cloud.example.com
). Uncheck ‘Block all public access’ - After creating, go to the Properties tab, scroll to bottom and enable static website hosting, set
Index document
toindex.html
. Save. - For a quick sanity check, upload a test
index.html
file. - Go to the Permissions tab. Add the following policy. Then you could test the S3 static website URL (go to the Properties tab again, scroll to bottom to find the URL)
{
"Version": "2012-10-17",
"Id": "Policy1729027101551",
"Statement": [
{
"Sid": "Stmt1729027092485",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::cloud.example.com/*"
}
]
}
Create an ACM cert and verify by adding CNAME records
- Create a cert for *.
example.com
in ACM - Add a CNAME in Route 53.
CNAME Name
in ACM maps to theSubdomain
CNAME value in Route 53. - The cert will probably verify in a few minutes.
Note, to add staging subdomain (e.g. cloud.staging.example.com), it will needed to be included on the cert (*.example.com doesn’t cover it), or a separate cert needs to be created. *.staging.example.com works.
Create a CloudFront distribution
- For Origin domain, choose the Amazon S3 bucket, then accept prompt to use the website URL.
- Choose
Redirect HTTP to HTTPS
- For WAF, select
Do not enable security protections
- For
Alternate domain name (CNAME)
add the domain (e.g.cloud.example.com
) - Choose the cert just created
- Set
Default root object
toindex.html
- With Vue, and SPAs generally, when you refresh a page or directly access a route other than the root, the server looks for a file that doesn’t exist (as your app’s routing is handled client-side). To solve this, add a redirect rule. On the Error pages tab, add a 404 error response with
Response page path
to/index.html
status code200
.
Configure the subdomain in Route 53
Assuming the root domain is already added as a hosted zone:
- Create an A record in Route 53 in the hosted zone (e.g.
example.com
) - When creating toggle
Alias
on - Route traffic to
Alias to CloudFront distribution
,us-east-1
(US East N. Virginia) - Select the CloudFront dist.
Create an IAM user for the deploy GitHub Action
- Create a user in IAM, named something like
github-actions-s3-deployer - Add the following policy, exclude staging values if not required, of course:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3BucketManipulation",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::cloud.example.com/*",
"arn:aws:s3:::staging.cloud.example.com/*"
]
},
{
"Sid": "AllowS3BucketListing",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::cloud.example.com",
"arn:aws:s3:::staging.cloud.example.com"
]
},
{
"Sid": "CFInvalidation",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": [
"arn:aws:cloudfront::147997118411:distribution/<cloudfront_dist_id>",
"arn:aws:cloudfront::147997118411:distribution/<staging_cloudfront_dist_id>"
]
}
]
}
Add GitHub Action to deploy to S3 and Invalidate the CloudFront cache
Create .github/workflows/aws-s3-cloudfront-deploy.yml
Add the following:
name: Deploy to S3 and invalidate CloudFront cache
on:
workflow_dispatch:
inputs:
environment:
type: choice
description: 'Where to deploy'
required: true
options:
- prod
- staging
jobs:
run:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build for production
if: github.event.inputs.environment == 'prod'
env:
VITE_APP_ID: ${{ secrets.VITE_APP_ID }}
run: npm run build
- name: Build for staging
if: github.event.inputs.environment == 'staging'
env:
VITE_APP_ID: ${{ secrets.STAGING_VITE_APP_ID }}
VITE_BAZAAR_URI: ${{ secrets.STAGING_VITE_BAZAAR_URI }}
run: npm run build
- name: Deploy to S3 and invalidate CloudFront - Production
if: github.event.inputs.environment == 'prod'
uses: reggionick/s3-deploy@v4
with:
folder: dist
bucket: ${{ secrets.S3_BUCKET }}
bucket-region: ${{ secrets.S3_BUCKET_REGION }}
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
invalidation: /
delete-removed: true
no-cache: true
private: true
files-to-include: '{.*/**,**}'
- name: Deploy to S3 and invalidate CloudFront - Staging
if: github.event.inputs.environment == 'staging'
uses: reggionick/s3-deploy@v4
with:
folder: dist
bucket: ${{ secrets.STAGING_S3_BUCKET }}
bucket-region: ${{ secrets.S3_BUCKET_REGION }}
dist-id: ${{ secrets.STAGING_CLOUDFRONT_DISTRIBUTION_ID }}
invalidation: /
delete-removed: true
no-cache: true
private: true
files-to-include: '{.*/**,**}'
Uses the S3 Deploy Marketplace action.
VITE_APP_ID
and STAGING_VITE_BAZAAR_URI
are env vars my project requires during the build step. dist
is my build directory.
Create the GitHub Action secrets in the yaml file.
If staging is needed, repeat the steps in this post and create the same resources for staging.
One day I’ll create a CloudFormation template for this… Maybe.