AWS CDK で CloudFront Functions での JavaScript runtime 2.0 の選択がサポートされました

2024.01.05

こんにちは、CX 事業本部 Delivery 部の若槻です。

先日に AWS CDK v2.118.0 がリリースされ、CloudFront Functions の L2 Construct で runtime プロパティを設定可能になりました。

Features

  • cloudfront: CloudFront Function runtime property (#28099) (9b466ae), closes #28163

当該のアップデートの Pull Request は以下になります。

このアップデートにより、AWS CDK による CloudFront Functions の構築で、新しいランタイムである JavaScript runtime 2.0 の選択がサポートされるようになりました。

CloudFront Functions のランタイムについて

CloudFront Functions が現在サポートしているランタイムは以下の 2 つです。

  • JavaScript runtime 1.0
  • JavaScript runtime 2.0

いずれも ECMAScript (ES) version 5.1 に準拠しているのに加え、古いランタイムである JavaScript runtime 1.0 が ES バージョン 6 から 9 の一部機能をサポートしているのに対して、新しいランタイムである JavaScript runtime 2.0 は ES バージョン 6 から 12 の一部機能に対応しています。

例えば、JavaScript runtime 2.0 は、1.0 に無い構文として以下をサポートしており、よりモダンなコードの記述が可能になっています。

  • async
  • await
  • const
  • let
  • () => 式(アロー関数式)

上記のような追加の構文や機能の記述を使用した CloudFront Functions が、今回のアップデートにより AWS CDK で簡単に構成できるようになりました。

試してみた

AWS CDK で runtime プロパティを指定して、JavaScript runtime 2.0 ランタイムの CloudFront Functions を構築してみます。

CDK ライブラリのアップグレード

AWS CDK のモジュールを v2.118.0 以上にアップグレードします。

npm i aws-cdk@latest aws-cdk-lib@latest

型定義の確認

node_modules に生成された型定義を確認してみます。

FunctionProps には runtime プロパティが追加されています。既定値は FunctionRuntime.JS_1_0 となっているので、JavaScript runtime 2.0 を使用する場合は明示的に設定が必要です。

node_modules/aws-cdk-lib/aws-cloudfront/lib/function.d.ts

/**
 * Properties for creating a CloudFront Function
 */
export interface FunctionProps {
    /**
     * A name to identify the function.
     * @default - generated from the `id`
     */
    readonly functionName?: string;
    /**
     * A comment to describe the function.
     * @default - same as `functionName`
     */
    readonly comment?: string;
    /**
     * The source code of the function.
     */
    readonly code: FunctionCode;
    /**
     * The runtime environment for the function.
     * @default FunctionRuntime.JS_1_0
     */
    readonly runtime?: FunctionRuntime;
}

FunctionRuntime プロパティです。2 つのランタイムがサポートされています。

node_modules/aws-cdk-lib/aws-cloudfront/lib/function.d.ts

/**
 * The function's runtime environment version.
 */
export declare class FunctionRuntime {
    readonly value: string;
    /**
     * cloudfront-js-1.0
     */
    static readonly JS_1_0: FunctionRuntime;
    /**
     * cloudfront-js-2.0
     */
    static readonly JS_2_0: FunctionRuntime;
    /**
     * A custom runtime string.
     *
     * Gives full control over the runtime string fragment.
     */
    static custom(runtimeString: string): FunctionRuntime;
    private constructor();
}

実装

CloudFront Functions のコードです。Add security headers to the response のテンプレートを使用しています。JavaScript runtime 2.0 で新しくサポートされた async および const が使用されています。

src/cloudfront-function/add-security-headers-to-the-response/index.js

async function handler(event) {
  const response = event.response;
  const headers = response.headers;

  // Set HTTP security headers
  // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation
  headers['strict-transport-security'] = {
    value: 'max-age=63072000; includeSubdomains; preload',
  };
  headers['content-security-policy'] = {
    value:
      "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; frame-ancestors 'none'",
  };
  headers['x-content-type-options'] = { value: 'nosniff' };
  headers['x-frame-options'] = { value: 'DENY' };
  headers['x-xss-protection'] = { value: '1; mode=block' };
  headers['referrer-policy'] = { value: 'same-origin' };

  // Return the response to viewers
  return response;
}

CDK スタックのコードです。aws_cloudfront.Function Construct で runtime プロパティを指定して、JavaScript runtime 2.0 を使用するようにしています。

lib/cdk-sample-stack.ts

import {
  aws_cloudfront,
  aws_s3,
  aws_s3_deployment,
  aws_cloudfront_origins,
  Stack,
  RemovalPolicy,
  Duration,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

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 Function の作成
    const cloudFrontFunction = new aws_cloudfront.Function(
      this,
      'AddSecurityHeadersToTheResponseFunction',
      {
        code: aws_cloudfront.FunctionCode.fromFile({
          filePath:
            'src/cloudfront-function/add-security-headers-to-the-response/index.js',
        }),
        // JavaScript runtime 2.0 を指定
        runtime: aws_cloudfront.FunctionRuntime.JS_2_0,
      }
    );

    // 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_RESPONSE,
          },
        ],
      },
    });

    // 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', ''),
      ],
    });
  }
}

動作確認

前述のコードを CDK デプロイにより AWS アカウントにデプロイします。

JavaScript runtime 2.0 の CloudFront Function が作成されました。

CloudFront Function を Viewer response でテスト実行すると、レスポンスの出力にセキュリティヘッダーが付与されました。

ブラウザから CloudFront Distribution に実際にアクセスをすると、レスポンスにセキュリティヘッダーが付与されていることが確認できました。

各ランタイムバージョンのコード例

JavaScript runtime 1.0 および 2.0 のコード例は以下の公式ドキュメントにまとめられています。

次のような実際によくあるユースケースのサンプルが紹介されており、とても参考になるのでぜひ合わせてご確認ください。

  • レスポンスに Cache-Control ヘッダーを追加する
  • Cross-Origin Resource Sharing (CORS) ヘッダーをレスポンスに追加
  • Cross-Origin Resource Sharing (CORS) ヘッダーをリクエストに追加
  • レスポンスにセキュリティヘッダーを追加する
  • リクエストに True-Client-IP ヘッダーを追加する
  • ビューワーを新しい URL にリダイレクトさせる
  • index.html を追加してファイル名を含まない URL をリクエストする
  • リクエストのシンプルトークンを検証する
  • クエリ文字列パラメータの正規化

おわりに

AWS CDK で CloudFront Functions での JavaScript runtime 2.0 の選択がサポートされたのでご紹介しました。

CloudFront Functions は Lambda@Edge とも異なる独自ランタイムを使用しています。今までは L2 Construct では JavaScript runtime 1.0 という構文や機能が比較的貧弱なランタイムのみサポートされていたのですが、今回のアップデートによりモダンな書き方ができる 2.0 が使用可能になりました。

公式ドキュメントでも JavaScript runtime 2.0 の利用が推奨されているので、今後 AWS CDK で新しく CloudFront Functions を構築する場合は、基本的には 2.0 を選択するようにしましょう。

以上