Deploying a Static Site to AWS (in 2021-2023)
Editor's note: This note is the original readme for this site's GitHub repo when I created the site in 2021 and explored a minimal, mostly automated, self-managed, serverless setup with HTTPS. Now that the site has grown a generator, I'm pulling the raw static infra information out to a dedicated page.
1. Just a website
The site itself is 50 lines of HTML, plus styles and supporting media assets, hosted on S3 and CloudFront.
2. But also infrastructure
And then there's the 200 lines of YAML, because what's a cloud-era project without a 1:4 ratio of content development to infrastructure work? This is roughly the minimum to spin up a static site on S3 and CloudFront with https. It looks like a lot for "just a website," but compared to running a server, I'd say it's similar effort to set up; and cheaper and more secure to run long term. Compared to using any SaaS provider, it's more work to set up but more stable and cheaper long term.
The remaining sections document various outposts on the journey to displaying a web page on the internet. The first two are fine for this project.
- Manual S3/CloudFront/ACM provisioning
- Automate static site deployment with CloudFormation
- Adding security headers with Lambda@Edge
- Making a private site authenticated with Cognito
Manual infrastructure deployment
To bootstrap S3 and CloudFront manually, the overview in How do I use CloudFront to serve a static website hosted on Amazon S3?: Use a REST API endpoint as the origin, and restrict access with an OAC works fine, but some details always get me and make it take longer than it should. Here I follow the same process and point out the gotchas. To avoid this altogether, consider skipping ahead to use the CloudFormation template.
Create the web site bucket. Specifying region us-east-1 saves time and headaches for CloudFront to resolve the REST endpoint of the new bucket. Otherwise it counterintuitively returns 307 redirects for an hour or so.
aws s3 mb --region us-east-1 s3://<your new bucket name>
Go to ACM and request a certificate for your domain name. Be sure to complete the domain verification part before moving on–do this before clicking to create a new CloudFront distribution. Otherwise your certificate will not be available in the dropdown.
Go to CloudFront and create a distribution.
- Select the bucket as origin
- Restrict bucket access. Create a new identify and update bucket permissions.
- Fill in your site details and select the certificate you created (and verified) in ACM
Infrastructure deployment via CloudFormation
This site includes a template infra/static-site.yaml
to create an S3 bucket,
CloudFront distribution, and certificate with reasonable defaults with some
automation. The template is derived from sources in Amazon CloudFront Secure
Static Website.
Simply deploy a new stack using the template and parameter overrides. If you do not have your domain set up as a Route53 hosted zone, set CreateDns=no. You'll need to create DNS records manually for: 1. Domain verification for the ACM cert (during deployment) 2. Pointing the subdomain to CloudFront (after deployment)
STACK=<your CloudFormation stack name> DOMAIN=<your domain name> SUBDOMAIN=<your subdomain e.g. www> aws --region us-east-1 cloudformation deploy \ --stack-name $STACK \ --template-file infra/static-site.yaml \ --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ --parameter-overrides DomainName=$DOMAIN SubDomain=$SUBDOMAIN CreateApex=yes CreateDns=no
Deployment will block on certificate DNS validation. Go to the ACM console to get the validation record details. Create the required record at your DNS provider.
Once deployment completes, point the domain to CloudFront. Create the following record at your DNS provider to point the site to AWS:
aws --region us-east-1 cloudformation describe-stacks \ --stack-name $STACK \ --query "Stacks[0].Outputs[?OutputKey=='DNSRecord'].OutputValue" --output text
Deploy site files
Any variant of `aws s3 cp` or `sync` will do.
echo "export S3_BUCKET=`aws --region us-east-1 cloudformation describe-stacks \ --stack-name $STACK \ --query \"Stacks[0].Outputs[?OutputKey=='S3BucketRoot'].OutputValue\" --output text`" >> .env_deploy echo "export CF_DIST=`aws --region us-east-1 cloudformation describe-stacks \ --stack-name $STACK \ --query \"Stacks[0].Outputs[?OutputKey=='CloudFrontDistribution'].OutputValue\" --output text`" >> .env_deploy aws s3 sync build s3://$S3_BUCKET \ --delete \ --cache-control max-age=86400
And browse!
URL=https://`aws --region us-east-1 cloudformation describe-stacks \ --stack-name $STACK \ --query "Stacks[0].Outputs[?OutputKey=='CloudFrontDomainName'].OutputValue" --output text` open $URL
If you want to force CloudFront invalidation of changed files, see script/deploy-aws.sh.
Configure GitHub Actions
Deploying from GitHub Actions requires additional setup to allow GitHub access to your AWS account. One option is to create an IAM user and add access keys as secrets in GitHub. Here, we deploy a stack to allow integrating with the GitHub OIDC provider, so new keys are needed. Ref.: aws-actions.
- Deploy the stack below.
- Go to IAM and create a policy with write access to the bucket and
CloudFront invalidation, e.g.
infra/www-ci-policy.json
. - Create a role linked to the GitHub OIDC provider. Attach the policy.
This template is setup for one GitHub user and repo. To tweak the subject condition see example subjects.
GH_USER=shoover GH_REPO=$(basename `pwd`) aws --region us-east-1 cloudformation deploy \ --stack-name github-aws-credentials \ --template-file infra/configure-aws-credentials.yaml \ --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ --parameter-overrides GitHubOrg=$GH_USER RepositoryName=$GH_REPO
Secure static site
CloudFront cannot add response headers on its own (as of April 2021). To serve a static site with security headers, you have to add a lambda function to add headers on every origin response. See Amazon CloudFront Secure Static Website. My no-r53-branch removes the Route53 requirement, tweaks content-security-policy, and allows lambda updates.
Private static site
Authorization@Edge using cookies is a solution for private static site hosting on CloudFront/S3 with authentication via Cognito. It can be yours for the low, low upcharge of a dozen lambda functions and 2000 lines of CloudFormation. Thankfully the CloudFormation stack is available on the Marketplace and works fine, including optimizations for both static HTML and SPA modes.
To deploy a static site:
- Deploy from the Marketplace with parameter tweaks:
- Application name: customize it
- Set
EnableSpa
tofalse
- HttpHeaders Content-Security-Policy: add
font-src 'self';
if you will host font files - AlternateDomainNames: set to your domain, matching what you put in the CloudFront distribution
- In the console, request a certificate via ACM and add your domain name to the CloudFront distribution.
- Upload your content to S3.