NaClの田中です。
Amazon S3に格納したファイルを、X-Sendfileを使って配信する仕組みを構築しました。この記事ではその実現方法を紹介します。
X-Sendfileとは、NGINXのドキュメントによると「認証、ロギングなどをバックエンドで処理した後、内部リダイレクトされた場所からエンドユーザにコンテンツを配信するようにWebサーバが処理することで、バックエンドを解放して他の要求を処理させる仕組み」だそうです。Webサーバにコンテンツ配信をさせ、バックエンドのスループットを向上させるための機能、ということですね。
詳しい利用方法についてはドキュメントを参照ください。
さて本題です。今回やりたかったことはAmazonS3上にあるファイルの配信です。
実現方法としてまず最初に考えたのは、WebアプリケーションプログラムでAmazonS3にアクセスし、取得したファイルをHTTPレスポンスに乗せて返却という方式です。しかしこの方式だと、配信するファイルのサイズが大きい場合に、ファイル送信に時間がかかってしまいWebアプリケーション全体のスループット低下が予想されます。
そこでX-Sendfileを検討しました。ただ、ローカルファイルシステムのファイル配信のためにX-Sendfileを利用したことはあったのですが別サーバのコンテンツ配信では利用したことがありません。早速、NGINXのドキュメントをみてみると、
You can also proxy to another server.
location /protected_files {
internal;
proxy_pass http://127.0.0.2;
}
まさにこれですね。NGINXでは別サーバのコンテンツも指定できるようです(Apacheのmod_xsendfileでは出来なさそうでした)。
しかし、AmazonS3に内部リダイレクトするにはもうひとつ課題がありました。以下がAmazonS3上のファイルを取得するGET Object APIです。
GET /ObjectName HTTP/1.1
Host: BucketName.s3.amazonaws.com
Date: date
Authorization: authorization string
Authorizationヘッダが必要である、と。当然ですね、不特定多数に公開しないコンテンツだからX-Sendfileを使うわけです(誰にでも公開できるファイルならリダイレクトでいいですよね)。
では、Authorizationヘッダに指定するauthorization stringとなどういった文字列なのか。
Signing and Authenticating REST Requests - Amazon Simple Storage Service
アクセス先パスや現在日時等をもとにゴニョゴニョ計算して作る文字列のようです。これならバックエンドのWebアプリケーションプログラムで実装するのはさほど難しくなさそうです。あとは、その文字列をX-Sendfileによる内部リダイレクトの際にAuthorizationヘッダに設定できればいいわけです。もう少しNGINXのドキュメントをみてみます。
Allows redefining or appending fields to the request header passed to the proxied server.
これを使ってproxyリクエストのリクエストヘッダに設定できそうです。
keep server response header fields.
また、これを使ってレスポンスヘッダに設定した値をNGINXのlocationディレクティブの中で参照できそうです。
まとめると、
となります。さて、これで技術的な課題はクリアできました。
出来上がったコードがこちらです(バックエンドプログラムは Rails です)。
def download
access_key_id = Settings::ACCESS_KEY_ID # AWSのアクセスキーID
secret_access_key = Settings::SECRET_ACCESS_KEY # AWSのシークレットアクセスキー
access_path = '/my-bucket-name/path/to/object' # S3上のコンテンツパス
file_name = 'my-image.jpg' # ユーザに見せるファイル名
date_string = Time.now.utc.to_s(:rfc822)
sign_base = ["GET", "", "", date_string, "" + access_path].join("\n")
auth_token = OpenSSL::HMAC::digest(OpenSSL::Digest::SHA1.new, secret_access_key, sign_base)
auth_token_base64 = Base64.encode64(auth_token).strip()
# S3上のパスの先頭に/s3redirectを付加してX-Accel-Redirect
response.headers['X-Accel-Redirect'] = "/s3redirect" + access_path
response.headers['X-S3Auth-Header'] = "AWS " + access_key_id + ":" + auth_token_base64
response.headers['X-S3Date-Header'] = date_string
response.headers['X-S3File-Name'] = file_name
head nil
end
location ~* /s3redirect/(.*)$ {
internal;
# proxy先ホスト名を解決するためにDNSサーバを指定
resolver ns-356.awsdns-44.com ns-921.awsdns-51.net ns-1187.awsdns-20.org ns-1573.awsdns-04.co.uk;
set $s3_access_path $1;
set $s3_auth_header $upstream_http_x_s3auth_header;
set $s3_date_header $upstream_http_x_s3date_header;
set $s3_access_file $upstream_http_x_s3file_name;
set $download_url https://s3-ap-northeast-1.amazonaws.com/$s3_access_path?$args;
## set request header
proxy_http_version 1.1;
proxy_set_header Date $s3_date_header;
proxy_set_header Authorization $s3_auth_header;
## set response header
proxy_hide_header Content-Disposition;
add_header Content-Disposition 'attachment; filename="$s3_access_file"';
# Do not touch local disks when proxying
# content to clients
proxy_max_temp_file_size 0;
# Download the file and send it to client
proxy_pass $download_url;
}
以上、Amazon S3に格納したファイルのX-Sendfileを使った配信を紹介しました。
実はちょっとググればAmazonS3のファイル配信については同様の事例が出てくるものなのですが(汗)、これを応用すれば様々なリソース(AmazonS3に限らず)に対する内部リダイレクトが実現できそうですね。