<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael</title>
    <description>The latest articles on DEV Community by Michael (@thornyweb).</description>
    <link>https://dev.to/thornyweb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559542%2Fcc6002ab-4629-45d2-9712-5fd628bfbb69.jpg</url>
      <title>DEV Community: Michael</title>
      <link>https://dev.to/thornyweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thornyweb"/>
    <language>en</language>
    <item>
      <title>How to serve multiple React apps in S3 from a single CloudFront distribution</title>
      <dc:creator>Michael</dc:creator>
      <pubDate>Tue, 15 Feb 2022 16:30:11 +0000</pubDate>
      <link>https://dev.to/thornyweb/how-to-serve-multiple-react-apps-in-s3-from-a-single-cloudfront-distribution-1pce</link>
      <guid>https://dev.to/thornyweb/how-to-serve-multiple-react-apps-in-s3-from-a-single-cloudfront-distribution-1pce</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the sake of the post we will say it's hosted on &lt;code&gt;myapp.example.com&lt;/code&gt; e.g. &lt;code&gt;myapp.example.com/home&lt;/code&gt; where a white-label would be &lt;code&gt;yourbrand.example.com/home&lt;/code&gt; 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)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;e.g. &lt;code&gt;myapp.example.com/newsite/home&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You cannot host multiple static sites through a single S3 bucket, the index.html file must be in the root of the bucket.&lt;/li&gt;
&lt;li&gt;CloudFront custom error pages &lt;strong&gt;always&lt;/strong&gt; redirect to the default origin regardless of path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;/newsite/&lt;/code&gt; path change over to a custom origin that sends traffic to the new site.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app
&lt;/h3&gt;

&lt;p&gt;In the build process of the new app we specify a &lt;code&gt;PUBLIC_URL&lt;/code&gt; env to put the static content on to a specifc route.&lt;/p&gt;

&lt;p&gt;e.g. &lt;code&gt;/newsite_static/&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The bucket
&lt;/h3&gt;

&lt;p&gt;The file structure of your new bucket should now resemble something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; - index.html
 - manifest.json
 - newsite_static/
     - static/
       - css/
       - js/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newBucketOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp-newsite.example.com.s3-website.eu-west-2.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MATCHING_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/newsite/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/newsite_static/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * If we want to use the new app, based on path,
   * Then set custom origin for the request
   * to override cloudfront config
   */&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MATCHING_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newBucketOrigin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sslProtocols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;keepaliveTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;customHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customHeaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newBucketOrigin&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcome
&lt;/h2&gt;

&lt;p&gt;Requests to myapp.example.com/home still go to the legacy app&lt;br&gt;
Request to myapp.example.com/newsite/home now go to the new app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancements
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;Origin Response&lt;/strong&gt; to handle that same status code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special mentions
&lt;/h2&gt;

&lt;p&gt;I spent &lt;strong&gt;a lot&lt;/strong&gt; 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. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/@mnylen/lambda-edge-gotchas-and-tips-93083f8b4152" rel="noopener noreferrer"&gt;Lambda@Edge gotchas and tips&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=Cz0unTbOfgo" rel="noopener noreferrer"&gt;Serving Multiple Websites with AWS CloudFront &amp;amp; Lambda at Edge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>react</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
