The Right Way to Deploy a SPA on AWS with S3 and CloudFront

Author image

Jack Baker

Published: 29/10/2024

#AWS

#S3

#CloudFront

#ACM

#SPA


Amazon Web Services (AWS) offers a broad range of tools and services to simplify the deployment, management, and scaling of applications.

I've noticed that many tutorials out there showing how to deploy a single-page application (SPA) to AWS recommend using S3 in static hosting mode with public bucket access enabled. 🚩 For me, this raised a red flag. Ensuring the security of your AWS resources is essential, and allowing public access to an S3 bucket can introduce significant risks. AWS even recommend that you turn on Block all public access. Here are some common issues with this approach:

  • Security Risks: Publicly accessible buckets can be vulnerable to unauthorised access, and there’s a risk of exposing sensitive files if they’re added in future.
  • Lack of Performance Optimisation: Static S3 hosting lacks the global caching and latency improvements offered by a content delivery network (CDN), which can result in slower load times for users further from the bucket’s region.

In this tutorial, I’ll walk you through the secure, scalable deployment of an SPA (built with frameworks such as React, Vue, or Angular) following AWS best practices. We’ll use S3 for storage, integrate CloudFront as a CDN, and secure everything with Amazon Certificate Manager. This approach provides key advantages:

  • Enhanced Security: Using CloudFront with Origin Access Identity (OAI) or Origin Access Control (OAC), we’ll restrict direct access to the S3 bucket, reducing exposure to unauthorised access.
  • Improved Performance: CloudFront caches content at edge locations worldwide, delivering faster load times by serving content closer to users.

Let’s dive into a step-by-step approach to deploying your SPA on AWS, the right way.

Prerequisites

  1. AWS Account
  2. Build output for a single-page application (React, Vue, Angular, etc.)
  3. A domain name (optional if you're happy with the CloudFront domain name)

1. Create AWS S3 bucket

Sign in to the AWS console and enter S3 into the search bar. Select the S3 service from the results pane.

Image

You should be presented with a page similar to this:

Image

S3 buckets are region-specific. Ensure that you have selected your desired region at the top right of the AWS console. You should select a region that is closest to your location, for me that is eu-west-2.

Click the Create bucket button and give your bucket a name.

Image

Under Object Ownership select ACLs disabled (recommended) and block all public access to the bucket. We only want to allow traffic to have access to our S3 files through CloudFront and not S3 directly.

Image

Leave everything else on the default setting and click Create bucket.

You should now see your bucket in the list of buckets.

Image

2. Upload your app to AWS S3

Navigate into the bucket you just created and click Upload.

Upload the build output of your single-page application. For a react app it should look something like this:

Image

3. Configuring CloudFront

Click into the search at the top of the AWS console and enter CloudFront. Select CloudFront from the list, it should look like this:

Image

Click the Create distribution button.

Select your S3 bucket as the Origin domain and under Origin access select Origin access control settings (recommended).

Create a new OAC by clicking Create new OAC.

Image

Leave all the options as default and click Create. Your CloudFront configuration should look like this.

Image

Scroll down to Default cache behaviour and set the cache policy to CachingDisabled. We're going to create a separate caching behaviour to cover static assets later.

Image

You can enable Web Application Firewall if you want, for the sake of this tutorial we will be disabling it.

Image

Leave everything else and click Create.

An alert should now appear at the top of the AWS console showing that the S3 bucket policy needs updating. Copy the policy and then click the Go to S3 bucket permissions to update policy link.

Scroll down to Bucket policy and click Edit.

Paste the policy into the editor, it should look something like this:

Image

Click Save changes.

You will now be able to access files in your S3 bucket through the CloudFront distribution domain name in the general settings of the CloudFront distribution.

Image

You can see below I'm accessing index.html and my app loads.

Image

This is great! But you'll notice if we go to the root level of the CloudFront domain we'll get an access denied error because our CloudFront distribution does not know what file to return.

Image

We can all agree that accessing index.html in the URL for our single-page application is not ideal so we need to implement a CloudFront function to handle index.html rewrites for our single-page application.

4. CloudFront Rewrite Rules

In the AWS console navigate back to CloudFront. On the left pane select functions. It should look similar to this:

Image

Click Create function.

Give your function a name such as spa-rewrite. The function will be generic enough to be shared across multiple CloudFront distributions so it can have a generic name.

Image

Click Create function.

Scroll down to Function code section and paste the following handler into the editor. This function adapts the example for single-page apps provided in the aws-samples GitHub for CloudFront functions.

1function handler(event) {
2    const request = event.request;
3    const uri = request.uri;
4
5    // If the URI is missing a file name rewrite to index.html.
6    // If the URI is missing a file extension rewrite to index.html.
7    if (uri.endsWith('/') || !uri.includes('.')) {
8        request.uri = '/index.html';
9    }
10
11    return request;
12}
Image

Click Save changes.

Click the Publish tab.

Click Publish function.

Scroll down to Associated distributions.

Click Add association.

Select the CloudFront distribution we created earlier.

Select the event type as Viewer request.

Select the Cache behaviour as Default (*).

Image

Click Add association.

You will see now that refreshing our app on the root level domain or any path that isn't a file name will load our app.

Image

We will now add caching for our static assets.

5. Caching Static Assets

By design, in this tutorial we don't want to cache our index.html file. This will ensure that when the application code is updated in our S3 bucket that any users that load the application will automatically be presented with the latest version of the app. You can see that in the x-cache header response when we request our application index.

Image

You can change this by implementing a default cache TTL of 10 minutes for example. However, this would mean that when your application updates changes may not be visible to the user until that 10-minute period has ended.

You can also see that any of our static assets are also currently not cached (x-cache value is Miss from cloudfront).

Image

With our example react app all of the static assets are within the static directory and all include a build hash. As these files include a build hash we can cache them with a long TTL to improve performance for fetching those files across the CloudFront edge locations. The folder where the static assets are located can change based on which framework you are using.

To create a new cache behaviour navigate to your CloudFront distribution and click the Behaviours tab.

Click Create behaviour.

Give the behaviour a path pattern that matches the folder structure for your static assets. For me, this is static/* to cache all files under the static directory.

Select the origin as your S3 origin.

Scroll down to Cache policy and select CachingOptimized. You can implement your own caching policy but for this tutorial we will use the AWS defaults.

ImageImage

Click Create behaviour.

You'll see now when we refresh our app that any static asset is now cached in CloudFront (x-cache value is Hit from cloudfront).

Image

6. Linking Custom Domain

If you don't want to be stuck with the default CloudFront distribution name then you will want to add a custom domain name to access our new single-page application. For this step, you will require a pre-purchased domain name. For this tutorial I want my React app to be available at react-app.jackbaker.dev.

Open your CloudFront distribution general settings tab.

Image

Click Edit.

We need to add an Alternate domain name.

Click Add item and enter the domain name you want to use. For me, that will be react-app.jackbaker.dev.

Image

We now need to request AWS to issue a SSL certificate for our custom domain. Scroll down to Custom SSL certificate and select Request certificate.

You should be presented with a new tab:

Image

Select Request a public certificate and then select Next.

Enter your domain name in the Fully qualified domain name field and select DNS validation (we will use DNS validation so that AWS can auto-renew out certificate when it is close to expiry).

Image

Leave the other settings and click Request.

Your certificate will now be pending validation.

Under the Domains section we can see that AWS has provided us with a CNAME name and value to add to our DNS configuration for validation. Add that record now to your DNS provider.

Image

Once you have added the required DNS records you will need to wait until the status for your certificate changes to Issued. This will depend on the time it takes for your DNS changes to propagate. This is usually not too long but can take up to 72 hours. When your certificate is issued it will look like this.

Image

Now navigate back to your CloudFront tab and select the newly issued certificate in Custom SSL certificate. Leave the rest of the settings the same and click Save changes.

From the CloudFront distribution general settings page copy the Distribution domain name.

Image

Now we need to make another DNS change. Open your DNS provider and navigate to the DNS settings for your domain. Create a new CNAME record, and set the CNAME name to the domain you want to access your application from. For this tutorial, it is react-app.jackbaker.dev. Then set the CNAME value to the CloudFront distribution name.

Image

Save your DNS changes and wait for them to propagate.

You'll now see that accessing our domain name within the browser loads our application over HTTPS.

Image

Final Thoughts

Congratulations 🎉 You now have a secure, scalable, globally distributed single-page application hosted on AWS using S3 and CloudFront, accessible through a custom domain over HTTPS. Enjoy the scalability and resilience provided by AWS!