Review apps with AWS CloudFront for HTML+JS projects
Improve your team’s reviews by automatically deploying each branch to its dedicated environment.
Review applications can make a team’s reviews quicker and more efficient by leveraging the repetitive tasks required to manually spin up a dynamic environment for a pull request. They can also be used to showcase product changes to designers without manual deployments to a sandbox environment.
In our experience, if developers are not forced to stash their work and checkout another branch every time a review is required, they are more focused in reviewing changes, and this has helped improve the overall quality of our work. By providing designers a live, always up-to-date preview environment, we also strengthened the cooperation between them and our devs, and accidental style changes are now fixed sooner than they used to be.
This article focuses on a simple, yet perhaps common scenario: an HTML+JavaScript application that uses an external API to run. As such, sources can be uploaded on AWS S3 and served through CloudFront. Throughout all the following examples, review.man.wtf
will be used as the base domain so that a working demo could be provided.
The big picture
The idea is to use a single CloudFront distribution that serves *.review.man.wtf
, and a single S3 bucket named review.man.wtf
that stores application files prefixed by the variable part of the domain. So, for example, the URL https://test.review.man.wtf/image.gif will serve the file stored in S3 as s3://review.man.wtf/test/image.gif
. When a new branch feature/foo
is pushed in the VCS, the branch name is made URL-friendly, and the review app is deployed at https://feature-foo.review.man.wtf/.
At the time of this writing, CloudFront configuration alone is not flexible enough to make this work seamlessly. However, developers are allowed to attach Lambda functions to their CloudFront distributions (known as Lambda@Edge), that are capable of manipulating requests and responses to and from the client, and to and from the upstream source — in this case the S3 bucket.
Creating resources on AWS
Prerequisites
Of course, an AWS account is required, as well as a domain since you’re going to create a couple DNS records. Always replace review.man.wtf
with your actual domain in the following examples and code snippets.
For encrypted communications to be trusted, an SSL certificate valid for *.review.man.wtf
must be imported or generated for free in AWS Certificate Manager. Since it will be associated to a CloudFront distribution, it is mandatory that said certificate is in us-east-1
region—known as US East (N. Virginia)—otherwise it won’t work.
The following section will guide you throughout the setup of all components—S3 bucket, Lambda functions and CloudFront distribution. If you prefer to get straight to the point, you can deploy a CloudFormation template instead. Due to Lambda@Edge constraints, the Stack must be created in US East (N. Virginia) region.
S3 Bucket
Start by creating an S3 bucket. The name can be whatever you like, but using the non-variable part of the domain can be a good choice.
Create the bucket in any region you like: the closest to the physical location of your team, the better. However, if you create a bucket in a region other than US East (N. Virginia), keep in mind that the “global” virtual-hosted-style URL for the bucket (bucket-name.s3.amazonaws.com
) may take hours to become available.
The bucket can be tagged, and other settings can be adjusted depending on your use case. Objects in the bucket should remain private, because you’re going to grant specific permissions for CloudFront shortly.
CloudFront Distribution
Next, create a CloudFront Web distribution.
In “Origin Setting”, choose the bucket you just created to be the source where CloudFront forwards requests upon cache misses. Leave the path empty: it will be populated by the Lambda function created for the Origin Request event.
Also, you must grant CloudFront the permission to read contents on the bucket. Either choose an existing Origin Access Identity or create a new one, and ensure bucket policy is configured properly. When using AWS Console, this can be done automatically by letting AWS update the bucket policy for you.
In “Default Cache Behavior Settings” most settings can be left untouched to their defaults, despite it can be a good idea to redirect HTTP requests to HTTPS and compress objects automatically.
It is crucial to tell CloudFront to create different caches based on the X-Original-Host
header that will be set by the first Lambda@Edge trigger. To do so, select “Whitelist” for “Cache Based on Selected Request Headers” and add the custom header “X-Original-Host”.
Scroll down to “Distribution Settings” and configure the domain name this distribution will serve. Write the domain with the wildcard (e.g. *.review.man.wtf
) so that CloudFront will accept requests for any subdomain. Then, choose the appropriate SSL certificate to make sure encrypted connections are started using a certificate that covers the domain in use.
Depending on how your application works, you’ll probably want to also set “Default Root Object” to index.html
—this value represents the entry point of your application. Adjust any other setting to fit your needs, however default values should be just fine for most users. Just be sure you do not accidentally choose “Legacy Clients Support” for “Custom SSL Client Support”, because that would cost 600$ per month!
Lambda@Edge function for Viewer Request event
Now, create a new Lambda function that will be triggered on “viewer-request” events, that is when a client request hits CloudFront, before any cache is evaluated. Since Lambda@Edge functions must be in us-east-1
region, you must create this function in US East (N. Virginia); otherwise you won’t be able to link this function to CloudFront. Also, when creating the service role for the Lambda function, remember to include the “Basic Lambda@Edge permissions (for CloudFront trigger)” policy template—or create a custom role that can be assumed by lambda.amazonaws.com
as well as edgelambda.amazonaws.com
.
This callback is responsible for storing the content of the Host
header originally passed by the client into another header, X-Original-Host
, so that its value can be read later. This is necessary because CloudFront overrides the Host
header when building the request to S3, and the original value would be lost unless stored somewhere else.
Choose Node 8.10 as the runtime, set index.handler
as the handler, and create a function with the following code:
After the function has been saved, it can be deployed to Lambda@Edge: click on “Actions › Capabilities › Deploy to Lambda@Edge”. A modal window will be shown that lets you configure the CloudFront trigger: choose the distribution created before, select “*” as “Cache behavior”, and “Viewer request” as the CloudFront event that will trigger this function.
Lambda@Edge function for Origin Request event
This time, create a new Lambda function that will be triggered on “origin-request” events, that is when CloudFront needs to forward the request to the upstream origin, after the attempt to serve a cached version was made, but before the request is sent to S3. Once again, you must create this function in US East (N. Virginia) and include the “Basic Lambda@Edge permissions (for CloudFront trigger)” policy template.
This callback is responsible for reading the contents of the X-Original-Host
header set by the other function, split the domain into its dot-delimited sub-parts (technically called “labels”) and use those that precede the common suffix to build the S3 path where resources are located. For the sake of simplicity, we’ll assume that only one sub-level is allowed and just use the first label, which is a reasonable assumption in most situations—the SSL certificate wouldn’t be valid for multiple sub-levels anyway. It is trivial to extend the code and cover more cases.
Choose Node 8.10 as the runtime, set index.handler
as the handler, and create a function with the following code:
Again, after the function has been saved it must be deployed to Lambda@Edge. Click on “Actions › Capabilities › Deploy to Lambda@Edge”; in the modal, choose the right distribution, “*” as “Cache behavior”, but this time set “Origin request” as the CloudFront event that will trigger this function.
DNS records
Last, but not least, make the distribution reachable by updating DNS. Create a CNAME record that points to the CloudFront distribution. If your DNS provider supports creating ALIAS records, prefer those over CNAMEs as they will speed up DNS resolution a little, although this is quite a pointless optimisation for a setup that’s meant to be for internal use.
Test setup
That’s it! The CloudFront distribution may take a while to provision, because changes need to be propagated across all “Edge Locations” around the world.
As soon as the distribution is ready, it’s time to run a quick test: upload an image in your bucket under some prefix (e.g. test/image.gif
), then try to access it from your browser (e.g. https://test.review.man.wtf/image.gif). If that’s the image you uploaded, congratulations, you’re done!
Managing review apps
With the infrastructure described above, starting a review application is just a matter of uploading built sources in the right S3 bucket under the right path.
The same review app can be re-deployed multiple times if, for instance, additional commits are added to a feature branch: in this case, you must create a CloudFront invalidation to notify the distribution that the caches should be nuked and served object should be refreshed from S3. Cache invalidations will always be sub-optimal, but that won’t be a big issue in most cases if review applications are meant for internal use.
Finally, review applications can be stopped (e.g. after the feature branch gets merged) by simply deleting files on S3, and again creating a CloudFront invalidation.
The following is an example GNU Makefile that implements recipes to start and stop review applications:
Conclusion
You have configured an environment that lets you deploy multiple versions of your application with ease. All the AWS resources created so far have no monthly fees, so as long as your sources’ size isn’t huge, and the data transfer remains below a reasonable value, the cost of your “review applications” stack shouldn’t be higher than 1/10th of a beer.
Review applications are most useful when integrated in a CI/CD flow: just update continuous integration scripts to run make review-start ALIAS=$BRANCH_NAME
every time a branch is pushed. This will make your pull requests much quicker to test, and new features much simpler to show off to executives and designers.
Some platforms like GitLab will also let you run make review-stop ALIAS=$BRANCH_NAME
once pull requests are closed or branches are deleted: this is good to keep bucket size under control, but it is not strictly necessary, and the same goal may also be achieved by other means—like S3 Object Lifecycle.
Although the setup described in this article is basic and limited to static sites, the approach can be tailored to fit your specific needs, or be extended to suit more complex scenarios. Please, let us know what you’ve built in the comments below!
See it in action:
Chialab is a design company. By developing strategy, design, software and content, we generate exciting relationships between brands and people. https://www.chialab.it.