Rails でファイルを返さず nginx で返す X-Accel-Redirect の設定と解説の記事で、ファイルをアプリケーションサーバで返さずに nginx などの Web サーバで返す方法を紹介しました。そこではファイルの場所はサーバ内のストレージでしたが、今回の記事では AWS S3 にストアしている非公開ファイルを nginx から返すようにする方法を紹介します。

これを使うことで、S3 で非公開にしているファイルを、アプリケーションの認証情報を使ってアクセスさせることができるようになります。S3 には PreSigned URL という一時的に公開するための仕組みがありますが、URL 自体がわかれば誰でもアクセスが可能になっているので、URL そのものを隠蔽したい場合には今回のやり方が使えると思います。

rails new してからの実装差分のコード全てはこちらです。

認証トークンを生成する

S3 の非公開 URL にアクセスするにはリクエストヘッダに署名した認証トークンを付ければ OK です。署名は Version4 を利用します。署名自体は Ruby 単体でもできますが面倒くさいので AWS Ruby SDK を利用します。

require 'aws-sigv4'

url = 'https://example-bucket.s3-ap-northeast-1.amazonaws.com/hoge.txt'

signer = Aws::Sigv4::Signer.new(
  service: 's3',
  region: 'ap-northeast-1',
  access_key_id: ENV['AWS_ACCESS_KEY'],
  secret_access_key: ENV['AWS_SECREAT_ACCESS_KEY'],
)

signature = signer.sign_request(
  http_method: 'GET',
  url: url
)

signature.headers
# {
#   "host"=>"example-bucket.s3-ap-northeast-1.amazonaws.com",
#   "x-amz-date"=>"20191230T142136Z",
#   "x-amz-content-sha256"=>"...",
#   "authorization"=>"AWS4-HMAC-SHA256 Credential=..., SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=..."
# }

IAM の Role には S3 ReadOnly な権限が必要です(利用するバケット限定で指定すると良いでしょう)。

生成されたトークンを curl で利用するサンプルはこちら。

curl -H 'x-amz-date: 20191230T142136Z' \
     -H 'x-amz-content-sha256: ...' \
     -H 'Authorization: AWS4-HMAC-SHA256 Credential=..., SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=...' \
     https://example-bucket.s3-ap-northeast-1.amazonaws.com/hoge.txt

Rails の設定

認証トークンの生成方法はわかったので、nginx に伝えるために Rails でレスポンスヘッダを設定します。また、今回はファイルがリモートにあるため send_file は使わずに自前で X-Accel-Redirect を設定します。

path = '/hoge.txt'
url = "https://example-bucket.s3-ap-northeast-1.amazonaws.com#{path}"

signer = Aws::Sigv4::Signer.new(
  service: 's3',
  region: 'ap-northeast-1',
  access_key_id: ENV['AWS_ACCESS_KEY'],
  secret_access_key: ENV['AWS_SECREAT_ACCESS_KEY'],
)

signature = signer.sign_request(
  http_method: 'GET',
  url: url
)

signature.headers.each do |key, val|
  headers[key] = val
end

headers['X-Accel-Redirect'] = "/x-files#{path}"

head :ok

nginx の設定

location ~ ^/x-files/(.*) {
    internal;

    set $x_amz_date $upstream_http_x_amz_date;
    set $x_amz_content_sha256 $upstream_http_x_amz_content_sha256;
    set $authorization $upstream_http_authorization;

    # AWS 内のサーバの場合は 10.0.0.2 など
    resolver 8.8.8.8 valid=5s;
    proxy_set_header x-amz-date $x_amz_date;
    proxy_set_header x-amz-content-sha256 $x_amz_content_sha256;
    proxy_set_header Authorization $authorization;

    set $download_host https://$upstream_http_host/$1;
    proxy_pass $download_host;
}

location / {
    proxy_set_header   Host                $host;
    proxy_set_header   X-Real-IP           $remote_addr;
    proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_pass http://host.docker.internal:3000;
}

Rails から返ってきたレスポンスヘッダに必要な情報があるので、変数にセットします。そしてそれを改めて S3 への proxy_pass に設定をしてリクエストをすれば、nginx の裏で非公開 S3 オブジェクトを参照して UserAgent に返すことができるようになります。

今回は AWS S3 で行いましたが、GCP Storage でも同様のことができます。便利ですね。