AWS CDKでCloudFrontFunctionsとKeyValueStoreを作成し、メンテモードを実装する
概要
S3
とCloudFront
でReactなどのSPAを配信する方法はコスト的にも安価(ほぼゼロ)で済むのでPoCやプロトタイプ開発のフェーズだと重宝します。
ただ、それらはその性質上、逐次更新していくため「メンテナンス中...」といった表記を挟みたくなることがしばしばあります。
今回はCloudFront Functions
を用いてメンテナンスモードを実現する方法を紹介します。
各種リソースはすべてAWS CDK
で作る前提で話を進めますが、マネコンからの手動作成でも手間なく可能な内容だと思います。
CloudFront Functionsでメンテモードが実現できる仕組み
CloudFront Functions
はCloudFront
を通過するリクエスト・レスポンスに対してJSの関数を設定することでその内容を制御することができます。
似たものにLambda@Edge
がありますが、あちらより実行時間などの面での制限が厳しく、それゆえに高速での処理が可能です。
そんなCloudFront Functions
で利用できるデータストアとしてKeyValueStore
が昨年リリースされました。
その名の通りシンプルなKVSで、用途によって様々な運用が可能です。
今回はメンテナンスモードであるかどうかのフラグの格納先として利用していますが、それ以外に例えばABテスト用のパラメータやリダイレクト先の設定などでも使えると思います。
準備
AWS CDK
を用いる場合は最低でもv2.118.0
から利用可能です。
バージョンが低い場合は引き上げます。既にv2.118.0
以上であれば不要です。
yarn upgrade aws-cdk@latest aws-cdk-lib@latest
実装
順番に関連するコードを見ていきます。
CloudFront Functions
まずはCloudFront Functions
本体です。
先ほども書いた通り、シンプルなJSの関数になっています。
KeyValueStore
のIDですが、これは後でCDK
から注入する形になるので、この段階では"KVS_ID"
という値でハードコーディングしています。この文字列を後から正しい値でreplaceをかけることになります。
KeyValueStore
から得られたメンテナンスモードフラグが"true"
の場合は、許可済みのアクセス元IPからのリクエストだけを通し、それ以外は503のレスポンスを返すようにしています。
CloudFront Functions
を差し込めるタイミングは2つあり、ViewerRequest
とViewerResponse
です。ざっくり言うとリクエスト取得時とレスポンス返却時です。
今回は若干冗長な感じがしますが、両方とも使用しています。
ViewerRequest
でリクエスト先を/maintenance.html
に振り替えて、ViewerReponse
で503エラーに置き換えています。
import cf from 'cloudfront';
const kvsId = 'KVS_ID';
const kvsHandle = cf.kvs(kvsId);
const ALLOW_IPS = ['xxx.xxx.xxx.xxx'];
async function handler(event) {
const request = event.request;
const currentUri = request.uri;
const doReplace = request.method === 'GET' && currentUri.indexOf('.') === -1;
const clientIp = event.viewer.ip;
const isMaintenance = await kvsHandle.get('isMaintenance');
if (
convertStrToBool(isMaintenance) &&
!ALLOW_IP_LIST.includes(clientIp) &&
doReplace
) {
request.uri = '/maintenance.html';
}
return request;
}
function convertStrToBool(str) {
if (typeof str != 'string') {
return Boolean(str);
}
try {
var obj = JSON.parse(str.toLowerCase());
return obj == true;
} catch (e) {
return str != '';
}
}
import cf from 'cloudfront';
const kvsId = 'KVS_ID';
const kvsHandle = cf.kvs(kvsId);
const ALLOW_IPS = ['xxx.xxx.xxx.xxx'];
async function handler(event) {
const response = event.response;
const request = event.request;
const currentUri = request.uri;
const doReplace = request.method === 'GET' && currentUri.indexOf('.') === -1;
const clientIp = event.viewer.ip;
const isMaintenance = await kvsHandle.get('isMaintenance');
if (
convertStrToBool(isMaintenance) &&
!ALLOW_IP_LIST.includes(clientIp) &&
doReplace
) {
response.statusCode = 503;
response.statusDescription = 'Is Maintenance.';
}
return response;
}
function convertStrToBool(str) {
if (typeof str != 'string') {
return Boolean(str);
}
try {
var obj = JSON.parse(str.toLowerCase());
return obj == true;
} catch (e) {
return str != '';
}
}
CDK
続いてCDK
から必要なリソースを作成するコードです。
CloudFront
の作成自体は一部省略しています。ポイントはKeyValueStore
を作成した後で、先ほどのCloudFront Functions
のコードからKVS_ID
の値を実際のIDで置換してます。
将来的にはもっとスマートにKeyValueStore
の参照をCloudFront Functions
に引き渡す方法が生まれるかもしれませんが、現時点(2024/03/18)ではこれぐらいしか方法が内容でした。情報求。
import { Stack, StackProps } from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
export class SampleStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// KeyValueStoreを作成
const keyValueStore = new aws_cloudfront.KeyValueStore(stack, 'KVS');
// CloudFront Function のコードを読み込む
const viewerRequestScript = readFileSync(
'assets/requestChecker.js',
{
encoding: 'utf-8'
}
).replace(/\n/g, '');
const viewerResponseScript = readFileSync(
'assets/viewerResponse.js',
{
encoding: 'utf-8'
}
).replace(/\n/g, '');
// CloudFront Functionsの作成
const requesetFn = new cloudfront.Function(stack, 'ViewerRequest', {
// KeyValueStoreを指定
keyValueStore,
// ランタイムを設定
runtime: cloudfront.FunctionRuntime.JS_2_0,
// 読み込んだコードから"KVS_ID"の文字列を実際のKeyValueStoreのIDに置き換える
code: cloudfront.FunctionCode.fromInline(
viewerRequestScript.replace('KVS_ID',keyValueStore.keyValueStoreId)
)
});
const responseFn = new cloudfront.Function(stack, 'ViewerResponse', {
keyValueStore,
runtime: cloudfront.FunctionRuntime.JS_2_0,
code: cloudfront.FunctionCode.fromInline(
viewerResponseScript.replace('KVS_ID',keyValueStore.keyValueStoreId)
)
});
// CloudFrontDestribution作成
const distribution = new cloudfront.Distribution(this, 'SampleDistribution', {
defaultRootObject: 'index.html',
defaultBehavior: {
// ...一部略
// CloudFront Functionsを紐付け
functionAssociations: [
// request,responseそれぞれのタイミングで紐付け
{
function: requestFn,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
},
{
function: responseFn,
eventType: cloudfront.FunctionEventType.VIEWER_RESPONSE,
},
],
},
});
}
}
これでKeyValueStore
および、それを参照するCloudFront Functions
が完成しました。
あとは KeyValueStore
に必要な値を入れていくことになります。
※もちろん、CDKでKeyValueStore
を定義する段階で設定することも可能です。その場合は宣言時にsource
を指定します。
KeyValueStoreの設定
マネコンから設定する場合は、他のサービスと同じようにEditから行います。
CloudFront Functions
のロジックと整合性がとれるようにisMaintenance
のキーで値を入れていきます。ちなみにCloudFront Functions
で扱う場合には"false"
といった形で文字列型として扱われるので注意です。
SDKから設定する
SDK
から値の設定をすることも可能です。
以下はLambda
からKeyValueStore
の値を更新するサンプルコードです。
import { Handler } from 'aws-lambda';
import {
CloudFrontKeyValueStoreClient,
DescribeKeyValueStoreCommand,
PutKeyCommand
} from '@aws-sdk/client-cloudfront-keyvaluestore';
import '@aws-sdk/signature-v4-crt';
const client = new CloudFrontKeyValueStoreClient({ region: 'us-east-1' });
export const handler: Handler = async () => {
try {
// 先にKVSを取得する
const kvs = await client.send(
new DescribeKeyValueStoreCommand({
KvsARN: 'xxxxxxxxxx'
})
);
// 取得したKVSのETagやARNなどの情報+更新したいKeyとValueを指定
const data = await client.send(
new PutKeyCommand({
KvsARN: kvs.KvsARN,
Key: 'isMaintenance',
Value: 'false',
IfMatch: kvs.ETag
})
);
} catch (err) {
console.error(err);
}
};
export default handler;
感想
今回はCloudFront Functions
とKeyValueStore
を使って静的サイトもしくはSPAのメンテナンスモードを実現する方法をまとめました。
フロントエンドにフォーカスした内容だったので特に触れませんでしたが、メンテモードにする場合はバックエンドも対応が必要になります。
よく使うものだとALB
+ECS
やEC2
もしくはAPI Gateway
などが挙げられます。
それぞれ方法は異なりますが、以下のような記事が参考になるかと思います。
この記事の内容が役立ちましたら幸いです。
参考
Discussion