eviry tech & service blog

「株式会社エビリー」の社員ブログです。弊社では、クラウド型動画配信サービス「millvi」、ソーシャル動画データ及び分析サービス「kamui tracker」、YouTube総合メディア「かむなび」を開発・提供しています。https://eviry.com/

CloudFront + Lambda@Edge で IP 制限をかける

こんにちは。 株式会社エビリーmillvi 開発チームでエンジニアをしております。 井上と申します。

今回は MediaStore に対して CloudFront でキャッシュする構成において、 Lambda@Edge でアクセス元の IP アドレスによるフィルタリングを実現する手順を記事に残したいと思います。

よろしければおつきあいくださいませ。

注意

本記事は 2020年8月14日 時点の情報です。 ご覧になられた時点で UI が変更されている可能性がありますので、その点ご注意ください。

環境

サービス 概要
macOS 10.15.x
Elemental MediaLive あらゆるデバイスへのブロードキャストおよびストリーミング向けにライブ動画をエンコードする
Elemental MediaStore ライブストリーミングによるメディアワークフロー向けにビデオアセットを保存、配信する
CloudFront 高速で安全性が高くプログラム可能なコンテンツ配信ネットワーク (CDN、content delivery network)
Lambda サーバーについて検討することなくコードを実行できる
Lambda@Edge ユーザーに近いロケーションでコードを実行
OBS AWS Lambda の使用開始ビデオ録画と生放送用の無料でオープンソースのソフトウェア。

想定する構成 

MediaLive + MediaStore のプロダクトを作りたい。 で、CloudFront でキャッシュし、かつ Lambda@Edge でアクセス元の IP アドレスによるフィルタリングも行いたい。

というのをイメージしたのが以下の図。

Lambda-01.png

フィルタリング

ホワイトリスト方式を採用する。指定した IP アドレスのみ許可し、リストにない IP アドレスからのアクセスはエラーとする。

制限事項

ホワイトリストの持ち方

本記事ではホワイトリストは Lambda 関数で保持する方法を示す。 ホワイトリストを DB や S3 等で保持する方法について、本記事では扱わない。

トリガーとするイベント

ビューワーリクエスト

開発ガイド から抜粋。

CloudFront がビューワーからのリクエストを受け取ると、リクエストされたオブジェクトが CloudFront キャッシュ内にあるかどうかを確認する前に、関数が実行されます。

注意事項

こちらも 開発ガイド から抜粋。

CloudFront イベントによって Lambda 関数の実行がトリガーされると、その関数が終了するまで CloudFront は続行できません。たとえば、CloudFront ビューワーリクエストイベントによって Lambda 関数がトリガーされた場合、Lambda 関数の実行が終了するまでは、CloudFront はビューワーにレスポンスを返したり、オリジンにリクエストを転送したりしません。つまり、Lambda 関数をトリガーするリクエストごとにリクエストのレイテンシーが長くなるため、関数をできるだけ速く実行する必要があります。

Lambda@Edge の制限

公式のこちら から転載。 これによると Lambda@Edge では通常の Lambda に比べて設定項目に対する制限が厳しくなっているとのこと。

イベントタイプによって異なるクォータ
エンティティ オリジンのリクエストおよびレスポンスイベントのクォータ ビューワーのリクエストおよびレスポンスイベントのクォータ
関数のメモリサイズ Lambda のクォータと同じ 128 MB
関数タイムアウト。関数は AWS リージョンの Amazon S3 バケット、DynamoDB テーブル、Amazon EC2 インスタンスなどのリソースに対してネットワーク呼び出しを実行できます。 30 秒 5 秒
ヘッダーと本文を含む、Lambda 関数によって生成されたレスポンスのサイズ 1 MB 40 KB
Lambda 関数および組み込みライブラリの最大圧縮サイズ 50 MB 1 MB

Lambda@Edge 関数での IP 制限

Lambda 関数を作成する

注意事項

Lambda 関数を作成するリージョン

CloudFront のリージョンは現状だとグローバルしか選択できない。それに関係してか、Lambda@Edge 用の関数は バージニア北部 のリージョンで作成する必要がある。(そうしないとトリガーに CloudFront を指定できない )

実行時のリージョンと出力されるログ

作成した関数は 世界中のAWSローケーション( リージョン )にレプリケートされる。 で、Lambda@Edge が実行されるとき、実際に実行されるのはクライアントから最寄りのリージョンにレプリケートされた関数 となる。 実行時のログは 実行されたリージョンの CloudWatch Logs に出力される。

参考: Lambda@Edge 関数の作成と使用の開始

手順

  1. リージョンに バージニア北部 を選択する スクリーンショット 2020-06-21 9.01.40.png

  2. Lambda > 関数 より「関数の作成」をクリック スクリーンショット 2020-05-25 11.29.25.png

  3. 「一から作成」で「基本的な情報」を埋めていく

    1. 関数名: 実行する処理に応じた関数名を入力
    2. ランタイム: デフォルトのまま(今回は Node.js 12.x)
    3. アクセス権限
      • 「実行ロールの選択または作成」で「基本的な Lambda アクセス権限で新しいロールを作成」を選択
    4. 「関数の作成」をクリック スクリーンショット 2020-05-25 12.07.36.png スクリーンショット 2020-05-25 12.07.47.png
  4. Lambda 関数の設定画面が表示される(これで Lambda 関数そのものは作成できた) スクリーンショット 2020-05-25 15.42.09.png

  5. 一旦完了

IAM ロールの設定

CloudFront へのアクセスに対して Lambda@Edge を適用する場合、Lambda@Edge 用の IAM ロール が必要なので、作成したロールに設定を追加する。

アクセス権限の設定

  1. IAM > ロール から「Lambda 関数を作成する」で作成したロールを選択 スクリーンショット 2020-05-25 12.27.16.png

  2. 表示された概要の「アクセス権限」タブからポリシーを選択し「ポリシーの編集」をクリック スクリーンショット 2020-05-25 12.30.28.png スクリーンショット 2020-05-25 12.32.44.png

  3. 編集画面が表示されるので「JSON」タブをクリック スクリーンショット 2020-05-25 12.33.19.png

  4. JSONを編集

    次の内容を設定する。

   {
       "Version": "2012-10-17",
       "Statement": [
           {
               "Sid": "VisualEditor0",
               "Effect": "Allow",
               "Action": [
                   "logs:CreateLogStream",
                   "iam:CreateServiceLinkedRole",
                   "lambda:GetFunction",
                   "cloudfront:UpdateDistribution",
                   "cloudfront:CreateDistribution",
                   "logs:PutLogEvents",
                   "lambda:EnableReplication*"
               ],
               "Resource": "*"
           },
           {
               "Sid": "VisualEditor1",
               "Effect": "Allow",
               "Action": "logs:CreateLogGroup",
               "Resource": "*"
           }
       ]
   }
  1. 「ポリシーの確認」をクリック スクリーンショット 2020-05-25 15.54.09.png

  2. 確認画面で設定した項目が追加されていることを確認し、「変更の保存」をクリック スクリーンショット 2020-05-25 12.39.22.png

信頼関係の設定

  1. 再度、IAM > ロール から「Lambda 関数を作成する」で作成したロールを選択

  2. 「信頼関係」のタブから「信頼関係の編集」をクリック スクリーンショット 2020-05-25 14.21.34.png

  3. JSONの編集画面になるので、Service プロパティに以下を追記

   edgelambda.amazonaws.com

Service プロパティの部分が配列ではない場合は配列に変更する。変更後の JSON が下記。

   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Effect": "Allow",
         "Principal": {
           "Service": [
               "lambda.amazonaws.com",
               "edgelambda.amazonaws.com"
           ]
         },
         "Action": "sts:AssumeRole"
       }
     ]
   }

▼JSON編集後の画面 スクリーンショット 2020-05-25 14.30.21.png

  1. 設定した項目が追加されていることを確認し、「信頼ポリシーの更新」をクリック スクリーンショット 2020-05-25 14.31.07.png

Lambda に戻って

IAM ロールの設定が完了したので、もう一度 Lambda に戻って Lambda 関数の実装を行う。

Lambda 関数の処理を実装する

  1. Lambda 関数を作成する」で作成した関数を選択〜表示し「関数コード」を編集する IPアドレスのホワイトリスト、並びにフィルタリング処理の例を以下に示す。
   'use strict'
   
   // IPアドレスのホワイトリスト
   const IP_WHITE_LIST = [
     'xxx.xxx.xxx.xxx', //IPアドレスは各自設定すること!
   ];
   
   const errorResponse = (httpVersion) => {
     const body =
       '<!DOCTYPE html>\n' +
       '<html>\n' +
       '<head><title>Lambda@Edge からのエラー</title></head>\n' +
       '<body>\n' +
       '許可されていない IP アドレスです\n' +
       '</body>\n' +
       '</html>'
   
     return  {
       status: '403',
       statusDescription: 'Forbidden',
       httpVersion: httpVersion,
       body: body,
       headers: {
         'cache-control': [{
           key: 'Cache-Control',
           value: 'max-age=100'
         }],
         'content-type': [{
           key: 'Content-Type',
           value: 'text/html; charset=utf-8'
         }],
       },
     };
   };
   
   exports.handler = (event, context, callback) => {
     const request = event.Records[0].cf.request;
     const httpVersion = request.httpVersion;
     const clientIp = request.clientIp; // リクエスト情報からアクセス元のIPアドレスを取得できる
     const isPermittedIp = IP_WHITE_LIST.includes(clientIp);
   
     if (isPermittedIp) {
       // 許可されているIPアドレスなので何もしない( 通常処理へ流れる )
       callback(null, request);
     } else {
       // 許可されていない IP アドレスなのでエラーを返す
       callback(null, errorResponse(httpVersion));
     }
   }
  1. 「保存」をクリックして編集内容を保存する

Lambda関数を Lambda@Edge として CloudFront に紐付ける

Lambda 関数の実装が終わったら、次は下記の手順で Lambda@Edge として CloudFront と紐付けを行う。

  1. 「アクション」から「新しいバージョンを発行」を選択 スクリーンショット 2020-05-25 14.03.52.png

  2. モーダル上で「発行」をクリック(バージョンの説明はなくてもOK) スクリーンショット 2020-05-25 14.04.03.png

  3. 「トリガーを追加」をクリック スクリーンショット 2020-05-25 14.04.25.png

  4. 編集画面で「CloudFront」を選択

    このとき Lmabda関数を バージニア北部 のリージョンで作成していないと CloudFront が選択肢に出てこないので注意。

    スクリーンショット 2020-05-25 14.04.40.png

  5. 「CloudFront トリガーの設定」を編集

    1. 「ディストリビューション」に紐付ける CloudFront のディストリビューションを設定
    2. 「キャッシュ動作」はいじらない
    3. 「CloudFront イベント」には「ビューアーリクエスト」を選択
    4. 「ボディを含める」にチェック
    5. 「関数のこのバージョンが〜」にチェック

    スクリーンショット 2020-05-25 14.05.18.png

  6. 「追加」をクリック スクリーンショット 2020-05-25 14.05.28.png

  7. トリガーに CloudFront が追加されていることを確認 スクリーンショット 2020-05-25 15.04.27.png

動作確認

ホワイトリスに登録されていないIPアドレスからのアクセス

Lambda 関数で設定したエラーメッセージが表示されていることが確認できる。 スクリーンショット 2020-06-21 10.11.25.png

まとめ

  • IPアドレスのフィルタリングはホワイトリスト方式とする
  • Lambda 関数での視聴者の IP アドレス取得方法
  exports.handler = (event, context, callback) => {
    // 省略  
    const clientIp = request.clientIp; // リクエスト情報からアクセス元のIPアドレスを取得できる
    // 省略
  }
  • Lambda@Edge 用の IAM ロール が必要
  • Lambda@Edge 用の関数は バージニア北部 のリージョンで作成する必要がある( そうしないとトリガーに CloudFront を指定できない )
  • Lambda@Edge で作成した関数は 世界中のAWSローケーション( リージョン )にレプリケートされる
  • 実際に実行されるのはクライアントから最寄りのリージョンにレプリケートされた関数
  • 実行時のログは 実行されたリージョンの CloudWatch Logs に出力される。
  • CloudFront のイベントには ビューワーリクエスト を指定する

参考

公式

その他

注意書き

本記事は Qiita でも公開 しております。