There are many guides online for how to host a single-page application (SPA) on AWS with S3 and CloudFront but none (that I found) covered this in a proper way. Many are out-dated, they left the S3 bucket as public, or didn’t identify how to properly handle errors when you get a 404 or if the object does not exist in a bucket. The later being a minor oversight that caused me to write this guide.
Prerequisites
Since you are using AWS, and Google favors HTTPS over HTTP, it makes sense to get a free SSL cert that can be used for your site. Unlike, Let’s Encrypt, this cert will last 1 year and is free as long as you are using it. You should go to Certificate Manager and create an SSL cert for your SPA. You should get a cert for the main domain and a wildcard at minimum.
e.g.
domain.com
*.domain.com
If you have any other sub-domains that doesn’t fit into this, something like stage.assets.domain.com
then you should add them too. For the example, you should use this wildcard *.assets.domain.com
as it will allow you to add more environments later if you need to.
You can also import an SSL cert from a third-party but this is outside the scope of this guide.
This is not required but there is no reason not to use an SSL cert at this point.
Create an S3 bucket
It’s important to note that there is no need to have the bucket named the same as the website. There is also no need to turn on static website hosting on the bucket. We are going to use CloudFront so this is not necessary. If you wanted to host directly from S3, then you will need to name the bucket the name of the website and enable that setting.
There is nothing special to be done here. Just click Create bucket, enter a name and click Create. Once CloudFront is setup, you will need to come back here and modify the Bucket Policy to ensure your SPA works as expected. If you miss this final step (explained at the bottom of this guide), your SPA will not work as expected.
Setting Up CloudFront
CloudFront is where public access is given to access the content of the bucket. You do not want the S3 bucket to be public primarily due to costs of accessing the content. Also, it’s a better security practice to keep your S3 buckets private.
I don’t feel I need to get into every setting of CloudFront or post any pics of settings for it. I will explain the setting needed for the setup as this is not a full guide for CloudFront as there is no point, the documentation does this well enough.
Go ahead and create a new CloudFront distribution. Select Web as this will be used for the web.
Origin Domain Name
The first setting is the Origin Domain Name. Given that this is an origin, where the content will originate, we want to use the S3 bucket we created above. You should see it in the list as an available option. If not, you can just use the URL for the bucket without the protocol. e.g. an S3 bucket named example-bucket is us-east-1 will be example-bucket.s3.amazonaws.com
. Note that if you are in other regions, the URL will differ, see the documentation to know your URL. Keep in mind, this is good to know but not necessary if your S3 bucket appears in the list.
Origin Path
Unless you have your project inside a folder in your bucket, you will not need to specify an Origin Path as this is identifying if CloudFront needs to point to a specific folder within the bucket. You could do version folders here if you want or even specify environments. There are numerous reasons to use origin paths and it varies on your project and company standards.
Restrict Bucket Access
We want to protect the S3 bucket, so we will restrict bucket access. Select Yes for this option.
Origin Access Identity
If you are following this guide, I am assuming you have no Access Identity setup for this already. Select Create a New Identity and keep the default name (Comment) or rename to make sense for your use. I have never had to rename this in my cases but to each their own.
Grant Read Permissions on Bucket
To apply the new Access Identify permissions to the S3 bucket, you will want to select Yes, Update Bucket Policy. I will show you how to verify this later in this guide.
Viewer Protocol Policy
If you have an SSL cert, you should select Redirect HTTP to HTTPS.
Allowed HTTP Methods
For most SPAs, the default setting (GET, HEAD
) will work.
Compress Objects Automatically
You should select Yes to compress objects automatically. This will make the files transmitted smaller thus saving you money.
Alternate Domain Names (CNAMEs)
This is where you enter the URL for your project. You can have multiple URLs for CNAMES here but if you used an SSL cert or created one with Certificate Manager, then these URLs should be included on the SSL cert.
SSL Certificate
Select your SSL cert from the drop down.Remember, it is important that the SSL cert contains all the URLs used in the setting above.
Default Root Object
This is typically the index.html
file but sometimes the project may use another.
This is all that is required to get CloudFront started but not fully working properly. We still need to make some minor edits to CloudFront but we need to create it first. Go ahead an click Create Distribution.
Finalize CloudFront
With the distribution created, click on the ID of the distribution to get back into the settings. You should see multiple tabs and one of them being Error Pages. Click on Error Pages and then click on Create Custom Error Response. From here, you need to setup a redirect for 404 handling to ensure your SPA works properly.
HTTP Error Code
You want to customize the 404: Not Found HTTP error code.
Error Caching Minimum TTL (seconds)
This is the time-to-live (TTL) which is how long the request will remain cached. For dev, I use a 0
value and for production I personally use a 300
value (5 mins).
Customize Error Response
You will want to set this to Yes, so you can customize the response.
Response Page Path
You will want to redirect the 404 error back to you SPA so you will want to enter /index.html
or if your SPA uses something else, enter that here.
HTTP Response Code
You want to return a 200: OK for the response since this is a SPA.
Click on Create and that is all you need to do for CloudFront!
Verify CloudFront Origin Access Identity
This step is not necessary but it’s useful to know. To verify the Origin Access Identity that you setup in CloudFront: while still on the CloudFront system, on the left sidebar, you will will see Origin Access Identity under the Security side-menu. Here is a list of all CloudFront origin access identities. If you ever need to find the ID for one of these identities, this is the place. You will see the ID column on this page. That is the ID that is used to secure your S3 bucket.
You can check the ID against the ID in your S3 bucket policy to ensure the correct CloudFront Origin Access Identity is being used.
Finalize S3 Bucket Policy
Now that you have your S3 bucket setup with CloudFront Origin Access Identity, you should also have your CloudFront error page response setup too. The final step to get this all working is to make one minor adjustment to your Bucket Policy.
By default, your bucket policy will look something like:
{ "Version": "2008-10-17", "Id": "PolicyForCloudFrontPrivateContent", "Statement": [ { "Sid": "1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ORIGIN_ACCESS_ID" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::BUCKET_NAME/*" } ] }
The problem here is if an object does not exist in the bucket, even with the 404 error page redirect, you will see the error NoSuchKey. This is common for endpoints like /about
. This is actually a known issue and is clearly defined in the knowledge center. The fix is very simple but unless you understand how these policies work, you will miss this. You need to add the action s3:ListBucket
. This sounds easy enough but what gets most people is that this works on a different resource than the s3:GetObject
so simply adding the action will not work.
To fix this, update your policy to:
{ "Version": "2008-10-17", "Statement": [ { "Sid": "AllowCloudFrontOriginAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ORIGIN_ACCESS_ID" }, "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::BUCKET_NAME", "arn:aws:s3:::BUCKET_NAME/*" ] } ] }
This policy now works for the SPA and allows missing object to work as expected.
By now, you should have a fully functioning SPA on AWS with S3 and CloudFront. This will keep your bucket private and ensuring requests are only made through CloudFront.