Amazon S3 (Simple Storage Service) is an AWS service for storing objects. Since objects are files, S3 can be viewed as a filesystem accessible over HTTP. I often use S3 for storing images, fonts, and other assets for my applications. Some examples include my jarombek.com assets and my global shared assets.
Since Amazon S3 stores files and acts as a filesystem, it can also be used to host static websites.
Static vs. Dynamic Websites
A static website displays the content found in its static assets (files containing HTML, CSS, JS, etc.). When a user navigates to a static website, the files stored on the web server are returned to the web browser without modification. There is no server-side logic impacting what content the web browser receives. In general, whatever exists in the front-end source code is what gets displayed on the webpage.
Dynamic websites serve content based on a back-end web application. This application returns assets to the web browser which are dependent on the context of the request. This means different users who request the same URL from their web browser may receive different files (HTML, CSS, JS, etc.) from the web server for rendering. For example, a signed in user might receive HTML for their profile page while a new user might receive HTML for the applications home page. Asset delivery is determined by the back-end code, which might be Node.js/Express, Java Spring, or another server-side web framework.
Dynamic websites are hosted on a web server, which nowadays often live on the cloud in a VM or container architecture. This is often more costly and takes more effort to maintain (especially in the case of a VM) than a static website's cloud storage system. However, this increased complexity also provides applications more flexibility.
I built the AWS infrastructure necessary to host my static website with Terraform. Terraform is a multi-cloud Infrastructure as Code (IaC) tool used to automate cloud infrastructure deployments. This article assumes you know how to use Terraform and work with AWS. If you need an introduction to Terraform, check out my first Terraform article.
The infrastructure I built with Terraform is shown in the diagram below.
When a client (usually a web browser) accesses content, it goes through a CloudFront distribution to an S3 bucket. The S3 bucket finds the resource matching the user's request, and passes it back through CloudFront and finally to the client.
In my infrastructure, the S3 bucket (react16-3.demo.jarombek.com) contains four resources. These resources are the files generated from my React applications Webpack build. They are index.html, app.js, styles.js, and styles.css. The following HCL code is the Terraform configuration for these objects.
index.html is the entrypoint to the React application, loading the JS and CSS files in its <script> and <link> tags, respectively. All four files are part of the same S3 bucket. Its configuration is listed next.
The name of the S3 bucket (react16-3.demo.jarombek.com) matches its domain name. The static websites index and error document are specified as index.html. This means accessing the domain from its base URL (react16-3.demo.jarombek.com instead of react16-3.demo.jarombek.com/index.html) still returns the index.html object from the S3 bucket.
One layer up from the S3 bucket is a CloudFront distribution. A CloudFront distribution is a CDN that delivers static assets through edge nodes hosted throughout the world3. CloudFront speeds up access times to static assets for clients under the premise of data locality. With assets stored on edge nodes closer to your geographical location than the S3 bucket (in my case hosted in an AWS data center in North Carolina, USA), assets can be accessed quicker.
I created two CloudFront distributions for the react16-3.demo.jarombek.com and www.react16-3.demo.jarombek.com domains. They both access assets from the same S3 bucket.
I only listed the react16-3.demo.jarombek.com distribution above because the www.react16-3.demo.jarombek.com distribution is identical except for its aliases, which are ["www.react16-3.demo.jarombek.com"] instead of ["react16-3.demo.jarombek.com"]. I could probably spend an entire article discussing CloudFront distribution configurations, however all you need to know for this demo is that it optimizes the retrieval of S3 objects.
The final piece of the static website infrastructure is Route53 DNS records for the domains and ACM certificates for using HTTPS. The Route53 records are shown below and the ACM certificate infrastructure is viewable in my jarombek-com-infrastructure and terraform-modules repositories.
The full Terraform configuration is viewable on GitHub.
One of the biggest issues I faced when setting up S3 to host a static website was redirecting www prefixed HTTPS requests to my S3 bucket. I read tutorials mentioning that two S3 buckets should be created, one with the main domain (react16-3.demo.jarombek.com) and another with the www prefixed subdomain (www.react16-3.demo.jarombek.com)4. Unfortunately this solution didn't work due to my configuration using CloudFront.
Since CloudFront creates proxy servers which retrieve and cache assets from S3, a different approach is needed to route www prefixed traffic to the same assets as base domain traffic. The solution is actually quite simple. Two CloudFront distributions are created instead of two S3 buckets. Both CloudFront distributions retrieve assets from the same underlying S3 bucket.
Another challenge arose when navigating through the React application and refreshing the page in a browser. CloudFront and S3's default behavior is to return whatever static asset is located at the path provided by the request. For example, if a browser navigates to react16-3.demo.jarombek.com/styles.css, CloudFront accesses the styles.css object from the S3 bucket.
With React Router, the URL changes whenever a new page is accessed. For example, a user can navigate from the home page (https://react16-3.demo.jarombek.com) to a Context API demo page ( https://react16-3.demo.jarombek.com/context). However, if a user refreshes the page, routing won't be handled by React Router since its only available on the client side in a static website. Therefore, the routing is handled by CloudFront.
By default, CloudFront looks for objects in S3 that correspond to the resource requested. In the case of https://react16-3.demo.jarombek.com/context CloudFront will look for an object in S3 with the name context. This object does not exist, so a 404 HTTP error is returned to the client. Users of the website receive an error page similar to the one below.
A generic XML error page isn't an ideal scenario. In this case there shouldn't be an error page displayed at all. React Router should perform the routing no matter which URL is used under the react16-3.demo.jarombek.com domain. With React Router in control, the proper pages will be loaded for valid paths and redirects to the home page will occur for invalid paths.
Learning the intricacies between static and dynamic websites helps when making infrastructure decisions for applications. Depending on the application, choosing a static website hosted on S3 can be a cheaper and easier option, resulting in zero maintenance of a web server. You can check out the full infrastructure for my static React web application on GitHub.