タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

Rails 6.1 の rails_storage_proxy_url でActiveStorage のリダイレクトURL問題を解決する

f:id:uzuki05:20151102155000j:plain

Rails 6.1 の新機能 rails_storage_proxy_url を使うと、ActiveStorage で添付したファイルへのリンクが署名付きURLへのリダイレクトにならず、RailsアプリのURLのままファイルをダウンロードできるようになります。

どういうこと?

ActiveStorageはこれまで、S3をバックエンドとして使った場合、S3への署名付きURL=タイムスタンプなどが付与されたURLへのリダイレクトを行ってきました。 しかしこれは扱いづらいことも少なくなく、悩みの種の1つでした。

Rails 6.1 でこの問題に対する回答が(ようやく)公式に用意されたことになります。

例

  1. url_for(user.photo) でActiveStorageへのURLを生成
  2. たとえば http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZDg9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--d248c202e2e205d7cff8385cceaa598ea0fe244f/photo.png のようなの
  3. 1で生成されたURLにアクセスすると、S3の署名付きURL(期限付き)にリダイレクト
  4. たとえば https://myapp-development-uploads.s3.ap-northeast-1.amazonaws.com/aots0fza2jg5yzznixa4a2eb5nwg?response-content-disposition=inline%3B%20filename%3D%22photo.png%22%3B%20filename%2A%3DUTF-8%27%27photo.png&response-content-type=image%2Fpng&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA2JJO3DN3RFKWDW7Q%2F20210120%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20210120T162827Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=645c971759d68c0c53dc3bdd2547838d9c03cdf6ade730777042f1cd69220d33
  5. S3の署名付きURLからファイルをGET

何に困るか

  • これでは前段にプロキシなどを置いてキャッシュすることができません。
    • 添付ファイルへのリクエスト毎、必ずRailsアプリが動くことになります。
    • S3からの直接ダウンロードは速くないです。
  • S3の署名付きURLからの応答はHTTPキャッシュが効きません。期限付きですから、本来キャッシュされると困るわけですので、当然と言えば当然です。
  • S3の署名付きURLの期限が切れるとアクセスできません。
    • たとえばクライアントアプリなどで署名付きURLがキャッシュされると、期限を切れると再読込しようとしてエラー・・・のようなことが、クライアントのキャッシュ実装次第で発生します。しました。

古代人の対応

古代の人はこの問題の解消のため、いろいろな工夫をしました。 たとえばS3のバケットポリシーで public read 可能にして署名を不要にした上で user.photo.service.send(:public_url, user.photo.key) みたいにして、 https://myapp-development-uploads.s3.ap-northeast-1.amazonaws.com/aots0fza2jg5yzznixa4a2eb5nwg のようなURLを得たりです。

もちろんS3のパブリックアクセスは有効にすべきではありません・・・

Rails 6.1 の rails_storage_proxy_url でこの問題への答えが出た

Rails 6.1 の Active Storage では、新たに rails_storage_proxy_url が実装されました。

これを使うと、バックエンドへのリダイレクトではなく、Railsアプリ内でバックエンドからのダウンロードを中継し、加えてHTTPキャッシュも有効にしてくれます。

<%= image_tag rails_storage_proxy_url(user.photo) %>

とやると

<img src="http://localhost:3000/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZDg9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--d248c202e2e205d7cff8385cceaa598ea0fe244f/photo.png" />

となり、このURLは ActiveStorage の Controller につながって

  def show
    http_cache_forever public: true do
      set_content_headers_from representation.image
      stream representation
    end
  end

rails/proxy_controller.rb at v6.1.0 · rails/rails · GitHub

こうなるわけです。

何が嬉しいか

署名付きURLでのリダイレクトが発生しなくなったことで、主にキャッシュが扱いやすくなります。

  • 前段にプロキシなどを置いてキャッシュすることができるようになりました。
  • HTTPキャッシュが使えるようになりました。

もちろん個人宛メッセージの添付ファイルのように、ファイルの性質によってはキャッシュできるとまずい場合もあり、そういったものについては

rails_blob_url(message.attachment) あるいは rails_representation_url(message.attachment.variant(strip: true)) のように使い分ける必要があります。

補足

url_for(attachment) で redirect ではなく proxy を使いたい!

url_for(attachment) としたときに生成されるURLは Rails 6.0 以前と同じ、リダイレクトするものです。これは互換性の観点から、自然だと思います。 しかし、そういった配慮は不要で、リダイレクトではなく新たなプロキシだけ使いたい場合もあり、都度 rails_storage_proxy_url をタイプするのも面倒です。

このような場合 config.active_storage.resolve_model_to_route で設定できるようになっています。

# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy

こうすることによって、ActiveStorageのモデル( ActiveStorage::Attachment ActiveStorage::Variant など )のルート解決には rails_storage_proxy_url が使われるようになり、単に次のように書けるようになります。

<%= url_for user.photo %>
<%= image_tag user.photo %>

proxy の URL をCDN経由のものにしたい!

rails_storage_proxy_url ではURLのホスト部はRailsサーバーのものになります。つまり、前段にキャッシュプロクシなどを用意しないと、ファイル取得の度にRailsサーバーにアクセスしてしまいます。 そうではなく、CDNを経由してRailsサーバーにアクセスし応答をキャッシュするように設定した上で、生成するURLをCDNのものにすれば、Railsサーバーにかかる負荷ははるかに小さく、応答ははるかに速くできます。

このような場合は、 config.active_storage.resolve_model_to_route に加えてダイレクトルーティング機能を利用するとうまく書けます。

Active storage add proxying by fleck · Pull Request #34477 · rails/rails · GitHub

# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :cdn_proxy
# config/routes.rb

  # (省略)

  direct :cdn_proxy do |model, options|
    cdn_options = if Rails.env.development?
        Rails.application.routes.default_url_options
      else
        {
          protocol: 'https',
          port: 443,
          host: Rails.env.production? ? "cdn.myapp.takeyuweb.co.jp" : "#{Rails.env}.cdn.myapp.takeyuweb.co.jp"
        }
      end

    if model.respond_to?(:signed_id)
      route_for(
        :rails_service_blob_proxy,
        model.signed_id,
        model.filename,
        options.merge(cdn_options)
      )
    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(cdn_options)
      )
    end
  end

このようにすれば、次のようにするだけで(開発モード以外では)CDN経由のURLになります。

<%= image_tag user.photo %>
<img src="https://cdn.myapp.takeyuweb.co.jp/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZDg9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--d248c202e2e205d7cff8385cceaa598ea0fe244f/photo.png" />

応用

CDK で CloudFront Distribution を作ってこの設定と組み合わせる方法について紹介した記事がこちらになります。

blog.takeyuweb.co.jp

参考

y-yagi.hatenablog.com

github.com

rails/CHANGELOG.md at 6-1-stable · rails/rails · GitHub

rails/activestorage at v6.1.0 · rails/rails · GitHub