AWS CDK v2.124.0 で CloudFront KeyValueStore の Functions への関連付けがサポートされました

2024.01.27

こんにちは、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 認証を行うものです。

src/cloudfront-function/basic-auth/index.js

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 スタックのコードです。

lib/cdk-sample-stack.ts

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 コードに直接埋め込む場合は、下記のようになります。

lib/cdk-sample-stack.ts

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 でリソースを管理している都合により利用を見合わせていたシステムなどでも利用しやすくなったのではないでしょうか。

以上