Serving static assets with a CDN in Rails

By Exequiel Rozas

- November 01, 2024

If your users are located all around the world or, at least, far from where your application servers are, serving static assets through a CDN is a good idea.

User experience improves because your page loads faster by saving the network latency produced by the distance to the server.

And, by decantation, SEO improves as well. Serving your assets from a CDN improves Core Web Vitals which are a set of proxy metrics for user experience that Google uses to rank sites.

Now that you know what the benefits are, let's get to work:

How does serving assets through a CDN work

By static assets we understand every asset in our application that's not very likely to change.

They're usually the ones managed by the asset pipeline: images like logos or icons, CSS, JavaScript, self-hosted fonts and even things like our site's favicon.

We already learned how to serve Active Storage uploads with a CDN, and static assets work very similarly.

The way it works to serve assets is basically the same as it works for file uploads.

The flows are explained in the following diagrams:

Flow diagram explaining the default Rails asset delivery

The above diagram shows the default Rails asset delivery: every static asset like CSS, JavaScript or self-hosted fonts are hosted on the server and delivered by it.

If the server is far from the user requesting the assets, network latency makes the process take longer thus degrading the user experience.

Conversely, delivering assets using a CDN looks like this:

Serving assets using a CDN using Rails flow diagram

Here, the CDN's job is to request assets from the origin (our server) and cache them if not already cached and return them to the user which is, at most, 100 ms away from the CDN PoP: point of presence, the actual CDN server that delivers the asset.

For this example, there's actually points of presence in Madrid, Barcelona, and Lisbon so our user is guaranteed to be close from the PoP.

Configuring CDN asset delivery with Rails is pretty straightforward, however we have to set our CloudFront distribution in order to make everything work:

Configuring a CloudFront distribution

In order to serve our assets from a CDN, we first need to configure a distribution in the CloudFront part of the AWS Console.

Let's go through the steps:

Create an AWS account

Before anything, we need to create an AWS account. We can do so by following this link.

AWS Sign up screen

You will need to provide a valid email and an account name. After validating your email address, you will be prompted to set a password and then provide some information about you, including billing information.

After completing the registration steps you have access to the whole suite of AWS products and a 12-month period free tier.

Create a CloudFront distribution with our domain as an origin

The second step is to create a distribution, which is the set of servers that will be serving our application's assets.

Configuring the origin for our CloudFront distribution

For this step we need to choose our application's domain as an Origin domain we pick HTTPS only for the protocol because we have the config.force_ssl set to true. The HTTPS port is 443 which is the default value. We set TLSv1.2 as the minimum origin SSL protocol because our server is using TLSv1.3.

The next step is to give a name to the distribution, which is optional, and configure the headers that CloudFront will send to our application:

Distribution name and header setup

The headers we're configuring here are forwarded to the client in each request to CloudFront that results in a cache MISS:

  • Access-Control-Allow-Origin: controls which domains can access the resources. In this case we only want the resources available from our application domain.
  • Access-Control-Allow-Headers: specify which headers can be included in a request when it is sent to the sever. Origin is the domain that makes the request, Content-Type indicates the media type of the request and Accept informs our server which content types the client can handle.
  • Access-Control-Allow-Methods: specifies the HTTP methods that are allowed when accessing the resource. GET is used to retrieve the assets and OPTIONS is used in preflight requests.
  • Access-Control-Allow-Credentials: it allows CloudFront to pass along any credentials that might be included in requests from the client to the server.

After this, we configure the “Default Cache Behavior” section:

Default cache behavior section in CloudFront

We practically leave the default options except for the "Allowed HTTP methods" which we set to GET and HEAD.

We also configure CloudFront to compress objects which allows CF to compress certain files received from the origin before delivering them to the user.

There's no need for viewer restriction, at least for our actual use case.

Next up is the “Cache key and origin requests” section:

Distribution cache key and origin requests configuration

For this section, we pick the “Legacy Cache Settings” and the Response headers policy to CORS-with-preflight-SecurityHeadersPolicy.

This policy sets the Access-Control-Allow-Origins to * which is very permissive. In case you need to customize that to only allow a limited set of origins you would have to create a cache and an origin request policy.

Lastly, and if you want, you can configure the Settings section:

General settings section for a new CloudFront distribution

We can leave all the default values. However, you can pick a limited set of edge locations in case your users are generally located in a limited area.

Rails application configuration

After we configured the CloudFront part of things, we just need to configure one config variable to make everything work in production.

# config/environments/production.rb
config.asset_host = 'd123456abcdefg.cloudfront.net' # The host for your newly create distribution

And that's it. This configuration would replace our local origin with the distribution we just created, and the process explained at the beginning of the article will take place for every request.

You could use Rails credentials or environment variables to handle this configuration, but you should know that the distribution will be displayed in the DOM and Network requests anyway.

Using a custom subdomain to serve assets

Another feature that's desirable when serving assets with a CDN is using a custom subdomain like cdn.origindomain.com or assets.origindomain.com.

There are a couple of reasons we could use a custom subdomain instead of the domain provided by CloudFront to serve assets:

  • Consistency: some curious users might find out where the assets are loading from and be curious about the “weird” URL or domain they come from.
  • Versatility: we could change providers without causing breaking changes.
  • SEO Benefits: assets like images or videos might be given more weight if they come from our domain. Also, if somebody links to our assets from their domain, we get to keep the authority.
  • Vanity and trust: if we need to share an asset like a PDF with someone, the fact that it is hosted on our domain is more trustworthy. Especially for non-technical users.

Let's follow the steps to make this work:

Set an SSL certificate for your subdomain

In order to use a subdomain to deliver assets with CloudFront, or any CDN for that matter, we need to set an SSL certificate for the subdomain.

To keep it all under the AWS umbrella, we'll be requesting the certificate using AWS Certificate Manager..

The first step would be to go back to your AWS console and search for the certificate manager and access it. Once you're in there you should see something like this:

AWS Certificate Manager welcome screen

We proceed to click on the "Request a Certificate" button. After that we should see the "Request certificate" step:

AWS Certificate Manager first step

We pick the only available option which is requesting a public certificate.

For the following step, we need to pick a fully qualified domain name, which in our case would be something like cdn.origindomain.com. For the validation method we want to pick DNS validation because it's quicker and more straightforward. For the encryption algorithm we pick RSA 2048 which is the default value.

View of the domain name, validation method and encryption algorithm configuration on AWS Certificate Manager

Then we click on the Request button at the bottom right part of the screen, and we should be redirected to the view for our specific certificate. At the middle of the screen we should see something like this:

Domains section of the certificate view on AWS Certificate Manager

Here we can find the CNAME name and value, we will use those values to set up the CNAME registry in our domain registrar.

CNAME config with our domain registrar

In order for Amazon to verify our domain ownership we need to create a CNAME entry in our domain registrar.

I use Hover, so the CNAME config looks like this:

CNAME registry view on the Hover DNS panel

We then paste the name value provided by AWS in our HOSTNAME field and the value in our TARGET NAME field.

After that, we click on the Save Changes button, and then we wait until the DNS changes are propagated.

Depending on your registrar, after maybe 10–15 minutes (it can vary) when you go to the Certificates section of the AWS Certificate Management console, you should see that the status for your certificate changed from Pending validation to Issued.

After your certificate is successfully issued by AWS, we now need to add a custom domain for the CloudFront distribution we created before.

To do that, we need to go to our distribution page in the AWS console and click on the Edit button for the Settings section that's located at the middle of the screen when seeing the distribution page.

Then, on the settings page, we should add our custom subdomain and the certificate we just registered with ACM:

Adding a custom subdomain to our CloudFront distribution

The last step is to add an extra CNAME entry to associate our subdomain with the CloudFront distribution. We have to add cdn to our HOSTNAME and the distribution name to the TARGET NAME:

Setting CloudFront distribution as a value for the CDN subdomain

Now, we have to wait until the domain propagates in order to serve our static assets with a CDN and from a custom subdomain.

I hope you've enjoyed the tutorial and that you can make it work for your next awesome application!

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.