Static Sites
Static Sites and their deployment. Demystified.
The Basics
It all starts with a AWS CloudFormation Template and an Amazon S3 Bucket.
The beginnings of the site's template: We will dive into the details of each property as we fill them in.
The AWSTemplateFormatVersion is optional and is omitted here.
Resources
The resources section is the more dense portion in all the land of yaml.
Bucket
This hosts our static site. The site consists of an index (index.html
) document, an error (404.html
) document, and directories for styles and images.
AWS CloudFormation Stack deletion fails for buckets that have contents.
The key take away from the above yaml snippet is the AccessControl property. S3 bucket misconfiguration is costly.
The website configuration definition informs the bucket to host the index page of the static site.
AWS CloudFormation will provide a name for the S3 bucket if one is not provided. We will be using the AWS Command Line Interface (CLI) to sync
the contents of the static site. As such we define an easy to type name by concatenating the AWS CloudFormation stack name and the purpose of our bucket.
Bucket names must be globally unique. The AWS CloudFormation Stack will fail to create the S3 Bucket if the name has been taken.
It is important to highlight the Amazon S3 Deprecation Plan.
Bucket Names with Dots – It is important to note that bucket names with “.” characters are perfectly valid for website hosting and other use cases. However, there are some known issues with TLS and with SSL certificates. We are hard at work on a plan to support virtual-host requests to these bucket...
Bucket Policy
This policy allows anyone (Principal: '*'
) to GET
all objects within the bucket (/*
).
First Deploy
This is the most basic form of hosting a static site from an Amazon S3 Bucket.
Define an output for the S3 website endpoint and deploy the static site.
The full template:
Template Validation
To validate the template use the CloudFormation Linter.
Stack Deployment
Create the AWS CloudFormation stack:
Site Contents Deployment
Note: This the value provided to BucketName
Navigate to the bucket's domain name to see the results.
TODO: add ways to retrieve the endpoint
Limitations of Website Configuration
This creates an public bucket and the domain name provided by Amazon S3 is not easy to grok or share. This solution will suffice for simple or temporary sites that do not contain sensitive documents. This approach is effective for impromptu presentations with Hugo or showing a client a demonstration.
Domain Names & Certificates
Adding a custom domain name to a static site is dependant on the manner in which you posses the domain and it's certificates.
For the sake of this paper it is assumed that domains are managed with Amazon Route 53.
Parameters
Adding parameters makes the deployment of statics sites repeatable. Start by defining a domain for the new static site.
Define a domain name to use for each site.
Further reading on extending parameters.
Certificate Validation
To use TLS we must provide a valid ViewerCertificate
to the Amazon CloudFront
DistDistribution. Certificates can be
uploaded
to AWS Certificate Manager but is outside the scope of this paper.
To obtain a valid certificate from AWS include the following in the Resources
section of template.yaml
.
AWS CloudFormation stacks will remain in the
CREATE_IN_PROGRESS
state until adding the appropriate CNAME record to your DNS configuration[0].
Limitations of Automating DNS Validation
AWS CloudFormation will only output the Name and Value for root domain and any sub domains need to be manually validated[0] in the console. Although it makes for a messy template, my preferred strategy is to validate a single certificate to include the wild card alias for the domain. e.g. *.domain.com
In that case a second parameter for that certificate's Amazon Resource Name (ARN) will need to be defined. Using a hard-coded value will suffice.
AWS CloudFormation Distributions
Domain names and certificates aren't enough. We will use a distribution to associate our bucket to our domain name via the Viewer Certificate.
SPA or Subdirectories
Amazon CloudFront does not return the root object from subdirectories[0]. This does not pose a problem for SPAs.
In the case of a single page application (SPA) we restrict access to the bucket using an Amazon CloudFront origin access identity (OAI). We will see that when hosting a static site with subdirectories, like a Hugo site, we leave the AccessControl
on the bucket as public-read
.
Subdirectory Distribution
Distributions could be their own paper. Distributions are defined within the Resources
property of template.yaml
.
The remaining DistributionConfig
properties in detail:
Aliases - the custom endpoint to use for the distribution.
Example with sub-domain.
CustomErrorResponses - inform distribution how to handle errors.
Origins - the source of the distribution
Take note of the OriginProtocolPolicy
. In order to surface the index document of subdirectories we must respect the S3 protocol policy. We rely on the DefaultCacheBehavior
's ViewerProtocolPolicy
to redirect to https. The caveat with this approach is that the S3 website endpoint will remain public.
DefaultCacheBehavior - inform the distribution how long to cache items
This block instructs the distribution to cache GET
and HEAD
requests. We also inform the distribution of the cache's target origin. We request that the assets be compressed. This distribution does not serve video and instruct the distribution to turn off stream smoothing. Finally we instruct the distribution to forward query strings but ignore cookies.
ViewerCertificate
SPA Distribution
As mentioned above, we are able to restrict access to our site's bucket by creating and using the Amazon CloudFront OAI.
Update SiteBucket
's AccessControl
property the now that access will be restricted to Amazon CloudFront. While we're at it, we can remove the WebsiteConfiguration
since the distribution defines the root object and custom
errors.
The static site bucket should be as follows:
Update the site bucket policy principal to reflect the Amazon CloudFront OAI user. This makes for an ugly template
TODO: Investigate a more elegant way to provide the OAI user string.
Replace the SiteDistribution
's Origins
CustomOriginConfig
with the S3OriginConfig
as follows.
Lesson Learned
SPAs restrict bucket access to a CloudFront Origin Identity. Distributions that need to surface the root object of a subdirectories require a custom origin configuration (CustomOriginConfig
).
Routing
Although it makes for a larger template, the distribution configurations for many website buckets can be contained in a single file. Create an Amazon Route 53 Record Set Group to coordinate multiple sub domains.
The example below defines two records. The base domain name and a blog sub-domain endpoint.
The Hosted Zone Id for Amazon CloudFront is fixed.
Specify Z2FDTNDATAQYW2. This is always the hosted zone ID when you create an alias record that routes traffic to a CloudFront distribution.
Outputs
Outputs are optional. They are used here to provide the necessary information for make tasks.
We will inform a publish task of the bucket name for publishing. In the same task we will invalidate the cache distribution to force the new static contents to be served.
Continuing with our blog sub-domain example:
Makefile
The full example can be found on github.
Main Source: