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 recommends that you turn on Block all public access
. Here are some common issues with this approach:
In this tutorial, I’ll walk you through the secure, scalable deployment of a 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:
Let’s dive into a step-by-step approach to deploying your SPA on AWS, the right way.
Sign in to the AWS console, and enter S3
into the search bar. Select the S3 service from the results pane.
You should be presented with a page similar to this:
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.
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.
Leave everything else on the default setting and click Create bucket
.
You should now see your bucket in the list of buckets.
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:
Click into the search at the top of the AWS console, and enter CloudFront
. Select CloudFront from the list; it should look like this:
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
.
Leave all the options as default, and click Create
. Your CloudFront configuration should look like this.
Scroll down to Default cache behaviour
, and set the cache policy to CachingDisabled
. We're going to create a separate caching behavior to cover static assets later.
You can enable the Web Application Firewall if you want; for the sake of this tutorial, we will be disabling it.
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:
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.
You can see below I'm accessing index.html
and my app loads.
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.
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.
In the AWS console navigate back to CloudFront. On the left pane, select functions
. It should look similar to this:
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.
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.
function handler(event) {
const request = event.request;
const uri = request.uri;
// If the URI is missing a file name rewrite to index.html.
// If the URI is missing a file extension rewrite to index.html.
if (uri.endsWith('/') || !uri.includes('.')) {
request.uri = '/index.html';
}
return request;
}
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 (*)
.
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.
We will now add caching for our 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, any users who 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.
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
).
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 behavior, navigate to your CloudFront distribution and click the Behaviours
tab.
Click Create behaviour
.
Give the behavior 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.
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
).
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.
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
.
We now need to request AWS to issue an SSL certificate for our custom domain. Scroll down to Custom SSL certificate
, and select Request certificate
.
You should be presented with a new tab:
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).
Leave the other settings, and click Request.
Your certificate will now be pending validation
.
Under the Domains
section, we can see that AWS provided us with a CNAME name and value to add to our DNS configuration for validation. Add that record now to your DNS provider.
Once you have added the required DNS records, you will need to wait until the status for your certificate changes to be 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.
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
.
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
.
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.
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!