
AWS CDK v2.124.0 で CloudFront KeyValueStore の Functions への関連付けがサポートされました
こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。
AWS CDK v2.124.0 で CloudFront KeyValueStore の CloudFront Functions への関連付けがサポートされました
cloudfront: associate key value stores to functions (#28571) (5ede456), closes #28377
以前までは下記ブログの通り KeyValueStore だけ可能で、CloudFront Functions との関連付けが未対応だったので、CDK のみで実装を完結させることができなかったのですが、今回のアップデートによりすべて CDK で完結させることができるようになりました。
試してみた
CDK ライブラリのアップグレード
AWS CDK のモジュールを v2.124.0 以上にアップグレードします。
npm i aws-cdk@latest aws-cdk-lib@latest
CloudFront Functions コード
CloudFront Functions のコードです。ユーザー名とパスワードを KVS に保存し、Basic 認証を行うものです。
import cf from 'cloudfront';
const kvsId = 'KVS_ID';
const kvsHandle = cf.kvs(kvsId);
async function handler(event) {
const request = event.request;
const headers = request.headers;
if (
typeof headers.authorization === 'undefined' ||
typeof headers.authorization.value === 'undefined'
) {
return response401;
}
const encoded = headers.authorization.value.split(' ')[1];
const decoded = Buffer.from(encoded, 'base64').toString();
const userRequest = decoded.split(':')[0];
const passRequest = decoded.split(':')[1];
const exist = await kvsHandle.exists(userRequest);
if (!exist) {
return response401;
}
const passStore = await kvsHandle.get(userRequest);
if (passStore !== passRequest) {
return response401;
}
return request;
}
const response401 = {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: { 'www-authenticate': { value: 'Basic' } },
};
このコードの以前に下記で紹介したものをマイナーアップデートしたものです。
コード 3 行目の KVS_ID
は CDK コード内で CloudFront KeyValueStore の ID に置き換えます。
CDK コード
AWS CDK スタックのコードです。
import {
aws_cloudfront,
aws_s3,
aws_s3_deployment,
aws_cloudfront_origins,
Stack,
RemovalPolicy,
Duration,
CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import fs from 'fs';
// CloudFront Function のベーシック認証のコードを読み込む
const basicAuthScript = fs
.readFileSync('src/cloudfront-function/basic-auth/index.js', {
encoding: 'utf-8',
})
.replace(/\n/g, '');
export class CdkSampleStack extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// S3 バケットの作成
const websiteBucket = new aws_s3.Bucket(this, 'WebsiteBucket', {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// CloudFront から S3 バケットへのアクセスを許可するために、
// Origin Access Identity を作成し、S3 バケットのアクセスポリシーに追加する
const originAccessIdentity = new aws_cloudfront.OriginAccessIdentity(
this,
'OriginAccessIdentity'
);
websiteBucket.grantRead(originAccessIdentity);
// CloudFront KeyValueStore の作成
const keyValueStore = new aws_cloudfront.KeyValueStore(
this,
'KeyValueStore'
);
// CloudFront Function の作成
const cloudFrontFunction = new aws_cloudfront.Function(
this,
'AddSecurityHeadersToTheResponseFunction',
{
runtime: aws_cloudfront.FunctionRuntime.JS_2_0, // JavaScript runtime 2.0 を指定
keyValueStore, // CloudFront Function と KeyValueStore の関連付け
// CloudFront Function のコードをインライン指定
code: aws_cloudfront.FunctionCode.fromInline(
basicAuthScript.replace('KVS_ID', keyValueStore.keyValueStoreId)
),
}
);
// CloudFront Destribution を作成
const distribution = new aws_cloudfront.Distribution(this, 'Distribution', {
defaultRootObject: 'index.html',
errorResponses: [
{
ttl: Duration.minutes(5),
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/error.html',
},
{
ttl: Duration.minutes(5),
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/error.html',
},
],
defaultBehavior: {
viewerProtocolPolicy:
aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new aws_cloudfront_origins.S3Origin(websiteBucket, {
originAccessIdentity,
}),
// CloudFront Function と Distribution の関連付け
functionAssociations: [
{
function: cloudFrontFunction,
eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST,
},
],
},
});
// CloudFront Distribution のドメイン名を出力
new CfnOutput(this, 'DistributionUrl', {
value: `https://${distribution.distributionDomainName}`,
});
// S3 バケットへのコンテンツのデプロイ、CloudFront Distribution のキャッシュ削除
new aws_s3_deployment.BucketDeployment(this, 'WebsiteDeploy', {
distribution,
destinationBucket: websiteBucket,
distributionPaths: ['/*'],
sources: [
aws_s3_deployment.Source.data(
'/index.html',
'<html><body><h1>Hello, World!</h1></body></html>'
),
aws_s3_deployment.Source.data(
'/error.html',
'<html><body><h1>Error!!!</h1></body></html>'
),
aws_s3_deployment.Source.data('/favicon.ico', ''),
],
});
}
}
aws_cloudfront.Function
の新規プロパティであるkeyValueStore
で、CloudFront Function と KeyValueStore の関連付けをしています。- ベーシック認証のコードを
fs.readFileSync
で読み込んでいるのは、KeyValueStore の ID を CloudFront Function のコードに CDK 合成時に埋め込むためです。- CloudFront Functions は ENV が指定できないので、コード内で直接指定する必要があります。
- keyValueStore を利用する場合は、ランタイムとして JavaScript runtime 2.0 を指定する必要があります。
動作確認
CDK デプロイをしたら動作確認をしてみます。
作成された KeyValueStore でベーシック認証のユーザー名とパスワードをキーペアとして登録します。
CloudFront Distribution のドメイン名にアクセスすると、ブラウザでベーシック認証が要求されます。
正しいユーザー名とパスワードを入力すると、コンテンツが表示されます。
ユーザー名とパスワードが間違っている場合は、ベーシック認証が要求され続けます。
CloudFront Functions コードにコメントがある場合は構文エラーになる
CDK コード内で replace() により CloudFront Functions コードに KeyValueStore の ID を埋め込んでいますが、その際に改行コードの削除が行われます。
よって Functions コードにコメントがある場合は、次のように Function が構文エラーになります。
よって CloudFront Functions コードにはコメントを書かないようにしましょう。
CloudFront Functions コードを CDK コードに直接埋め込む場合
ちなみに、CloudFront Functions コードを CDK コードに直接埋め込む場合は、下記のようになります。
export class CdkSampleStack extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// S3 バケットの作成
// 省略
// CloudFront から S3 バケットへのアクセスを許可するために、
// Origin Access Identity を作成し、S3 バケットのアクセスポリシーに追加する
// 省略
// CloudFront KeyValueStore の作成
// 省略
// CloudFront Function の作成
const cloudFrontFunction = new aws_cloudfront.Function(
this,
'AddSecurityHeadersToTheResponseFunction',
{
code: aws_cloudfront.FunctionCode
.fromInline(`import cf from 'cloudfront';
const kvsId = '${keyValueStore.keyValueStoreId}';
const kvsHandle = cf.kvs(kvsId);
async function handler(event) {
const request = event.request;
const headers = request.headers;
if (
typeof headers.authorization === 'undefined' ||
typeof headers.authorization.value === 'undefined'
) {
return response401;
}
const encoded = headers.authorization.value.split(' ')[1];
const decoded = Buffer.from(encoded, 'base64').toString();
const userRequest = decoded.split(':')[0];
const passRequest = decoded.split(':')[1];
// KVS にユーザー名のキーが存在するかチェック
const exist = await kvsHandle.exists(userRequest);
if (!exist) {
return response401;
}
// ユーザー名に対応するパスワードを KVS から取得し、リクエストのパスワードと一致するかチェック
const passStore = await kvsHandle.get(userRequest);
if (passStore !== passRequest) {
return response401;
}
return request;
}
const response401 = {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: { 'www-authenticate': { value: 'Basic' } },
};`),
// JavaScript runtime 2.0 を指定
runtime: aws_cloudfront.FunctionRuntime.JS_2_0,
keyValueStore,
}
);
// CloudFront Destribution を作成
// 省略
// CloudFront Distribution のドメイン名を出力
// 省略
// S3 バケットへのコンテンツのデプロイ、CloudFront Distribution のキャッシュ削除
// 省略
}
}
コードの見通しは悪くなりますが、fs.readFileSync
でファイルを読み込む必要がなくなります。
おわりに
AWS CDK v2.124.0 で CloudFront KeyValueStore の Functions への関連付けがサポートされたのでご紹介しました。
今回は例としてベーシック認証を行う CloudFront Functions を作成しましたが、他にもリダイレクト先のマッピングや IP アドレスの管理など様々な用途で便利に利用できると思います。今まで KeyValueStore を利用したくても CDK でリソースを管理している都合により利用を見合わせていたシステムなどでも利用しやすくなったのではないでしょうか。
以上