AWS上でとあるシステムを作っていました。そのシステムを簡単に説明すると
- 画像や動画のような静的なファイルを閲覧することができる
- ログイン機能がある
- ログインしないとその静的ファイルは閲覧することができない
といった感じのもの。ただし、静的ファイルはS3にアップロードしていて、CDN(今回はCloudFront)を用いてキャッシュしている。
システム構成
今回作成したシステム構成は以下のようになっている。
CloudFront
- 通常のリクエスト
- Route 53 → CloudFront → ALB(Application Load Balancer) → EC2
- 静的ファイルへのリクエスト(ここでは/S3/*に対してリクエストが来た場合とする)
- Route 53 → CloudFront → S3
このようなルーティングの設定は「CloudFront→Behaviors」で以下のように設定することで実現できる。
設定した項目の上が優先度が高いため、「/s3/*だったらS3へ、それ以外だったらELBへ」のようになります。
EC2
PHPで静的ファイルの閲覧システムを実装しています。
なので、今回の記事はPHPのソースコードが登場します。
S3
EC2から画像や動画などの静的ファイルのアップロードを行ったりしています。
CloudFrontからS3のファイルにアクセスしているため、S3のバケット名とディレクトリを指定すればアクセスできる形になっています。
S3のコンテンツにアクセスできるユーザを制御する
さて、ここからが今回の記事のメインです。
ログインしたユーザしか見れないというような、S3の静的ファイルにアクセス制限をつけたいのです。
1つ目の考え(最終的にはボツ案)
これを実現するために、最初に考えたのが、CloudFrontの手前で制御する方法です。
というのは、Lambda@Edgeを使用するもの。
Lambda@Edgeは、CloudFrontが発信するコンテンツをカスタマイズしてビュワーに近いAWS地域でLambda Functionを実行できるというもの。
- CloudFront がビューワーからリクエストを受信した後 (ビューワーリクエスト)
- CloudFront がリクエストをオリジンサーバーに転送する前 (オリジンリクエスト)
- CloudFront がオリジンからレスポンスを受信した後 (オリジンレスポンス)
- CloudFront がビューワーにレスポンスを転送する前 (ビューワーレスポンス)
ここでは、CloudFrontの手前で処理をしたいので、「ビューワーリクエスト」で実行するようにします。
Lambda@Edgeを使うためには、通常と同じようにLambda Functionを実装して、実装が完了したらCloudFrontのEdit Behaviorから以下のようにLambda Function Associationsのところを設定するだけです。
今回はビューワーリクエストのため、Event TypeをViewer Requestに設定します。
LamdaFunctionでの処理内容としては以下のようになります。
- /s3/*のパターンのURIのリクエスト以外の処理は何もしない
- /s3/*のパターンのURIのリクエストに対して以下の処理を行う
- ヘッダーのCookie情報を用いてEC2のリクエストを投げてログイン済みかどうかをチェックする
- ログインしている場合は、リクエストをそのまま通して、CloudFront→S3と流れて、静的ファイルを返す
- ログインしていない場合は、そこで失敗とみなし、静的ファイルにはリクエストせずエラーを返却する
このようなLambdaFunctionを実装した。
だが、ここで問題が発生する。
- すべてのリクエストに対してLambda関数を呼び出すわけで、実行回数がものすごい量になる。
- また、WebページのためS3以外のEC2インスタンス上の画像などの静的ファイルへのリクエストに対してもすべてLambdaFunctionを呼び出してしまう。もちろんfavicon.icoのリクエストも毎回呼び出してしまう。
- またS3へのリクエストに対しても、EC2のPHPのAPIを叩くことになるので、静的ファイルのリクエスト時間(具体的にはブラウザのWaiting)が長くなってしまう。
- EC2インスタンスが死んだ場合、リクエストがタイムアウトになるまで待つ恐れがあり、Lambdaの実行時間がとても長くなる
以上のことから、ユーザ体感も悪くなるし、コストも高めになってしまう恐れがあるため、この手法はやめることにした。
2つ目の考え(こちらが採用案)
次に考えたのが、CloudFrontの署名付きCookieを用いる方法である。
以下が今回参考にした公式のドキュメントのURLです。
CloudFrontに署名付きURL、または署名付きCookieが使えます。
これを簡単に説明すると、ある鍵を使用してS3にリクエストをしないとアクセスできないというもの。署名付きURLと署名付きCookieのざっくりとした違いは以下のように自分は認識している。
- 署名付きURL:クエリパラメータに鍵を付ける
- 署名付きCookie:リクエストヘッダーのCookieに鍵を付ける
署名付きURLの場合は、全てのS3へのリンクに対して、鍵を付ける必要があるため、めんどくさい。(きっとPHPのフレームワーク上の作りに影響があると思われるが、自分が今回使用したものはリンクの修正がとても大変だった)
そこで、署名付きCookieを用いることにしました。これはリクエストヘッダーのクッキー情報に付与するだけなため、SetCookieをしてあげるだけでそれ以降のリクエストはすべてCookieに鍵情報をつけてくれるというとても簡単な作り。
PHPのソースは以下のようになる。以下の処理をログイン完了処理の直後に挿入してあげることで、鍵の発行・クッキーにセットすることができる。
// S3の動画を見れるようにする $cloudFront = new Aws\CloudFront\CloudFrontClient([ 'region' => 'ap-northeast-1', 'version' => '2014-11-06' ]); // Set up parameter values for the resource $resourceKey = 'https://*'; $expires = time() + 60*60*1000; $customPolicy = <<<POLICY { "Statement": [ { "Resource": "{$resourceKey}", "Condition": { "IpAddress":{"AWS:SourceIp": "0.0.0.0/0"}, "DateLessThan": {"AWS:EpochTime": {$expires}} } } ] } POLICY; // Create a signed cookie for the resource using a custom policy $signedCookieCustomPolicy = $cloudFront->getSignedCookie([ 'policy' => $customPolicy, 'private_key' => '/tmp/private_key.pem', 'key_pair_id' => '[key_pair_idを入力する]' ]); foreach ($signedCookieCustomPolicy as $name => $value) { setcookie($name, $value, 0, "/"); }
「https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/service_cloudfront-signed-url.html」の「プライベートディストリビューションのCloudFront cookieの署名」に書いてあるソースをそのままつないだ感じになります。
リージョンはオレゴンを使用したため、ap-northeast-1になっている。ここは使用するリージョンに応じて変更する必要がある。
expireもテキトーに入力しているがいい感じに調整する必要がある。
private_keyの発行のためにはルート権限でAWSコンソールにログインする必要があるので気をつけてください。
また、IpAddressは自分の場合は不要かなと思って、最初入力しないで実行してみたら以下のようなエラーが出たので、全許可でも0.0.0.0/0のように入力する必要がありました。
<Error> <Code>MalformedPolicy</Code> <Message>Malformed Policy</Message> </Error>
プライベートキーはとりあえず/tmp/以下に置いておきましたが、消えちゃうのでちゃんとした場所に置いておきましょう。
以上のソースを埋め込めたら、後はCloudFront側の設定をします。
また、CloudFrontのEdit Behaviorを開いて、「Restrict Viewer Access (Use Signed URLs or Signed Cookies)」の項目をYesにします。
すると、ログインしないで直接S3のデータを見に行くと以下のようにアクセス制限のエラーに引っかかります。
<Error> <Code>AccessDenied</Code> <Message>Access Denied</Message> <RequestId>XXXXXXXXXXXXXX</RequestId> <HostId> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= </HostId> </Error>
そして、ログインしてみるとちゃんとアクセスできるようになります!
このようにして、S3に上がっている静的ファイルをログインしないと見れないようにすることができました。