I've been tasked with a very particular problem that I've been racking my brain to solve and despite being a fairly proficient Googler I was really struggling to find a solution. By no means is this a definitive solution, but it is a solution.
This post makes a few assumptions about your knowledge, namely that you already know; how to set up an S3 bucket to host a static website, how to set up a cloud-front distribution to use an S3 static website origin and how to create and link Lambda@Edge scripts to CloudFront distributions.
The problem
We have a complex React application that has been in use in production for approx 5 years. It has white-label theming and is served on well over a dozen subdomains (one per theme, plus one for our own brand) It is hosted through S3 and CloudFront on AWS. With lot's of alternate names and a wildcard SSL.
For the sake of the post we will say it's hosted on myapp.example.com
e.g. myapp.example.com/home
where a white-label would be yourbrand.example.com/home
resolving to the same content in CloudFront and the same S3 bucket. (Each subdomain is set up in DNS just to CNAME to the same cloudfront URL)
Time has passed and we have now built a new React app, however we are required to host it off of the same subdomain on a static subpath. It is also required that both the old and new apps can keep working in tandem.
e.g. myapp.example.com/newsite/home
Limitations
- You cannot host multiple static sites through a single S3 bucket, the index.html file must be in the root of the bucket.
- CloudFront custom error pages always redirect to the default origin regardless of path.
The solution
Store the new app in a new S3 bucket with static website hosting enabled and use Lamda@Edge function in the CloudFront behaviour on Origin Request to handle requests and if they are on the /newsite/
path change over to a custom origin that sends traffic to the new site.
Crucially, in this setup, the CloudFront distribution itself does not need to know anything about the new S3 origin directly, the only thing you need to update is the behaviours so that the Origin Request is linked to the Lambda@Edge function. If you have any default error pages you will need to remove them too.
The app
In the build process of the new app we specify a PUBLIC_URL
env to put the static content on to a specifc route.
e.g. /newsite_static/
The bucket
The file structure of your new bucket should now resemble something like
- index.html
- manifest.json
- newsite_static/
- static/
- css/
- js/
The script
exports.handler = async (event, context, callback) => {
const request = event.Records[0].cf.request;
const newBucketOrigin = "myapp-newsite.example.com.s3-website.eu-west-2.amazonaws.com";
const MATCHING_PATHS = ['/newsite/', '/newsite_static/'];
/**
* If we want to use the new app, based on path,
* Then set custom origin for the request
* to override cloudfront config
*/
if (MATCHING_PATHS.some(path => request.uri.startsWith(path))) {
request.origin = {
custom: {
domainName: newBucketOrigin,
port: 80,
protocol: "http",
path: "",
sslProtocols: ["TLSv1", "TLSv1.1", "TLSv1.2"],
readTimeout: 5,
keepaliveTimeout: 5,
customHeaders: { ...request.origin.custom.customHeaders }
}
}
request.headers['host'] = [{ key: 'host', value: newBucketOrigin }];
}
callback(null, request);
};
Our script is slightly modified from the above, as we use the same script across multiple cloudfront distros so the origin match is not a hard coded string but an object which has a lookup performed against the host to find the correct origin.
Outcome
Requests to myapp.example.com/home still go to the legacy app
Request to myapp.example.com/newsite/home now go to the new app.
Enhancements
Because we've turned off the custom error pages in CloudFront that handle things falling back to our index.html file we have also introduced a 2nd Lambda@Edge script to fire on Origin Response to handle that same status code change.
Special mentions
I spent a lot of time trying to figure this out so it worked exactly as we needed it to, and read a lot of articles and watched a lot of content, some more helpful than others. 2 pieces in particular really helped though.
Top comments (1)
Thank you so much for this write up! I spent an entire day wrestling with wanting to use example.com/ as production and example.com/staging using React and AWS S3 + Cloudfront and finally got it to work!
One question I have is why does the folder structure for the new bucket have to be in the form of /public-url-name/static? Why can't I have the PUBLIC_URL be blank and deploy to the new bucket without the extra folder?