Putting a CDN in front of ActiveStorage

I am using DigitalOcean Spaces to store images uploaded via ActionText/ActiveStorage. This works great. I have now created a CDN (via DigitalOcean) which I’d like to put in front of my images.

My goal is for links in my ActionText to use the CDN (e.g. https://cdn.myapp.com/image-goes-here.png) rather than /rails/representations/etc links which hit my app, but I’m finding it rather unclear whether this is possible. I found this comment buried on a Rails pull request:

https://github.com/rails/rails/pull/34477#issuecomment-651467261

However, what this appears to do is replace the host name in the URLs but not the path (/rails/representations is still in the path which doesn’t work for my CDN). I could take this and hack it to remove the Rails-specific paths but this feels really dirty and wrong.

Has anyone managed to do this before and is it even possible? I’d love some advice. It feels like something that should be baked into Rails at some point.

Thanks!

1 Like

I have managed to serve files uploaded through AS through CDN on Rails 5.2 and, 6.0, and 6.1. Which version are you on? Happy to provide instructions.

You could simply do this to display an image for example:

image_tag (ENV['CDN'] + talent.avatar.key)

or if it’s a variant for example:

image_tag (ENV['CDN'] + talent.avatar.variant(resize_to_fill: [260, 260], quality: "40").key)

Here ENV[‘CDN’] is an environment variable which is basically your CDN’s domain, ex:

https://yourcdn.cloudfront.net/

1 Like

How to do this for images embedded in Action Text?

1 Like

I haven’t tried this myself, but in 6.1 you can use the proxy I think. This doesn’t answer your question directly, but maybe helpful:

Put this into routes:

direct :cdn_proxy do |model, options|
    if model.respond_to?(:signed_id)
      route_for(
        :rails_service_blob_proxy,
        model.signed_id,
        model.filename,
        options.merge(host: Settings.asset_host)
      )
    else
      signed_blob_id = model.blob.signed_id
      variation_key  = model.variation.key
      filename       = model.blob.filename
      route_for(
        :rails_blob_representation_proxy,
        signed_blob_id,
        variation_key,
        filename,
        options.merge(host: Settings.asset_host)
      )
    end
  end

Put this into your environment files:

config.active_storage.resolve_model_to_route = :cdn_proxy

You can call the images as:

rails_storage_proxy_url # for the main attachments
rails_blob_representation_proxy_url # for the variants

Credit: Vito Botta

Thanks for the replies, all of which I think will work with standard ActionStorage images. However, these solutions won’t work for images attached to ActionText RichText objects as far as I can tell. As @sdubois mentions, it’s unclear how to make a CDN work with ActionText.

Any ideas welcomed!

1 Like

You should be able to customize the partial for embeds to use the proxy urls, but I haven’t tried it myself. Will post a comment once I tested it.

1 Like

For me another key question is: how to add a CDN in front of Active Storage private assets (stored in a non-public S3 bucket)?

In the past I used CloudFront signed URLs which allowed to combine security and performance.

Maybe this is somehow also possible in combination with CloudFlare?

1 Like

@sdubois believe you need to set up access policy for the S3 bucket to allow connections from CloudFront, this is pretty straightforward in aws console (they even give you a copy-paste option). This way your S3 bucket stays private but CloudFront can connect to it.

However, it should not matter these days as the way to set it up recommended by Rails guides is to use proxy mode and set your CloudFront origin as the app domain. This way, first time the asset is served by the Rails app, which will download it from S3, serve to CloudFront at /rails/active_storage/blobs/proxy/xxx and let CF cache it.

1 Like

Thank you @Janusz_M for taking the time to explain! So it should be possible to have CloudFront cache the assets, but to still ensure that CloudFront URLs at which the files are served are short-lived using CloudFront signed URLs? This would be to ensure that the generated URL provided in the source code of the website’s HTML response is only valid for, say, 5 minutes, and then won’t work any more. Then to get the file again a new authenticated request needs to be performed again (but the data would then be re-downloaded from CloudFront’s edge, to maintain performance).

I really appreciate your posts, and it helped me a lot.

I still have two questions:

  1. You mentioned image_tag(ENV['CDN']) + talent.avatar.key; well it works well, but this is permanent. If anyone gets access to the key, they can access the image from everywhere, anytime.
  2. If I use what has been provided in Rails 6, for rails_storage_proxy_url what should be passed as host here? It can’t be the same as ENV['CDN'] because that directly points to the CDN server. Shouldn’t it be the host that points to the Rails app?

So, all in all, what I see as the benefit of using cdn_proxy_url is: it won’t disclose the key to the end user. Instead, an end user will see a Rails URL, something like: /rails/active_storage/blobs/proxy/.

Could you please help me here?

Hi,

I have been through your post and it seems very complex however I will try to fix it. According to my knowledge you missing something related to the ActionText or you may need to modify your ActionText content. For which you can modify the URLs to use your CDN because in your case In your ActionText content, the URLs for images will still be generated using the /rails/representations/ path.

Using modify the URLs, you can create a custom helper method in your Rails application. For example, in app/helpers/application_helper.rb, define a method to replace these URLs-

def cdn_image_url(url)
  url.gsub('/rails/representations', '')
end

The comment you shared can help you if you are not modifying URLs-

https://github.com/rails/rails/pull/34477#issuecomment-651467261

Thanks Tyson