Jets CloudFront Uploads

For Jets and Rails ActiveStorage, Jets can create a CloudFront CDN to serve the files uploaded by Rails ActiveStorage.

How It Works

Here’s an example of how it works.

CloudFront Uploads Distribution -> Lambda Function URL -> Rails -> ActiveStorage S3

The CloudFront distribution will have a target origin of the “Lambda Function URL”, which serves your Rails app.

Interestingly, ActiveStorage can be configured for S3 or any other storage. CloudFront only knows about the Lambda Function URL.

Config

You can enable the creation of the Uploads CloudFront distribution with the following:

config/jets/deploy.rb

Jets.deploy.configure do
  config.uploads.cloudfront.enable = true
  config.uploads.cloudfront.cert.arn = acm_cert_arn(domain: "example.com", region: "us-east-1")
  # optional since a conventional alias is created
  # config.uploads.cloudfront.aliases = [
  #   "uploads-#{Jets.env}.example.com"
  # ]
  config.uploads.cloudfront.route53.enable = true
end

The CloudFront aliases config is optional since Jets create a convention CloudFront alias. It looks something like this:

demo-dev-uploads.example.com

If dns.enable = true, then a route53 record is also created that matches the domain of the cloudfront.cert.arn.

CDN For Rails ActiveStorage Uploads

Important: For the CloudFront CDN to work, you must use the ActiveStorage in rails_storage_proxy mode. Here’s one way to configure it.

config/initializers/active_storage.rb

Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy

Jets will also automatically configure a JETS_UPLOAD_HOST environment variable with the CloudFront endpoint. It will use the first value of config.uploads.cloudfront.aliases or the conventionally created alias.

So you can create a cdn_image route helper that will route to the CDN. Example:

config/route.rb

Rails.application.routes.draw do
  direct :cdn_image do |model, options|
    expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }

    route_for_options = {
      host: ENV["JETS_UPLOAD_HOST"], # automatically set when creating Jets managed uploads cloudfront distribution
      port: ENV["JETS_UPLOAD_PORT"]  # only for development
    }

    if model.respond_to?(:signed_id)
      route_for(
        :rails_service_blob_proxy,
        model.signed_id(expires_in: expires_in),
        model.filename,
        options.merge(route_for_options)
      )
    else
      signed_blob_id = model.blob.signed_id(expires_in: expires_in)
      variation_key = model.variation.key
      filename = model.blob.filename

      route_for(
        :rails_blob_representation_proxy,
        signed_blob_id,
        variation_key,
        filename,
        options.merge(route_for_options)
      )
    end
  end
end

Here’s an example of how you would use the helper in the views.

app/views/photos/show.html

<%= image_tag(@photo.image.variant(resize_to_limit: [300, 300])) %>

Rails Docs:

Tips

In general, CDNs like CloudFront cache content. This means errors may be cached if you have an error while setting your configuration. This can be confusing when debugging. Here are a few tips:

  • Try adjusting the cloudfront.default_cache_behavior.cache_policy_id = cloudfront_cache_policy_id("Managed-CachingDisabled"), for debugging. Docs: Using the managed cache policies
  • Browsers will cache images also. They simply follow the instructions for the cache headers. You can do a hard browser refresh request with Shift-Command-R. You can also use a private or incognito browser while testing.
  • You can invalidate all CloudFront caches with: aws cloudfront create-invalidation --distribution-id $DIST --paths '/*'
  • CloudFront Assets Distribution: Jets can create a CloudFront distribution and automatically configure it for assets that get precompiled to the public/assets folder by rails assets:precompile.
  • CloudFront Lambda Distribution: Jets can create a CloudFront distribution and automatically configure it in front of the deployed controller Lambda Function.
  • CloudFront Uploads Distribution: Jets can create a CloudFront distribution and automatically configure it for files like your ActiveStorage uploads.

Cache Control Header

With ActiveStorage, you can configure cache-control response header with:

config/storage.yml

amazon:
  service: S3
  upload:
    cache_control: "private, max-age=<%= 1.day.to_i %>"

Note this is meta-data added to the s3 object. So if you want to change the cache-control, you must reupload the object.

For more info see ActiveStorage Docs

Reference

The table below covers each setting. Each option is configured with config.OPTION. The config. portion is not shown for conciseness. IE: logger.level vs config.logger.level.

Name Default Description
assets.cloudfront.cert.arn nil ACM Cert ARN. Required when using assets.cloudfront.enable = true. Must be in us-east-1 since it’s for CloudFront. This helper method is useful: acm_cert_arn(domain: "example.com", region: "us-east-1")
assets.cloudfront.cert.minimum_protocol_version TLSv1.2_2021 The TLSv1.2_2021 has been the Cloudfront console default as of 12/24/23.
assets.cloudfront.cert.ssl_support_method sni-only The distribution accepts HTTPS connections from only viewers that support server name indication (SNI). This is recommended. Most browsers and clients support SNI.
assets.cloudfront.default_cache_behavior.allow_methods %w[HEAD GET] Allow methods for the distribution.
assets.cloudfront.default_cache_behavior.properties {} Default cache behavior properties to merge. Allows overriding the propertes in a general way.
assets.cloudfront.default_cache_behavior.viewer_protocol_policy redirect-to-https How CloudFront should handle http requests. The default is to redirect http to https. IE: A https upgrade.
assets.cloudfront.route53.comment “Jets managed CloudFront distribution DNS record” Route53 Record comment.
assets.cloudfront.route53.enable false Enables creation of the Route53 DNS Records that match the CloudFront aliases.
assets.cloudfront.route53.hosted_zone_id nil Route53 Hosted Zone ID. This takes higher precedence over hosted_zone_name.
assets.cloudfront.route53.hosted_zone_name nil Route53 Hosted Zone ID. Allows you to specify the config in a human-readable way. Note route53.domain also works as a convenience.
assets.cloudfront.route53.properties {} Route53 DNS record properties to merge. Allows overriding the propertes in a general way.
assets.cloudfront.route53.ttl 60 Route53 DNS TTL. This is only used when assets.cloudfront.route53.use_alias = false and a CNAME is created instead.
assets.cloudfront.route53.use_alias true Use an A Record with the “Alias” Route53 feature. This allows APEX domains to work with CloudFront distributions.
assets.cloudfront.enable false Enables CloudFront Distribution in front of the Lambda URL. See: Lambda URL CloudFront Distribution
assets.cloudfront.http_version http2 HTTP version that you want viewers to use to communicate with CloudFront.
assets.cloudfront.ipv6_enabled true Enables IPV6 also for CloudFront.
assets.cloudfront.origin.custom_origin_config { HTTPSPort: 443, OriginProtocolPolicy: “https-only” } Custom origin config.
assets.cloudfront.origin.properties {} Origin properties to merge. Allows overriding the propertes in a general way.
assets.cloudfront.origin.viewer_protocol_policy redirect-to-https How CloudFront should handle http requests. The default is to redirect http to https. IE: A https upgrade.
assets.cloudfront.price_class PriceClass_100 Price class you want to pay for CloudFront. There’s PriceClass_100, PriceClass_200, PriceClass_All. Note, since the lower price classes use less regions, they deploy faster.
assets.cloudfront.properties {} Properties to merge and override CloudFront Distribution
assets.cloudfront.spread_hosts true Whether or not to create multiple aliases with pattern assets%d.example.com to spread the requests. Related: Rails Assets
assets.enable true Enables Lambda Function URL for the Controller Lambda Function.

See Full Config Reference