Rails 6.1 の rails_storage_proxy_url でActiveStorage のリダイレクトURL問題を解決する
Rails 6.1 の新機能 rails_storage_proxy_url
を使うと、ActiveStorage で添付したファイルへのリンクが署名付きURLへのリダイレクトにならず、RailsアプリのURLのままファイルをダウンロードできるようになります。
どういうこと?
ActiveStorageはこれまで、S3をバックエンドとして使った場合、S3への署名付きURL=タイムスタンプなどが付与されたURLへのリダイレクトを行ってきました。 しかしこれは扱いづらいことも少なくなく、悩みの種の1つでした。
Rails 6.1 でこの問題に対する回答が(ようやく)公式に用意されたことになります。
例
url_for(user.photo)
でActiveStorageへのURLを生成- たとえば
http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZDg9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--d248c202e2e205d7cff8385cceaa598ea0fe244f/photo.png
のようなの - 1で生成されたURLにアクセスすると、S3の署名付きURL(期限付き)にリダイレクト
- たとえば
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
- S3の署名付きURLからファイルをGET
何に困るか
- これでは前段にプロキシなどを置いてキャッシュすることができません。
- 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 を作ってこの設定と組み合わせる方法について紹介した記事がこちらになります。