NaClの田中です。

Amazon S3に格納したファイルを、X-Sendfileを使って配信する仕組みを構築しました。この記事ではその実現方法を紹介します。

X-Sendfileとは?

X-Sendfileとは、NGINXのドキュメントによると「認証、ロギングなどをバックエンドで処理した後、内部リダイレクトされた場所からエンドユーザにコンテンツを配信するようにWebサーバが処理することで、バックエンドを解放して他の要求を処理させる仕組み」だそうです。Webサーバにコンテンツ配信をさせ、バックエンドのスループットを向上させるための機能、ということですね。
詳しい利用方法についてはドキュメントを参照ください。

AmazonS3上のファイル配信

さて本題です。今回やりたかったことは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のドキュメントをみてみます。

proxy_set_header

Allows redefining or appending fields to the request header passed to the proxied server.

これを使ってproxyリクエストのリクエストヘッダに設定できそうです。

upstream_http_*

keep server response header fields.

また、これを使ってレスポンスヘッダに設定した値をNGINXのlocationディレクティブの中で参照できそうです。 まとめると、

  • NGINXのX-Sendfileでは外部URL(すなわちAmazonS3のREST API)へのリダイレクトが可能
  • 外部URLへリダイレクトする際に任意のリクエストヘッダを設定可能
  • Webアプリケーションで作成した任意の値を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

NGINX設定

  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に限らず)に対する内部リダイレクトが実現できそうですね。