S3のファイルをX-Accel-Redirectで配信する

こんにちは。
マネーフォワード クラウドBox (以下MFCBox)というサービスを開発しています、RailsエンジニアのReoです。

MFCBoxはその名の通りストレージのマイクロサービスなのですが、ファイルの配信方法においてセキュリティと処理の負担軽減を考慮した結果、NGINXの機能である X-Accel-RedirectAWSの署名バージョン 4 を利用することにしました。

 

X-Accel-Redirect

こちらが、公式ドキュメントの概要説明です。

X-accel allows for internal redirection to a location determined by a header returned from a backend.
This allows you to handle authentication, logging or whatever else you please in your backend and then have NGINX handle serving the contents from redirected location to the end user, thus freeing up the backend to handle other requests.
https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/

X-Accel-Redirectは、バックエンドサーバからのレスポンスヘッダーを用いて内部リダイレクトを可能にする仕組みです。
X-Accel-Redirectを用いることで、認証のみをバックエンドで行い、コンテンツ配信はNGINXに肩代わりさせることができるようになります。
コンテンツ配信は一般に大量のメモリを必要としバックエンドサーバの負荷を高めます。コンテンツ配信に優れたNGINXへその処理を移譲し、高速配信を実現するとともにバックエンドサーバの負荷を低減させることができるのが大きなメリットです。

MFCBoxでは

  • 認証されたユーザーにのみファイルを配信する
  • 多数の社内サービスからAPI経由でファイルを配信する

これらの要件が求められていたため、サーバ負荷を低減し高速配信を可能にするX-Accel-Redirectを採用しました。

その他には、S3ある署名付きURLという機能も利用検討しました。
署名つきURLはURLを受け取った人にファイルを限定公開することができます。ただし、MFCBoxでは見積書や請求書といった社外秘の書類を扱うため、URLが漏れたら意図しないユーザに社外秘の情報が見えてしまいます。そのためこの仕組みの採用は見送りました。

 

AWS 署名バージョン4

X-Accel-Redirectを用い、NGINXがバックエンドサーバの代わりにファイルを配信するにはもう1つ課題があります。ファイルはS3に保存されているため、NGINXはS3にアクセスしてデータを取得する必要があります。もちろん、S3へのアクセス権限が必要です。

そこで、私たちはNGINXがS3に送るHTTPリクエストに対して事前に署名をし、プロキシヘッダに署名情報を詰め込んでNGINXがS3へアクセスするようにしました。
こうすることで、NGINXに認証情報を設定しなくてもNGINXはS3へアクセス可能になります。
署名には、署名バージョン4 を用いています。

実際の処理を追っていきながら流れを確認してみましょう。

サンプルコード

signer = Aws::Sigv4::Signer.new(
  service: 's3',
  region: region,
  credentials_provider: Aws::InstanceProfileCredentials.new
)

url = "https://#{bucket_name}.s3-#{region_name}.amazonaws.com/#{key}"
signature = signer.sign_request(
  http_method: 'GET',
  url: url
)

%w[host x-amz-date x-amz-security-token x-amz-content-sha256 authorization].each do
  headers[_1] = signature.headers[_1]
end

headers['signed-url'] = url
headers['X-Accel-Redirect'] = "/files"

# AWS Ruby SDKを使用しています。
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Sigv4/Signer.html
  1. S3の認証情報を渡すことでsignatureのインスタンスを作成します。
  2. 署名したいHTTPメソッドとURLを準備し、signatureインスタンスに渡すことでそのHTTPメソッド・URLへのアクセスを可能にするための認証情報が返されます。
  3. この認証情報をレスポンスヘッダに詰めてNGINXに渡します。

認証情報に関してはsignerにAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYを引数とすることもできますが、Podが IAM ロールを引き受けるようになっているため、長期の AWS セキュリティ認証情報を定義する必要がありません。[参考]

その代わりにリクエストヘッダに一時的な認証情報を示すx-amz-security-tokenが必要になっています。[参考] 注記に記載あり

NGINXの設定は該当部分を抜粋すると、

location = /files {
  internal;

  set $x_amz_date $upstream_http_x_amz_date;
  set $x_amz_security_token $upstream_http_x_amz_security_token;
  set $x_amz_content_sha256 $upstream_http_x_amz_content_sha256;
  set $authorization $upstream_http_authorization;
  set $signed_url $upstream_http_signed_url;

  proxy_set_header x-amz-date $x_amz_date;
  proxy_set_header x-amz-security-token $x_amz_security_token;
  proxy_set_header x-amz-content-sha256 $x_amz_content_sha256;
  proxy_set_header Authorization $authorization;

  proxy_pass $signed_url;
}

このようになっており、ヘッダの値をNGINXで受け取りプロキシする際のヘッダに改めてセットしています。
なお2行目のinternalの設定をしておくと、内部リダイレクトの場合のみリクエストを受け付けるようになります。

 

まとめ

少しニッチな内容になってしまったかもしれませんが、参考になれば嬉しいです。
引き出しをたくさん持っておくことは大切ですよね(自戒をこめて

X-Accel-Redirectという技術があることはPJ発足当時は全く知らず、要件が定まってきた段階でインフラチームに話を持っていったところ、提案してもらいました。すぐにこの技術を採用すると決まったわけではなく、記事内でも触れた通りセキュリティリスクやパフォーマンス等々を相談した上での決定でした。
マネーフォワードでは少人数チームで働くことが多いですが、他チームとの知見の共有も活発に行われており、相談もとてもしやすい環境だと思っています!

 

マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket