はじめに
こちらの記事 で IP アドレスによるフィルタリングが実現できたので、本記事ではフィルタリングに用いる IP アドレスのリストを
- S3
- DynamoDB
に持たせる方法について見ていく。
注意
本記事は 2020年7月10日 時点の情報です。
ご覧になられた時点で 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);
};