Amazon Web Services ブログ
Amazon CloudFront を利用した都市レベルでの禁輸措置への対応
本記事は、Solutions Architect の Ajit Puthiyavettle と Arnab Ghosh によって投稿された「Complying with city-level embargos using Amazon CloudFront」と題された記事の翻訳となります。
はじめに
OFAC (Office of Foreign Assets Control) などの政府機関の制裁により、Web サイトに都市レベルの詳細な禁輸措置を実施する必要がある場合があります。このブログでは、Amazon CloudFront の位置情報ヘッダーと Amazon CloudFront Functions を使用して実現する方法を説明します。国レベルでの地理的な制限は、AWS WAF を使用して AWS サービスコンソールで設定できることに注意してください。また、このブログでは、都市レベルの記録を管理し、CloudFront Functions を更新できる簡単なCI/CDプロセスを作成する手順も紹介しています。
ソリューションの概要
きめ細かい制御を実装するためには、Amazon CloudFront のサーバーレスコンピュート機能であるCloudFront Functionsを使用して、スケーラブルなエッジソリューションを設定する必要があります。CloudFront Functions は、CloudFront を流れるリクエストとレスポンスを操作し、基本的な認証と認可を実行し、エッジで HTTP レスポンスを生成することなどが可能です。
これがどのように機能するかを見るために、CloudFront を通過するトラフィックがどのように流れるかを簡単に見てみましょう。以下の図 1 は、エンドユーザークライアントからの典型的なリクエストフローを示しています。ユーザーのリクエストが最も近いエッジロケーションにヒットし、要求されたコンテンツがエッジキャッシュに存在しない場合、エッジロケーションはリージョナルエッジロケーションからデータを取得します。要求されたデータがリージョナルエッジロケーションに存在しない場合、データはオリジンから取得され、エッジロケーションと同様にリージョナルエッジロケーションにキャッシュされます。aa
図 1 :CloudFront エッジへのリクエストデータフロー
次に、この新しいソリューションでトラフィックがどのように流れるかを見てみましょう。図 2 に示すように、リクエストがエッジロケーションにヒットすると、エッジで 2 つのイベントが発生します。「ビューワーリクエスト」( CloudFront がビューワーからのリクエストを受信するとき)または「ビューワーレスポンス」( CloudFront がビューワーにレスポンスを返す前)で、エッジロケーションでの CloudFront functions のトリガーに使用できます。このソリューションでは、 CloudFront でサービスを提供する前にリクエストを検証する必要があるため、「ビューワーリクエスト」イベントが使用されていることがわかります。関数コードは CloudFront リクエストヘッダー CloudFront-Viewer-Country
, CloudFront-Viewer-Country-Region
, CloudFront-Viewer-City
を検査し、リクエストの位置情報を決定します。次に、このヘッダーを禁輸ポリシーとマッチングさせ、現在のリクエストが禁輸された都市から発信されているかどうかを検証します。一致するものが見つかった場合、リクエストはHTTPステータス403で拒否される。それ以外の場合、リクエストは CloudFront に転送されます。
図 2 では、ビューワーからのリクエストが CloudFront Function にヒットし、CloudFront エッジキャッシュに転送されるか、403 forbiddenエラーを返してリクエストを拒否するかを決定していることがわかります。
図 2 :都市レベルの禁輸を扱う CloudFront Functions
実施手順
前提条件
以下の前提条件をクリアしていることを確認してください。
- CloudFront Functions を使用するには、CloudFront ディストリビューションが必要です。もし、作成していない場合は、簡単な CloudFront ディストリビューションの開始方法の手順に従ってください。
- CloudFront Function で位置情報ヘッダーを利用するには、
CloudFront-Viewer-Country
,CloudFront-Viewer-Country-Region
,CloudFront-Viewer-City
というヘッダーを含むオリジンリクエスト設定を持つ Origin Request Policy を作成する。Origin Request Policy の作成方法については、オリジンリクエストポリシーの作成 を参照してください。
CloudFront ディストリビューションの設定が完了したら、以下の手順で設定します。
- CloudFront Functions で使用する禁輸ポリシーの構造を決定します。
ポリシーは、必要に応じて(データストアやファイルシステムに)簡単に保存したり操作したりできるように、単純な JSON オブジェクトにすることができます。この実装では、次の構造を使用します。{ "policy": { "country_cd": "US", "country_region_cd": "TX", "country_region_city": "Prosper" }, "policy_id": "p-103" }
country_cd
:ヘッダーCloudFront-Viewer-Country
と一致します。
country_region_cd
: ヘッダーCloudFront-Viewer-Country-Region
と一致します。
country_region_city
: ヘッダーのCloudFront-Viewer-City
と一致します。CloudFront のロケーションヘッダーの詳細については、ビューワーの場所を特定するためのヘッダーを参照してください。
- CloudFront Functions の作成、公開、関連付け
CloudFront Functions は、ECMAScript(ES)バージョン5.1 に準拠し、ES バージョン 6 から 9 の一部の機能もサポートする JavaScript ランタイム環境を使用します。詳しくは CloudFront FunctionsのJavaScript runtime 機能をご覧ください。CloudFront Functions は CloudFront のネイティブ機能です。つまり、コードのビルド、テスト、デプロイはすべてCloudFront 内で行うことができます。AWS コンソールを使った CloudFront Functions の作成、公開、関連付けの方法については、チュートリアル: CloudFront Functions を使用した単純な関数の作成を参照してください。今回の実装では、CloudFront Functions を作成する際に、以下の JavaScript コードを使用することができます。function handler(event) { var policies = {"items": [{"country_cd": "us", "country_region_cd": "tx", "country_region_city": "aubrey"}]}; var request = event.request; var headers = request.headers; if (!headers['cloudfront-viewer-city']) { // "cloudfront-viewer-city" header is missing, skip the validation.' return request; } var requestCountry = headers['cloudfront-viewer-country'].value; var requestRegion = headers['cloudfront-viewer-country-region'].value; var requestCity = headers['cloudfront-viewer-city'].value; var matched = policies.items.some(function(e) { return this[0].toLowerCase() == e.country_cd.toLowerCase() && this[1].toLowerCase() == e.country_region_cd.toLowerCase() && this[2].toLowerCase() == e.country_region_city.toLowerCase() }, [requestCountry, requestRegion, requestCity] ); if (matched) { var response = { statusCode: 403, statusDescription: 'Forbidden', } return response; } return request; }
CloudFront Functions のアップデートの維持
禁輸ポリシーは、コンプライアンス要件やビジネスニーズに基づいて変更することができます。そのため、セキュリティ/運用チームはしばしば禁輸ルールを変更する必要があります。これは、Amazon DynamoDB のような NoSQL データベースに禁輸ポリシーを格納し、必要に応じてポリシーの更新を行う権限を管理チームに与えることで簡単に実現することができます。最新のポリシーを使用するために、CloudFront Functions は実行中に DynamoDB からポリシーをフェッチする必要があります。注意点としては、CloudFront Functions の実行環境は動的なコード評価をサポートしておらず、ネットワークやファイルシステムへのアクセスが制限されているため、DynamoDB への問い合わせができないことです。そのため、CloudFront Functions のコード内でハードコードされたポリシーが使用されるため、禁輸ポリシーが変更されるたびにCloudFront Functions を更新して再デプロイする必要があります。
図 3 は、このプロセスを自動化するシンプルなデプロイメントフレームワークを表しています。なお、これらは他の手段( Code Pipeline など)でも実現できるかもしれませんが、シンプルでサーバーレスであることから、この選択肢を選びました。このフレームワークは、以下のAWSサービスから構成されています。
- Amazon DynamoDB:禁輸ポリシーストアとして使用される NoSQL データベース。
- AWS Lambda:CloudFront Functions コードを更新し、再デプロイするサーバーレスコンポーネントです。
- Amazon S3:CloudFront Functions コードのテンプレートを保存するストレージレイヤーです。
図 3:CloudFront Functions コードのデプロイメントプロセス
デプロイメント・ソリューションのウォークスルー
- セキュリティチームは、AWS SDK /AWS コマンドラインインターフェイス (AWS CLI)/AWS マネジメントコンソールを使用してAmazon DynamoDB テーブルに格納された禁輸ポリシーを更新します。
- 変更イベントは、Amazon DynamoDB ストリームで構成されている AWS Lambda 関数をトリガーします。
- AWS Lambda 関数 (CFF Code Build Function) は以下の動作を行います。
- Amazon DynamoDB テーブルから最新の禁輸ポリシーを取得します。
- Amazon S3 に保存されている最新のコードテンプレートを取得する。
- コードテンプレートと禁輸ポリシーを使って、新しいバージョンの CloudFront Functions のコードを生成する。
- 既存の CloudFront Functions を最新のコードで更新し、公開する。
デプロイメント・ソリューションの導入ステップ
- DynamoDBテーブルの作成
Amazon DynamoDB テーブルは、禁輸ポリシー用のデータストアになります。DynamoDB テーブルの作成方法については、テーブルを作成する。を参照してください。今回の実装では、テーブルの構造は以下のようになります。{ "policy": { "M": { "country_cd": { "S": "us" }, "country_region_cd": { "S": "tx" }, "country_region_city": { "S": "prosper" } } }, "policy_id": { "S": "p-103" //primary key } }
- Amazon S3 バケットを作成する
CloudFront Functions のコードを生成するための CloudFront コードテンプレートは、Amazon S3 バケットにcloudfrontfunction_code_template.txt のようなテキストファイルで格納されます。今回の実装では、以下のようなテンプレートが利用可能です。function handler(event) { var policies = <>; //This is where policies will be injected var request = event.request; var headers = request.headers; if (!headers['cloudfront-viewer-city']) { // if "cloudfront-viewer-city" header is missing, skip the validation.' return request; } var requestCountry = headers['cloudfront-viewer-country'].value; var requestRegion = headers['cloudfront-viewer-country-region'].value; var requestCity = headers['cloudfront-viewer-city'].value; var matched = policies.items.some(function(e) { return this[0].toLowerCase() == e.country_cd.toLowerCase() && this[1].toLowerCase() == e.country_region_cd.toLowerCase() && this[2].toLowerCase() == e.country_region_city.toLowerCase() }, [requestCountry, requestRegion, requestCity] );
Amazon S3 バケットを作成し、ファイルをアップロードする方法については、バケットの作成とバケットにオブジェクトをアップロードするに関するドキュメントをご覧ください。
- AWS Lambda関数を作成する
Python ベースの Lambda 関数は、Boto3 AWS SDK を使用して、コードの生成、CloudFront Functions の更新、およびライブパブリッシングを行います。Python ベースの Lambda 関数の作成方法については、Python による Lambda 関数の構築を参照してください。Lambda 関数には、以下のようなコードを使用できます。import json import boto3 dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('<name of the dynamodb table>') cloudfront = boto3.client('cloudfront') s3 = boto3.resource('s3') CFF_FUNCTION_NAME = '<CloudFront function name>' CFF_CODE_TEMPLATE_BUCKET = '<s3 bucket name>' CFF_CODE_TEMPLATE_KEY = '<name of the template file>' def lambda_handler(event, context): scan_response = table.scan() policy_json = { "items": list(map(lambda x: x['policy'], scan_response['Items'])) } code_template_response = s3.Object(CFF_CODE_TEMPLATE_BUCKET, CFF_CODE_TEMPLATE_KEY) code_template: str = code_template_response.get()['Body'].read().decode('utf-8') updated_code = code_template.replace('<>', json.dumps(policy_json)) describe_fn_response = cloudfront.describe_function( Name=CFF_FUNCTION_NAME ) fn_etag = describe_fn_response['ETag'] fn_config = describe_fn_response['FunctionSummary']['FunctionConfig'] update_fn_response = cloudfront.update_function( Name=CFF_FUNCTION_NAME, IfMatch=fn_etag, FunctionConfig=fn_config, FunctionCode=bytes(updated_code, 'utf-8') ) fn_etag = update_fn_response['ETag'] publish_fn_response = cloudfront.publish_function( Name=CFF_FUNCTION_NAME, IfMatch=fn_etag ) return { 'statusCode': 200, 'body': json.dumps('Cloudfront Function redeployed') }
- DynamoDB Streams の設定
CloudFront Functions は、禁輸ポリシーに変更があるたびに再デプロイする必要があります。そのため、先ほどの手順で作成したLambda 関数を呼び出すために、DynamoDB Streams を利用します。DynamoDB Streams を有効化し、Lambda 関数を呼び出すための設定方法については、DynamoDB Streams と AWS Lambda のトリガーを参照してください。
ソリューションのテスト
このソリューションをテストするために、プロスパー (Prosper) 市(州:テキサス州、国:アメリカ合衆国)にいるユーザーをブロックすることをゴールとします。現在デプロイされている禁輸ポリシーには、まだこのルールは含まれていません。
図 4 プロスパー(Prosper) の禁輸ポリシーなし
つまり、プロスパーにある PC からテストサイト(テスト用の画像を使用)にアクセスすると、サイトにアクセスできます。
図 5 :許可されたサイトアクセス
ここで、DynamoDB テーブルに新しいポリシーを追加します。
図 6:プロスパー(Prosper) が追加された禁輸ポリシー
新しいポリシーを追加すると、Lambda 関数が起動し、最新のポリシーで CloudFront Functions が再デプロイされます。この場合、プロスパーにあるPCからテストサイト(テスト用の画像を使用)にアクセスしようとすると、ブロックされます。
図 7 :サイトへのアクセスが遮断された状態
リソースをクリーンナップする
AWS アカウントへの継続的な課金を避けるため、作成したリソースを削除してください。
- CloudFront のディストリビューションを削除します。
- CloudFront Functions を削除します。
- DynamoDB テーブルを削除します。
- S3 バケット を削除します。
- Lambda 関数を削除します。
まとめ
これで、Amazon CloudFrontを使用した都市別禁輸措置を有効にする準備が整いました。
機能実装にお時間をいただいたことに感謝いたします。
この投稿について質問がある場合は、Amazon CloudFront フォーラム に新しいスレッドを立ち上げるか、AWS サポートにご連絡ください。
翻訳は Solutions Architect の長谷川 純也が担当しました。原文はこちらです