Active Storage(プロキシモード) + S3 + CloudFront でファイル配信する
はじめに
現在携わっているプロジェクトで、ActiveStorageでS3にアップロードしたファイルをCDNを利用して配信するタスクを担当することになりましたが、同じようなタスクを処理した経験がなかったため、私用のAWSアカウントでS3とCloudFrontを用意して試しに実装してみました。
この記事では、Active Storage(プロキシモード) + S3 + CloudFront でファイル配信を実現した方法や、学びになったと感じたことをまとめました。
「自分なりにこう理解している」という内容を記載していますが、記載されている内容に誤りが含まれている可能性がありますので、「この部分おかしいんじゃ?」「ここはもっとこうした方がいいのに」等のご指摘やご質問がございましたら、是非コメント頂けますと幸いです🙏
CDNとは?なぜ必要なのか?
CDNとは、Content Delivery Network(コンテンツ・デリバリー・ネットワーク)の略で、世界中の複数の地点に配置される「エッジサーバー」と呼ばれる代理サーバーが、「オリジンサーバー(=大元のサーバー)」からのコンテンツをキャッシュし、その場所に近いユーザーに提供するように設計された、世界中に分散したサーバーのネットワークです。
CDNの目的は、遠くのサーバーからリソースを取得することによって発生するネットワーク遅延を減らし、パフォーマンスとユーザー体験を向上させることです。
CDNサービスにも様々な種類があるようですが、今回は、Amazon CloudFrontを利用してファイル配信する方法を書いていきます。
Active Storageによるファイル配信
Active Storageは「リダイレクト」と「プロキシ」という2種類のファイル配信をサポートしています。
リダイレクトモード
下の図は、Active Storageがリダイレクトモードでファイルをユーザーに配信する場合のシーケンス図で、処理の流れは下記の通りです。
- ユーザーのブラウザがアプリケーションサーバーにファイルのリクエストを送信します。
- アプリケーションサーバーは、Amazon S3の特定のファイルへの署名付きURLを生成します。このURLは一時的に有効で、アクセス制御を提供します。
- アプリケーションサーバーは、ユーザーのブラウザにHTTP 302リダイレクトで署名付きURLを返します。
- ユーザーのブラウザは、署名付きURLを使用して直接Amazon S3からファイルを取得します。
- Amazon S3は、ファイルデータをユーザーのブラウザに返します。
リダイレクトモードを導入することで、リソースリンクをレスポンスで直接公開しないようにできます。
このモードは、実際のリソース(今回はS3バケット)へのリダイレクトに続くURLをクライアント(ブラウザ)に提示することで機能します。
ただ、リダイレクトリンクには有効期限(5分)があり、cache-control: privateとなっているため、このままの設定だとCDNは利用できなさそうだと感じました。
ビューで生成されるimgタグの例は下記の通りです。
<img src="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NiwicHVyIjoiYmxvYl9pZCJ9fQ==--122513a3fbc7b256ca79e281352176dbb5f9d0fa/%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%20.png">
ブラウザが画像をGETするために<img src=のURLをヒットすると、レスポンスはロケーション・ヘッダ付きのHTTP 302となり、クライアントはサービスのS3バケットから直接添付ファイル(この例では画像)を取得することになります。
Request URL: https://example.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NiwicHVyIjoiYmxvYl9pZCJ9fQ==--122513a3fbc7b256ca79e281352176dbb5f9d0fa/%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%20.png
Request Method: GET
Status Code: 302 Found
# レスポンスヘッダーの一部抜粋
cache-control: max-age=300, private
location: https://bucket-name.s3.region.amazonaws.com/**********...
プロキシモード
下の図は、Active StorageがプロキシモードかつCDNを利用してファイルをユーザーに配信する場合のシーケンス図で、処理の流れは下記の通りです。
- クライアントは、CDNがアプリケーションサーバー(Rails App)に要求するファイルを表すパスを指定して、CDNにリクエストを行います。
- CDNにキャッシュがある場合は、キャッシュからファイルデータを返しますが、キャッシュミスが発生した場合は、アプリケーションサーバーはS3からファイルをダウンロードしてCDNに提供します。
- Active Storageによって生成されたパスへのそれ以降のリクエストは、すべてCDNにリダイレクトされるので、ファイルはユーザーに近いエッジサーバーによって提供されます。
ビューで生成されるimgタグの例は下記の通りです。
<img src="https://example.cloudfront.net/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NiwicHVyIjoiYmxvYl9pZCJ9fQ==--122513a3fbc7b256ca79e281352176dbb5f9d0fa/%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%20.png">
ブラウザが画像をGETするために<img src=のURLをヒットすると、レスポンスはHTTP 200となり、クライアントはCDNから添付ファイル(この例では画像)を取得することになります。
初回リクエスト時にActive Storageのデフォルトのプロキシコントローラでhttp_cache_foreverが利用されているため、ブラウザやプロキシでキャッシュが無期限となるようにヘッダーが設定されています。
Request URL: https://example.cloudfront.net/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NiwicHVyIjoiYmxvYl9pZCJ9fQ==--122513a3fbc7b256ca79e281352176dbb5f9d0fa/%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%20.png
Request Method: GET
Status Code: 200 OK
# レスポンスヘッダーの一部抜粋
cache-control: max-age=3155695200, public, immutable
last-modified: Sat, 01 Jan 2011 00:00:00 GMT
via: example.cloudfront.net (CloudFront)
リダイレクト方式に対するプロキシ方式のメリットは、実際のリソースURLをユーザーから隠せることや、CDNを利用してパフォーマンスを向上させることができることなのかなと感じました。
デメリットとして、ファイルストリーミングが終了するまでアプリケーションサーバーの負荷増がありますが、CDNを利用する場合、アプリケーションを経由するのは初回リクエストのみのはずなので、そこまでデメリットでもないのかなと感じました。
違い・使い分け
結局どちらのモードを使うべきなのでしょうか?🤔
両者の違いを自分なりにまとめてみました。
項目 | リダイレクトモード | プロキシモード |
---|---|---|
仕組み | ユーザーは署名付きURLを介して、直接ストレージ(例:S3)からファイルを取得する。 | ユーザーのリクエストはアプリケーションサーバーを経由し、サーバーがストレージからファイルを取得して返す。 |
サーバー負荷 | サーバーは署名付きURLを生成するだけで、ファイルデータはストレージから直接配信される。 | ファイルデータがアプリケーションサーバーを経由するため、サーバーの負荷が増加する。 |
セキュリティ | 署名付きURLで一時的なアクセス制御が可能だが、ユーザーが直接ストレージにアクセスする。 | アプリケーションサーバーで詳細なアクセス制御や認証が可能。ユーザーはストレージへの直接アクセスがない。 |
キャッシュ制御 | 署名付きURLが一時的で一意のため、CDNでのキャッシュが難しい。(できない?) | 固定のURLを使用するため、CDNやブラウザでのキャッシュが有効に機能する。 |
上記の違いを踏まえた上で、大まかに下記の通りに使い分けたらいいのかなと思いました。
リダイレクトモードを選択した方がいい場合
- CDNの利用予定がなく、サーバー負荷を抑えたい場合
- キャッシュの必要性が低い場合
- ユーザーごとにファイルへのアクセス制御が不要な場合
プロキシモードを選択した方がいい場合
- アプリケーションで認証や権限チェックを行いたい場合
- ブラウザキャッシュやCDNを活用してパフォーマンスを向上させたい場合
- ユーザーにストレージへの直接アクセスをさせたくない場合
今回はプロキシモードでファイル配信する方法を書いていきます!
S3バケットの作成
Amazon S3(Simple Storage Service)は、データをインターネット経由で保存・取得できるストレージサービスです。
S3バケットを作成することで、画像、動画、ドキュメントなどのファイルを保存し、ウェブアプリケーションと連携して利用できます。
この章では、Active StorageでアップロードするコンテンツをS3に保存するために、AWSでS3バケットを作成する方法を書いていきます。
まず、AWSマネジメントコンソールにログインし、サービス一覧から「S3」を選択して、S3ダッシュボードを表示します。S3ダッシュボードの右上にある「バケットを作成」をクリックすると、下記画面に遷移します。
上記画面でバケット名を入力し、下へスクロールします。
セキュリティの観点から、バケットには認証されたリクエストでのみアクセスできるようにした方が良いと思いますので、「パブリックアクセスをすべてブロック」にチェックが入ったままにして、バケットを作成ボタンをクリックして完了です!
今回触れた設定項目以外の詳細が気になる方は、S3の公式ドキュメントをご参照ください。
CloudFrontディストリビューションの作成
Amazon CloudFrontは、CDN(コンテンツデリバリネットワーク)サービスの一つで、アプリケーションのコンテンツ(画像、動画など)を世界中のユーザーに配信するためのサービスです。
サービスを利用するためには、CloudFrontディストリビューションを作成する必要がありますので、ここからはその方法を説明していきます。
AWSマネジメントコンソールにログインし、サービス一覧から「CloudFront」を選択して、CloudFrontダッシュボードを表示します。CloudFrontダッシュボードの右上にある「CloudFrontディストリビューションを作成」をクリックすると、下記画面に遷移します。
Active Storageのプロキシモードでファイル配信するには、アプリケーションのドメインをオリジンとしてCloudFrontディストリビューションを作成する必要があります。
Origin domain欄に、先ほど作成したS3バケットではなく、自分のアプリケーションのドメインを入力してください。
続いて、下記のように、CloudFrontからアプリケーションサーバーへのリクエストに含まれるカスタムヘッダーを設定します。
続いて、キャッシュキーとオリジンリクエストを下記のように設定します。
最後に、ディストリビューションを作成ボタンをクリックして完了です!
今回触れた設定項目以外の詳細が気になる方は、CloudFrontの公式ドキュメントをご参照ください。
Railsアプリケーションの実装
本番環境でS3にファイルをアップロードするための設定は済んでいるとします。
(私は、Ruby on Rails チュートリアル 第13章の「マイクロポストの画像投稿」関連部分、Railsガイド等を参考に設定しました。)
ファイル配信のために生成するURLで、アプリのホストではなくCDNのホストが使われるようにする方法は複数あるようですが、今回はRailsガイドに例示されているように、アプリのconfig/routes.rbファイルを調整する方法で実装していきます。
config/routes.rbファイルに下記コードを追加します。
# config/routes.rb
direct :cdn_image do |model, options|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
if model.respond_to?(:signed_id)
route_for(
:rails_service_blob_proxy,
model.signed_id(expires_in: expires_in),
model.filename,
options.merge(host: ENV["CDN_HOST"])
)
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(host: ENV["CDN_HOST"])
)
end
end
上記のコードが何をしているのか、いくつかの部分に分けて見ていきます。
direct :cdn_image do |model, options|
- directはRailsのURLヘルパーをカスタマイズするためのメソッドです。今回の例では、cdn_imageという名前のカスタムURLヘルパーを作成しています。
-
model
には、ActiveStorage::Blob
のインスタンス、もしくは、ActiveStorage::Variant
のインスタンスが格納されます。 -
options
は、ユーザーが指定する追加のオプション(例:有効期限のカスタマイズなど)です。
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
-
expires_in
にURLの有効期限を指定するオプションを代入しています。 -
options[:expires_in]
の値がある場合はそれを利用し、値がない場合はデフォルト値としてActiveStorage.urls_expire_in
を使用します。
if model.respond_to?(:signed_id)
model
がsigned_id
というメソッドを持っているかどうかを確認します。signed_id
はActiveStorage::Blob
オブジェクトのメソッドなので、model
がActiveStorage::Blob
のインスタンスの場合、下記コードでURLが生成されます。
route_for(
:rails_service_blob_proxy,
model.signed_id(expires_in: expires_in),
model.filename,
options.merge(host: ENV["CDN_HOST"])
)
model
がsigned_id
というメソッドを持っていない、つまり、model
がActiveStorage::Variant
のインスタンスの場合、下記コードでURLが生成されます。
route_for(
:rails_blob_representation_proxy,
signed_blob_id,
variation_key,
filename,
options.merge(host: ENV["CDN_HOST"])
)
あとは、本番環境の環境変数(CDN_HOST
)を設定して、ビューに下記のようなコードを書けば、CDN経由でファイルを配信するためのURLが生成されているはずです🎉
# ActiveStorage::Blobオブジェクトの場合
<%= image_tag cdn_image_url(user.image) if user.image.attached? %>
# ActiveStorage::Variantオブジェクトの場合
<%= image_tag cdn_image_url(user.image.variant(resize_to_limit: [128, 128])) if user.image.attached? %>
model
がActiveStorage::Blob
だった場合、実際に生成されるURLの例は下記の通りです。
https://example.cloudfront.net/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NiwicHVyIjoiYmxvYl9pZCJ9fQ==--122513a3fbc7b256ca79e281352176dbb5f9d0fa/%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%20.png
まとめ
今回の取り組みを通じて、ActiveStorageのプロキシモードを利用し、S3とCloudFrontを組み合わせてファイル配信を行う方法を学びました。
プロキシモードを活用することで、アプリケーションサーバーでの認証やアクセス制御を行いつつ、CDNやブラウザキャッシュを活用してパフォーマンスを向上させることができることがわかりました。
初めての試みで手探りの部分も多くありましたが、実際に手を動かしてみることで、少しずつですが理解を深めることができたと感じています。
この記事が、私と同じようなレベル感の方の参考になれば幸いです!
最後までご覧いただき、ありがとうございました🙇♂️
Discussion