Amazon CloudFront KeyValueStoreをSAMを用いて作成してみた
カシオで、バックエンド開発に携わっている入社2年目の伊藤です。
現在担当しているプロジェクトで、AWSが提供するサービス「CloudFront」を使用していることから、このサービスに関する学習や調査を実施しています。
その過程で、2023年11月からCloudFrontがサポートするようになったサービス「KeyValueStore」に目が止まり、現在のプロジェクトに盛り込めないかと考えるようになりました。この記事では、CloudFrontをはじめとしたサービスの役割を紹介したうえで、新サービスKeyValueStoreと、これに紐付くCloudFront Functionsの実装内容をまとめました。
なるべく平易な形で、自分の中で噛み砕いて書いてみましたので、本質的な誤りなどはぜひご指摘いただけると幸いです。
CDNとCloudFront
AWSは、CDN(コンテンツ配信ネットワーク)というサービスを提供しています。CDNは、世界中のエッジロケーションに配置されたサーバーを利用してコンテンツを配信することが特徴で、ユーザーがコンテンツにアクセスする際に、最も近いエッジサーバーからコンテンツが提供されることから、高速な応答時間と低遅延を実現します。
なお、AWSが提供する代表的なCDNサービスとして、CloudFrontが挙げられます。CloudFrontは、ウェブサイトや動画、アプリケーションの静的・動的なコンテンツを配信するだけでなく、APIやストリーミングメディアなど、さまざまなコンテンツの配信に対応しています。
また、CloudFrontは、CloudFront Functionsというサービスを使用することでリクエストやレスポンスの変換、ルーティング、認証などの処理を関数コード(JavaScript)を用いてエッジ(エンドユーザーから最も近い物理的な場所やネットワークの境界)で行うことができます。
そして、このCloudFront Functionsは、KeyValueStoreと呼ばれるデータストアを2023年11月からサポートするようになりました。
KeyValueStore
KeyValueStoreは、CloudFront Functions内でのデータを共有するためのデータストアとなっています。主にキーと値のペアを格納し、CloudFront Functionsからアクセスすることができます。これにより、CloudFront Functionsのコード内で参照するキーと値を別の場所で安全に管理できるようになります。
以下に、KeyValueStoreのSAMを用いた作成方法を記します。
KeyValueStoreの作成方法
SAMテンプレート
KeyValueStoreを作成するためのSAMテンプレートは、以下の通りです。
Type: AWS::CloudFront::KeyValueStore
Properties:
Comment: String
ImportSource:
SourceArn: String
SourceType: String
Name: String
comment
KeyValueStoreに対するコメント
ImportSource
KeyValueStoreのインポートソース
Name(必須キー)
KeyValueStoreを識別する名前
以下、実装例となります。
CloudFrontKeyStore:
Type: AWS::CloudFront::KeyValueStore
Properties:
Comment: "Authority management"
Name: !Sub "${Product}-${System}-${Environment}-key-value-store"
【補足】ImportSourceキーの活用
今回は、KeyValueStoreでデータ管理しましたが、よりセキュアにデータを管理するためにS3にキーと値を保管し、KeyValueStoreでS3のデータをインポートし活用することも可能となっています。
この場合は、ImportSourceキーでSourceArn及びSourceType(許可される値:S3)を指定し、S3と紐付けします。
CloudFront Functionsとの紐付け方
SAMテンプレート
Cloud FrontFunctionsを作成するためのSAMテンプレートは、以下の通りです。
Type: AWS::CloudFront::Function
Properties:
AutoPublish: Boolean
FunctionCode: String
FunctionConfig:
FunctionConfig
FunctionMetadata:
FunctionMetadata
Name: String
AutoPublish
LIVE関数の作成時に関数を自動的に公開するか否かを決定するフラグ
FunctionCode(必須キー)
リクエストやレスポンスを処理する関数コード(JavaScript)
FunctionConfig(必須キー)
CloudFront機能に関する設定情報
FunctionMetadata
CloudFront 関数に関するメタデータ
Name(必須キー)
関数を識別する名前
KeyValueStoreと紐付くキー
この4つのキーのうちKeyValueStoreと紐付くキーは
FunctionConfig
FunctionCode
となります。
FunctionConfigでは、KeyValueStoreARNと紐付けさせます。
FunctionConfig:
Comment: ""
KeyValueStoreAssociations:
- KeyValueStoreARN: !GetAtt CloudFrontKeyStore.Arn
FunctionCodeでは、KeyValueStoreに保存されているキー・値を呼び出して活用します。例として、リクエスト認証を実施するコードを以下に示します。
FunctionCode: !Sub |
import cf from 'cloudfront';
// KeyValueSrtoreのIdを取得
const kvsId = '${CloudFrontKeyStore.Id}';
// KeyValueSrtoreにIdを用いてアクセス
const kvsHandle = cf.kvs(kvsId);
async function handler(event) {
// KeyValueSrtoreから"secretKey"を非同期に取得
const secretKey = await kvsHandle.get("secretKey");
// リクエストのヘッダー情報を取得
const request = event.request;
const headers = request.headers;
// 認証処理
if (!headers['x-secret-key'] || secretKey != headers['x-secret-key'].value) {
const response = {
statusCode: 401,
statusDescription: 'Unauthorized'
}
return response;
}
return request;
}
このリクエスト認証では、KeyValueStoreに保存されているキーの値とリクエストヘッダーに含まれるシークレットキーの値の一致を確認することで、リクエストの認証を行います。認証が成功した場合には、リクエストオブジェクトを返します。認証が失敗した場合には、401(Unauthorized)のレスポンスオブジェクトを返します。
このコードの詳細処理について、ステップ順に解説します。
1.cloudfrontライブラリからcfモジュールをインポートします
import cf from 'cloudfront';
2.${CloudFrontKeyStore.Id}の値をkvsId変数に代入しています。これは、実行時に実際のKey Value Store(KVS)IDで置き換えられる変数です。
// KeyValueSrtoreのIdを取得
const kvsId = '${CloudFrontKeyStore.Id}';
3.kvsIdを使用して、KeyValueStoreにアクセスするためのハンドルを作成します。
// KeyValueSrtoreにIdを用いてアクセス
const kvsHandle = cf.kvs(kvsId);
4.handler関数が定義されており、eventパラメータを受け取ります。
async function handler(event) {
5.handler関数の内部では、kvsHandleを使用して非同期にKeyValueStoreから"secretKey"の値を取得します。
// KeyValueSrtoreから"secretKey"を非同期に取得
const secretKey = await kvsHandle.get("secretKey");
6.eventオブジェクトからリクエストヘッダーを取得します。
// リクエストのヘッダー情報を取得
const request = event.request;
const headers = request.headers;
7.認証プロセスが行われます。リクエストヘッダーにx-secret-keyが存在し、かつその値がKeyValueStoreから取得した"secretKey"の値と一致しない場合、ステータスコードが401(Unauthorized)のレスポンスオブジェクトを返します。
認証が成功した場合、コードはrequestオブジェクトを返します。
// 認証処理
if (!headers['x-secret-key'] || secretKey != headers['x-secret-key'].value) {
const response = {
statusCode: 401,
statusDescription: 'Unauthorized'
}
return response;
}
return request;
}
実装例
上記の説明を踏まえて、KeyValueStoreとこれに紐づくCloudFrontFunctionsのSAMテンプレート例は以下のようになります。
※CloudFrontFunctionsとCloudFrontの紐付け実装例は、今回の記事のテーマ外のため割愛します。
CloudFrontKeyStore:
Type: AWS::CloudFront::KeyValueStore
Properties:
Comment: "Authority management"
Name: !Sub "${Product}-${System}-${Environment}-key-value-store"
CloudFrontFunction:
Type: AWS::CloudFront::Function
Properties:
Name: !Sub "${Product}-${System}-${Environment}-cloudfront-function"
FunctionConfig:
Comment: ""
KeyValueStoreAssociations:
- KeyValueStoreARN: !GetAtt CloudFrontKeyStore.Arn //CloudFrontKeyStore紐付け
Runtime: cloudfront-js-2.0
AutoPublish: true
FunctionCode: !Sub |
import cf from 'cloudfront';
const kvsId = '${CloudFrontKeyStore.Id}';
const kvsHandle = cf.kvs(kvsId);
async function handler(event) {
const secretKey = await kvsHandle.get("secretKey");
const request = event.request;
const headers = request.headers;
if (!headers['x-secret-key'] || secretKey != headers['x-secret-key'].value) {
const response = {
statusCode: 401,
statusDescription: 'Unauthorized'
}
return response;
}
return request;
}
KeyValueStore活用の場面
今回の実装で、KeyValueStoreは、CloudFront Functionsのコード内で参照するキーと値を別の場所で安全に管理してくれることが改めて分かりました。このことから、機密情報を扱う処理(リクエスト認証処理など)をCloudFront Functionsで実施する際に、KeyValueStoreは重宝されるかと考えています。
以前は、CloudFrontFunctionsを使用する際にシークレットキーや機密情報を直接コード内に埋め込む必要があり、これには機密情報の漏洩や不正利用のリスクがありました。このような懸念点が解消されるのは大きなメリットかと考えます。
また、より安全な情報管理を実現するために、機密情報をS3バケットに保存し、KeyValueStoreを介してインポートする方法もあります。
この方法では、S3バケットへのアクセス権限を厳密に制御し、必要なユーザーだけが機密情報にアクセスできるようになります。この手法については、まだ試したことがないので、是非とも試して、セキュリティ強化につなげたいと考えています。
【カシオのソフトウェア採用についてはこちら】