この記事は フラー株式会社 Advent Calendar 2022 11日目の記事です。
10日目は @nnsnodnb さんで Firebase App Distribution で配信するための CircleCI Orb を自作した でした。

はじめに

早いもので前回のブログから1年経ってしまいました。(去年も同じこと言ってる)
毎年のことながらブログを書く前にhugoのアップデートとCIのアップデートにばかり時間を使ってしまいました。
来年はこれだけで記事がかけるかもしれない

さて、今回もとある案件でのお話です。
今回は静的コンテンツを特定の認証されたユーザーのみに配信するという要件がありました。(よくありますね!)
かんたんにPrivateなS3バケットのコンテンツを安全に配信できる方法ないかな〜とぼんやり考えていた時にあることを思い出しました。それは2021年のISUCON予選の振り返り会をしている時に、@sora_hさんが「実は認証のみアプリケーションで行い画像の配信はnginxで行えたんですよ〜」という話をしていて「そんなことできるのか!すげぇ〜」と感動したことがあったのです。
当時はDBにバイナリーで格納されている画像ファイルをエクスポートしてnginxで配布することでDBサーバーへの負荷軽減につながるという話だったのですが、まさかこんな形で業務で使うことになるとはISUCON様様です。

今回はマネーフォワードさんのテックブログS3のファイルをX-Accel-Redirectで配信する を参考にgoで実装してみます


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. This feature is commonly known as X-Sendfile.

<翻訳>
X-accelでは、バックエンドから返されるヘッダによって決定される場所への内部リダイレクトが可能です。

これにより、バックエンドで認証、ログ、その他何でも処理し、NGINXにリダイレクトされた場所からエンドユーザーへのコンテンツを提供させ、 バックエンドを他のリクエストの処理に解放させることができるようになります。この機能は一般的にX-Sendfileとして知られています。

ということでX-Accelヘッダーを使うことで別の場所に内部的にリダイレクトさせることができるようです


今回やりたいこと

  • ユーザーリクエストの認証をアプリケーション側で実施
  • S3に格納されているファイルの配信はnginxで行う

nginxからS3へのアクセスは署名付きURLでも可能ですが、上記記事でも触れられているようにURLが外部に漏洩した場合誰でも見れる状態になってしまう為、記事と同じくAWS 署名バージョン4 で進めます。

自分の中で整理する為に改めてシーケンスを書きました

シーケンス図

やるべきことは以下の通り

  1. S3のバケット情報からAWS 署名v4 の認証情報を生成する ⑤
  2. 生成した認証情報をアプリケーションのレスポンスヘッダーにセットしてnginxに渡す ⑦
  3. X-Accel-Redirectで指定されたパスの設定 ⑧

1. AWS 署名v4 の認証情報を生成する

はじめに⑤のAWS 署名バージョン4で署名する部分です
gits/main.go AWS 署名v4の認証情報取得サンプル github.com

1.1 signerを初期化

実行するロールにS3の参照権限が必要です

main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
  	if err != nil {
  		return err
  	}
  
  	signer := v4.NewSigner(func(signer *v4.SignerOptions) {
  		signer.DisableURIPathEscaping = true
  	})
  	cred, err := cfg.Credentials.Retrieve(ctx)
  	if err != nil {
  		return err
  	}

1.2 URLへアクセスする為の認証情報を作成

シーケンス図⑤のnginxがS3へリクエストする為に必要な認証情報の作成をします
signer.SignHTTPに渡したreqに認証用のヘッダーがセットされます

main.go
53
54
55
56
57
58
59
60
61
62
63
  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, originalURL.String(), nil)
  	if err != nil {
  		return err
  	}
  	req.Header.Set("X-Amz-Expires", expireStr)
  
  	now := time.Now()
  
  	if err := signer.SignHTTP(ctx, cred, req, "UNSIGNED-PAYLOAD", "s3", cfg.Region, now); err != nil {
  		return err
  	}

これで認証情報の生成が完了しました。
このサンプルではcurlコマンドを利用して動作確認していますが実際はレスポンスヘッダーに各値を追加する必要があります。

2. 生成した認証情報をレスポンスヘッダーにセットする

例:http.Headerを受け取りレスポンスヘッダーにセットする

handler.go
  headers, err := GetSignHeader(ctx, url, http.MethodGet, sign.UnsignedPayload, "s3") // main.goの処理をメソッド化
  if err != nil {
  	return nil, err
  }
  headers.Add("signed-url", url)            // S3のURL
  headers.Add("X-Accel-Redirect", "/files") // 今回用意したnginxX-Accel-Redirect用のパス
  headers.Add("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD")
  
  for k := range headers {
  	ctx.ResponseData.Header().Set(k, headers.Get(k))
  }

3. X-Accel-Redirectで指定されたパスの設定

アプリケーションでレスポンスヘッダーに設定した値をproxy_set_headerでセットし直します

nginx.conf
  resolver 169.254.169.253 valid=5s;

  server{
  ...

  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;
  }

これでS3のファイルをX-Accel-Redirectを用いて取得することができるよになりました。

まとめ

S3のURLを元にAWS 署名v4で認証情報を生成し、nginxのX-Accel−Redirectを使うことで認証情報を外部に漏洩させずにファイルを取得できるようになりました。
S3のファイルをX-Accel-Redirectで転送するためには以下の設定が必要です

  1. nginxからS3へのファイル参照をするためにアプリケーション側でAWS 署名v4を利用し認証情報を取得する
  2. レスポンスヘッダーにX-Accel-Redirectと取得した認証情報をセットする
  3. X-Accel-Redirect先の設定でproxy_set_headerを利用してヘッダーをセットする

余談(ハマったポイント)

実装をする中でハマったことがありました。
手元のターミナルでは正しく取得できるが サーバーにデプロイしたらS3から403が返却される事象に悩まされました。
原因は手元の実行ロールは管理者権限で動いており、S3ファイルを参照できる権限があったがサーバーには参照する権限がなかったため権限エラーになりました。
AWS 署名v4の認証情報の確認は接続した時に発行したロールの権限に依存します。
したがってサーバー側にアタッチメントしてるロールにS3の参照権限を事前に設定しておく必要があります。


明日はフラー株式会社 Advent Calendar 2022 12日目 @nnsnodnb さんで  を食べる です お楽しみに〜