This is a practical deployment script for serving an SPA with CloudFront + S3.
Assumptions
- AWS is the cloud provider
- Frontend build artifacts are placed in S3 and served through CloudFront
Infrastructure layout
S3
- Bucket versioning: disabled
- Encryption: SSE-S3
- Block Public Access: ON
- Static website hosting: disabled
CloudFront
- Price class: use all edge locations
- Supported HTTP versions: HTTP/3, 2, 1.1, 1.0
Origin
- Origin name: S3-frontend-app
- Origin domain:
<bucket_name>.s3.ap-northeast-1.amazonaws.com - Origin path:
/current - Origin access: Origin access control settings
Behavior
Default (*)
- Origin: S3-frontend-app
- Viewer protocol policy: HTTP to HTTPS
- Cache policy: Managed-CacheOptimized
- Viewer request: CloudFront Functions
Deployment script
I am skipping AWS credential details because that depends on the execution environment.
# global varsreadonly S3_BUCKET=""readonly DIST_DIR=""
function main() { # BUILD_ID can also be a Git commit hash local -r build_id=$(date -Iseconds)
local -r base_uri="s3://$S3_BUCKET" local -r deploy_uri="$base_uri/builds/$build_id" local -r previous_uri="$base_uri/previous" local -r current_uri="$base_uri/current"
# Deploy into builds/
# js,css aws s3 sync "$DIST_DIR" "$deploy_uri" \ --exclude "*" \ --include "*.js" \ --include "*.css" \ --metadata-directive "REPLACE" --cache-control "public,max-age=31536000,immutable"
# everything except js,css,html aws s3 sync "$DIST_DIR" "$deploy_uri" \ --exclude "*.js" \ --exclude "*.css" \ --exclude "*.html" \ --metadata-directive "REPLACE" \ --cache-control "public,max-age=1,stale-while-revalidate=604800"
# html aws s3 sync "$DIST_DIR" "$deploy_uri" \ --exclude "*" \ --include "*.html" \ --metadata-directive "REPLACE" \ --cache-control "no-cache"
# Back up the currently served frontend assets from current to previous aws s3 sync "$current_uri" "$previous_uri" --delete --exact-timestamp
# Copy this build into current and publish it aws s3 sync "$deploy_uri" "$current_uri" --delete --exact-timestamp}Why set Cache-Control in this order
Ideally, I would like to set cache-control in the very last step, aws s3 sync "$deploy_uri" "$current_uri", but when you sync from one S3 bucket path to another using --metadata-directive "REPLACE", AWS stops inferring Content-Type and everything ends up as binary/octet-stream.
When syncing from local files to S3, Content-Type inference is preserved, so this arrangement is a practical compromise.
Rollback script
If something goes wrong, restore previous into current.
# global varsreadonly S3_BUCKET=""
function revert() { local -r base_uri="s3://$S3_BUCKET" local -r previous_uri="$base_uri/previous" local -r current_uri="$base_uri/current"
# copy previous to current aws s3 sync "$previous_uri" "$current_uri" --delete --exact-timestamp}Summary
When serving an SPA with CloudFront + S3, combining appropriate cache-control settings with a blue-green style deployment layout gives you a safer and more efficient deployment flow.
By keeping build history under builds and managing the active and previous versions with current and previous, you can roll back quickly when necessary.
hsb.horse