How to use CloudFront with static websites and SPAs

Last update

cover image

TABLE OF CONTENTS
  • Is CloudFront worth it?
  • The root of the error is routing
  • S3 bucket as a hosting
  • CloudFront distribution with S3 as a origin
  • Error page as a redirection to 200. Please don't do that!
  • CloudFront url rewriting function
  • CloudFront origin with S3 hosting endpoint
  • Summary and comparison

Is CloudFront worth it?

 

The short answer is YES. CloudFront is a Content Delivery Network service with extra features like:

  • multiple origins for any distribution (behaviors)
  • CloudFront functions
  • Lambda edge functions
  • POST, PUT, PATCH and DELETE requests availability
  • custom URLs
  • private content
     

On top of that CloudFront uses AWS private networks omitting often slow and congested public routes. It will serve both static and dynamic content from the nearest edge location drastically reducing load time. 

You could use any kind of host to work with CloudFront but as always those from AWS family benefits in terms of functionality. AWS officially support the following possible origins for CloudFront:
 

  • S3 (Simple Storage Service)
  • EC2 (Elastic Compute Cloud)
  • ELB (Elastic Load Balancer)
  • Lambda function
  • Elementat MediaStore container or MediaPackage endpoint
     


How well does it work? Just take a look at a blowstack.com website performance measured after migrating to CloudFront. It managed to score 100% for desktops (~25% gain) and 80% for mobiles (~20% gain) in the page speed benchmark.

BlowStack desktop version page speed results

 

However, using CloudFront with static websites or SPAs made in Vue, React, Angular or any other modern JavaScript framework is not as seamless as you would expect.

 

The root of the error is routing

 

Your app should work fine on S3 hosting but on CloudFront you will encounter an error related to routing. The error will actually look like access denied rather than 404 error but let's not get ahead.

Many javascript frameworks / libraries use similar routing solutions. At least most of them internally rewrite urls, omitting file extensions in order to serve better looking urls. In other words: it's a lot nicer to see urls that don't end with .html.

This can be problematic when you expect CloudFront to serve your static website or SPA the same way as NPM, Yarn or web servers do. S3 will handle by default urls rewrites in hosting mode but Cloudfront does not.

To better understand this let's jump into a case when you have already generated a static app and eventually want to serve it through CloudFront.
 

S3 bucket as a hosting

 

CloudFront requires a source it can cache content from. S3 will work with static websites and SPAs efficiently and cheap as a source so let's just create a new bucket. Notice that only buckets in the N. Virginia region can get free certificates generated by AWS. So if you plan to use SSL with your custom domain and Cloudfront then that's the region you should pick to minimize costs.

AWS S3 new bucket creation in N Virginia.



Create a new bucket (which I called blowstack-example-app), upload the website files and switch S3 to hosting mode. Setup this at the bottom in the Properties Tab. Configuration is easy, fill index and error document properties (those are usually named index.html and 404.html respectively). 

 

AWS enables static website hosting.



Very often static websites are built into a folder named dist. If you dump the whole folder rather it's files to the bucket then S3 hosting won't work by default.

This happens because index and error documents can't be specified with slashes in static website hosting configuration (both index and error files have to be in the root directory of the S3 bucket).

In the next post I will show you an interesting strategy related to S3 where putting things in the dist folder makes a lot of sense and how to properly configure that.

If you did everything well then your static website url should be visible on the Properties tab.
 

AWS S3 hosting endpoint.


However after visiting the url you will find out that website is not accessible and An Error Occurred While Attempting to Retrieve a Custom Error Document with AccessDenied code will pop out.
 

AWS S3 bucket access denied.


In order to fix this issue make the bucket public. First in the S3 Permissions tab unblock all public access.
 

AWS bucket all public access.


Your visitors won't see the contents of the bucket yet though. You have to additionally edit bucket policy and explicitly allow anyone to getObject from your S3 bucket (which simply means allow reading any bucket files by anyone). Keep in mind to make a slash and asterisk after the bucket arn address in the Resource property. 
 

{ 
	"Version": "2012-10-17",
	"Statement": [
	 	{ 
	 		"Sid": "PublicReadGetObject",
	 		"Effect": "Allow",
	 		"Principal": "*",
	 		"Action": [
	 			"s3:GetObject"
	 		 ],
	 		 "Resource": [
	 		 	"arn:aws:s3:::blowstack-virtual-website/*"
	 		  ]
	 	 }
	]
}


If you made everything right your static website should be visible from the url at the moment. You can proceed to the CloudFront panel.

 

CloudFront distribution with S3 as a origin

 

Go to the CloudFront panel and start creating a new distribution. As an origin, choose the S3 bucket with your app files.

AWS CloudFront choosing S3 as origin.

 

 

In bucket access, I recommend choosing the “Yes use OAI (bucket can restrict access to only CloudFront)” option and then creating a new OAI. This is a super useful feature that guarantees that the content will be served only through CloudFront and not S3 as well. If you decide to block the access on S3 then remember to tick the updating bucket policy to handle OAI as shown on the image below.

 

AWS CloudFront enabling OAI.

 

You can leave the rest options as they are and hit the create distribution button. You will be redirected to the distribution detail on the General tab. Notice Last modified field value which is also available in distributions list (one step back in navigation). It takes a while every time you make a change to update your distribution globally so don't rush with instant testing and frequently refresh distribution details to see if the latest changes are included.

 

AWS CloudFront deploying.

 

When the last modified value changes (red marker) to a specific date then you can try to reach your distribution by hitting the url from the general tab (Distribution domain name - green marker).

 

Error page as a redirection to 200. Please don't do that!

 

When you hit the distribution url for the first time you will get an error due to a specific routing behavior of CloudFront.

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>1PAV3XZGC5DWXWWT</RequestId>
<HostId>6m5XjbDoONbvRhzcexjJWmg5zSrAjp5UE0ngef9iXrY4XfkNK7tWkW3T6nmrFptBhSHhTdjUD1g=</HostId>
</Error>

 

Because access denied simply means 403 error code you could rewrite from 403 to 200. But please don't do that. I warn you because it's often used technique which will unfortunately break your app one way or another.

To make the auto code rewrite you would have to go to the error page tab in the distribution detail and create a new redirection when 403 occurs. Take a look at the specific configuration on the image below.

 

AWS CloudFront custom error page panel.

 

When you try the distribution url one again your app should work at the first sight. But then try to refresh the page with your app opened in the browser, it will crash. The same crash will happen if you try to open any nested url of your app directly providing the url (going from the app menus will not make your app crash though). 

As you see, redirecting from 403 to 200 responses with a specific path file does the job only partially and rather shouldn't be applied. Moreover your app should have 403 reserved for different purposes. 

But why did such redirecting work even partially in the first place? It worked because basically 403 just triggers redirection to /index.html file but it only works for root url (this is well described by AWS How default root object works). As I mentioned earlier, modern javascript frameworks redirect internally urls to omit file extensions. Here we go, that's the problem CloudFront is not going to solve by itself.

 

CloudFront url rewriting function

 

In order to make CloudFront work when:

  • user visit your app directly entering nested url
  • user refreshing the page in the browser
  • you want to keep origin and distribution (OAI) separate
     

A simple routing function that mimics js framework routing will do the job. Go to the functions of the Cloudfront (not the distribution, look in the left side hamburger menu).

 

AWS CloudFront main menu.

 

 

Then create a new function that will rewrite internally urls omitting file extensions (code comes from AWS repo with rewriting function).

function handler(event) {
    var request = event.request;
    var uri = request.uri;

    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

 

Remember to publish the function and then add an association with the distribution cache behavior related to the app source.

 

AWS CloudFront function association modal.

 

After successful distribution redeployment your app should be fully workable.

However, if you still get glitches when refreshing (or even when just entering the website) then go to the S3 and switch off hosting mode. Afterwards make full invalidation of CloudFront distribution as shown on the image below.

 

AWS CloudFront full distribution invalidation.

 

CloudFront origin with S3 hosting endpoint

 

Alternatively you can entirely drop the idea of using CloudFront function in favor of using S3 bucket hosting url as the origin of the distribution behavior.
 

Return to the S3 bucket and switch hosting on again. This time copy the url and use it as a source in the behavior (edit or create new one).

 

Notice that S3 hosting endpoint appeared as an option after you had pasted it's content manually. To use the endpoint here remember to switch S3 bucket to hosting mode.

 

Summary and comparison

 

Because CloudFront can use any http server (S3 as a hosting included) and even impact requests and responses through custom functions there are no chance you won't be able to run a static website or a SPA on CloudFront. Time spent on initial configuration is definitely worth the boost that CloudFront will give your app (mobile included).

Because there is at least three different model ways to make CloudFront work properly with url rewrites I decided to make a neat comparison to keep everything clean and easy to digest. See results in the table below. 

 

 S3 as the originS3 hosting endpoint as the origin
(custom http host with extra features)
Custom http host as the origin
(i.e. EC2, VPS, GOE)
CostsStandard S3 and CloudFront costs for the usage.
Additional costs for CloudFront functions.
Predictable costs by default.
Standard S3 and CloudFront costs for the usage.
Less predictable costs by default.
Server + standard CloudFront costs  for the usage.
Less predictable costs by default.
Performance*Depends on network quality and distance from a particular edge location.
Lacking features-OAIOAI, Private content

*Origin will impact performance when CloudFront misses hits and needs to fetch data from the origin.

 

The table clearly shows that S3 as an origin will give you the most from CloudFront but for the higher price compared to the other options. By choosing S3 as the origin you will be forced to use CloudFront functions which cost additionally apart from the fixed costs (related to the all three solutions) for hosting and CloudFront usage. However S3 as the origin costs are better predictable from the beginning compared to the others. This is happening because S3 as the origin can utilize the OAI mechanism to access S3 thus blocking any other traffic. Custom origins can't use this option which makes them vulnerable for different kinds of issues. Of course there is a way to mimic such functionality but there is no magic button for this and it has to be implemented on the origin not CloudFront itself. Why - apart from cost perspective - is such separation important? 

There are strong reasons for blocking simultaneous access to you app from both origin and CloudFront:

  • app will be available on two different addresses (the one from S3 properties tab and the second from Cloudfront distribution options) 
  • such duality will confuse users and search bots
  • you risk domain authority dilution and SEO problems as well
  • the content on both services may differ due to the nature of caching mechanism
  • you may be forced to handle behavior of both services (i.e. they route files differently)
  • total costs may be higher than using just one service
  • costs will be less predictable

 

The last very important difference this time between any S3 origin kind and custom http hosts is that the latter can't use private content features. Ofc there is a workaround for that too but it's not native and there are side effects attached.