/ INFRASTRUCTURE, AWS

Multisite Deployment with AWS CloudFront

AWS CloudFront is a popular solution for deploying websites and single page applications (SPAs). This post explains how to deploy multiple sites on different subdomains using a single S3 bucket and CloudFront distribution. This is not a step-by-step tutorial, but it does contain relevant sample code.

CloudFront Overview for SPAs

Hosting a website in CloudFront is relatively straightforward. Create an S3 bucket and create a CloudFront distribution pulling from the S3 bucket. Set the default index document to index.html (or whatever), and this setup will work fine for most static sites. However, if CloudFront is being used to host a single page application, the error document will also need to be set to index.html.

This is due to the way CloudFront serves files. When a path, such as /login or /user/account is requested, CloudFront will look for an object matching that path in the S3 origin bucket. Since such a path does not exist, CloudFront will return a key error, when the desired behaviour is to let the SPA handle this path. Setting the error document to the index document, will give the desired behaviour.

This is a well-documented method for hosting SPAs on CloudFront.

Here’s the gotcha: CloudFront will not return index or error documents in ‘subdirectories’.

Related: S3 is an object store and doesn’t actually have directories.

Routing and S3 bucket structure

Since the goal is to use different subdomains as the differentiator between sites, the S3 bucket will contain a separate folder for each site. e.g. The following domains should point to the following ‘directories’. site1.example.com => s3:///site1 site2.example.com => s3:///site2 site3.example.com => s3:///site3

Furthermore, a wildcard DNS record will need to be created direct all traffic from *.example.com to the CloudFront distribution.

Modifying the request with CloudFront and Lambda

CloudFront provides four different types of hooks to modify requests and responses via Lambda@Edge functions, but we only need to focus on two, the Viewer Request and the Origin Request. In AWS CloudFront console, under Behaviours, Lambda function ARNs can be specified for each type of hook.

Viewer Request

This is triggered as soon as the request from a client comes to CloudFront. In this request, we need to take the leftmost part of the domain and add it as a prefix to the URI.

function.js

exports.handler = (event, context, callback) => {
  // Get request object from CloudFront event
  var request = event.Records[0].cf.request;

  // Extract host from request
  // e.g. host = site1.example.com
  var host = request.headers.host[0];

  // Extract folder from host
  // e.g. folder = site1
  var folder = host.value.split(".")[0];

  // update uri to include folder
  request.uri = '/' + folder + request.uri;

  return callback(null, request);
};

Origin Request

This is triggered after the request has entered CloudFront, but before the request hits the S3 origin bucket. In this request, we need to take the return a file for any file that is specified, otherwise, return the index document in the subdomain folder.

function.js

exports.handler = (event, context, callback) => {
  // Get request object from CloudFront event
  var request = event.Records[0].cf.request;

  // This is a fragile, hacky way to check if the URI specifies an actual file.
  

  // update uri to include folder
  request.uri = new_uri;

  return callback(null, request);
};