こんにちは。 株式会社エビリーの millvi 開発チームでエンジニアをしております。 井上と申します。
こちらの記事 で CloudFront へのアクセスに対して Lambda@Edge でアクセス元の IP アドレスによるフィルタリングを実現する手順について扱いました。 今回は そのフィルタリングのための情報を S3 と DynamoDB で持つ方法、またその参照の仕方について記事に残したいと思います。
よろしければおつきあいくださいませ。
はじめに
こちらの記事 で IP アドレスによるフィルタリングが実現できたので、本記事ではフィルタリングに用いる IP アドレスのリストを
- S3
- DynamoDB
に持たせる方法について見ていく。
注意
本記事は 2020年8月21日 時点の情報です。 ご覧になられた時点で UI が変更されている可能性がありますので、その点ご注意ください。
前提
- こちらの記事 の構築が完了していること
環境
サービス | 概要 |
---|---|
macOS | 10.15.x |
Elemental MediaLive | あらゆるデバイスへのブロードキャストおよびストリーミング向けにライブ動画をエンコードする |
Elemental MediaStore | ライブストリーミングによるメディアワークフロー向けにビデオアセットを保存、配信する |
CloudFront | 高速で安全性が高くプログラム可能なコンテンツ配信ネットワーク (CDN、content delivery network) |
Lambda | サーバーについて検討することなくコードを実行できる |
Lambda@Edge | ユーザーに近いロケーションでコードを実行 |
S3 | どこからでもお好みの量のデータの保存と取得が簡単に行えるオブジェクトストレージ |
DynamoDB | どんな規模にも対応する高速で柔軟な NoSQL データベースサービス |
やりたいこと
- CloudFront へのアクセスを Lambda@Edge で IP フィルタリングする
- フィルタリングほホワイトリスト形式で許可するIPアドレスを管理する
- ホワイトリストは S3 / DynamoDB で管理する
S3 での実現方法
想定する構成
MediaLive + MediaStore のプロダクトを作る場合、CloudFront でキャッシュすると想定すると、以下の図になる。
S3 にホワイトリストを登録
バケットの設定
暗号化
- AES-256 を設定
アクセス権限
- ブロックパブリックアクセスをすべてブロックに設定
ホワイトリストの内容
次の JSON
ファイルを S3 のバケットにアップロード。
{ "white_list": [ "xxx.xxx.xxx.xxx" // 許可するIPアドレス ] }
Lamdba@Edge の編集
アクセス権限
- S3 へのアクセス権限をもつロールを付与
- フルアクション
- フルアクセス
Lamdba 関数
以下の実装で実現できた。(あくまでサンプル。実運用の際は精査する)
'use strict' // S3 からファイルを読み込む const aws = require('aws-sdk'); aws.config.region = 'ap-northeast-1'; const s3 = new aws.S3(); const paramsToGet = { Bucket: 'ip-white-list', Key: 'white_list.json' }; 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; // S3 からファイルを読み込む s3.getObject(paramsToGet, (err, response) => { if (err) { console.log(err); callback(err); } console.log('response: ' + JSON.stringify(response)); if (!response) { callback('response is null or undefined'); } console.log('response.body: ' + response.Body.toString("utf-8")); if (!response.Body) { callback('response.body is null or undefined'); } // ここで指定する `white_list` は S3 のバケットにアップロードされた JSON 中の key const permitIp = JSON.parse(response.Body.toString("utf-8"))["white_list"]; console.log('permitIp: ' + permitIp); const isPermittedIp = permitIp.includes(clientIp); if (isPermittedIp) { // 許可されているIPアドレスなので何もしない( 通常処理へ流れる ) callback(null, request); } else { // 許可されていない IP アドレスなのでエラーを返す callback(null, errorResponse(httpVersion)); } }); }
要検討事項
- バケットの設定(暗号化等)とアクセス権限
- Lamdba@Edge のロールに持たせるアクセス権限
DynamoDB での実現方法
想定する構成
MediaLive + MediaStore のプロダクトを作る場合、CloudFront でキャッシュすると想定すると、以下の図になる。
DynamoDB にホワイトリストを登録
テーブルの作成とデータ登録
NoSQL テーブルを作成してクエリを実行する に従いテーブルを作成する。 今回作成したテーブルと登録したデータの内容は次の通り。
Lamdba@Edge の編集
アクセス権限
- DynamoDB へのアクセス権限をもつロールを付与
- フルアクション
- フルアクセス
Lamdba 関数
以下の実装で実現できた。(あくまでサンプル。実運用の際は精査する) ポイントは以下の2つ。
aws.DynamoDB.DocumentClient
を使用するscan
メソッドを使用する
'use strict' const aws = require('aws-sdk'); aws.config.region = 'ap-northeast-1'; // DynamoDB のオブジェクトを作る const ddb = new aws.DynamoDB.DocumentClient({apiVersion: '2012-08-10'}); const params = { TableName: 'ip-white-list' }; 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; // DynamoDB からデータ取得 ddb.scan(params, function(err, response) { if (err) { console.log(err); callback(err); } console.log('response: ' + JSON.stringify(response)); if (!response) { callback('response is null or undefined'); } console.log('response.Items: ' + response.Items); if (!response.Items) { callback('response.Item is null or undefined'); } const permitIp = response.Items.map((item) => { // ここで指定する `ip_address` は DynamoDB のテーブルに作成したカラム return item.ip_address; }); console.log('permitIp: ' + permitIp); const isPermittedIp = permitIp.includes(clientIp); if (isPermittedIp) { // 許可されているIPアドレスなので何もしない( 通常処理へ流れる ) callback(null, request); } else { // 許可されていない IP アドレスなのでエラーを返す callback(null, errorResponse(httpVersion)); } }); }
要検討事項
- DynamoDB の設定
- Lamdba@Edge のロールに持たせるアクセス権限
まとめにかえて
以上、S3 と Dynamo DB に IP アドレスのホワイトリストを持たせて、Lambda@Edge から利用する方法を見てきた。
これで
- 別サービスと連携してホワイトリストを実現出来る
- Lambda 関数内にデータを持つ必要はなくなった
- 実装 と データ で密な関係を持たずに済む
というのが分かった。
要検討事項として
- S3 や DynamoDB の設定
- アクセス権限
といったものはあるが、これらについては実際の運用に合わせて適宜見直す。
備考
エラー時のレスポンス
記事中の Lambda 関数では許可されていない IP アドレスだった場合にレスポンスに body
を設定しているが、場合によってはリダイレクトを行って任意のページに誘導したいケースもあると思う。
リダイレクトについては 公式の開発ガイド にサンプルが載っているので参照されたい。
一応参考までにサンプルコードを転記しておく。
例: HTTP リダイレクトの生成 (生成されたレスポンス)
'use strict'; exports.handler = (event, context, callback) => { /* * Generate HTTP redirect response with 302 status code and Location header. */ const response = { status: '302', statusDescription: 'Found', headers: { location: [{ key: 'Location', value: 'http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html', }], }, }; callback(null, response); };
参考
公式
注意書き
本記事は Qiita でも公開 しております。