AWS CDK で CloudFront Functions をやってみた

AWS, TypeScript

CloudFront Functions が AWS CDK で L2 対応したので触った。
サンプルコードはこちら

対象バージョン

CDK 1.107.0 以降が必須。

CloudFront Functions とは

CloudFront エッジロケーションで動かす Function。略して CF2。
Lambda@Edge のリージョナルエッジロケーションより、手前のレイヤーだとかなんとか。
Lambda@Edge は呼び出し回数と実行時間で課金されるが、CloudFront Functions は呼び出し回数のみで、無料枠もある。

(公式よりクラメソさんの記事の方がわかりやすいので、そちらをご覧ください)
エッジで爆速コード実行!CloudFront Functionsがリリースされました! | DevelopersIO
エッジでJavaScriptを実行するCloudFront Functionsのユースケースまとめ | DevelopersIO

制約

制約はかなり厳しい。

  • ES5(ECMAScript 5.1)
  • 実行時間 1ms 未満
  • 最大メモリ 2MB
  • 最大サイズ 10 KB
  • トリガーイベントは Viewer request と Viewer response のみ
  • ネットワーク、ファイル、request body へのアクセス不可

あまり凝ったことはできない。

Lambda のコードを書く

今回は、リダイレクト処理とパス解決を雑に ESBuild でビルドする Lambda@Edge があるので、それを CloudFront Functions でも動かしてみた。

ES5 という制約がきついけど、これのために Babel や Polyfill を入れたくないし、大したことしてないので手動で ES5 互換に置き換えようとした。
結局怒られたのは const だけだったので var に書き換えるだけで済んだ。
あとは、バンドルやめて index に突っ込んだりしたので、テスタビリティはどこかに消えてしまった。

Req, Res の型は仮。

src/lambda/index.ts

/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-var */

import { CloudFrontRequest } from 'aws-lambda'

type CloudFrontFunctionRequest = CloudFrontRequest & {
  request: CloudFrontRequest & {
    headers: {
      host: {
        value: string
      }
    }
  }
}

type CloudFrontFunctionResult =
  | {
      statusCode: number
      statusDescription?: string
      headers: {
        location: {
          value: string
        }
      }
    }
  | CloudFrontRequest

function handler(event: CloudFrontFunctionRequest): CloudFrontFunctionResult {
  var request = event.request
  var uri = request.uri
  var host = request.headers.host.value
  var newUrl = `https://${host}/bar/`

  // redirect
  if (uri.match(/^\/foo\/?/)) {
    console.log(`redirect: ${uri} to /bar/`)
    return {
      statusCode: 301,
      statusDescription: 'Found',
      headers: {
        location: {
          value: newUrl,
        },
      },
    }
  }

  // rewrite
  var normalizedUri: string
  var INDEX_PATH = 'index.html' as const
  var extension = /(?:\.([^.]+))?$/.exec(uri)

  if (extension && extension[1]) {
    normalizedUri = uri
  } else {
    // endsWith はそのままいけた
    normalizedUri = uri.endsWith('/')
      ? `${uri}${INDEX_PATH}`
      : `${uri}/${INDEX_PATH}`
  }
  
  return { ...request, uri: normalizedUri }
}

あとはビルドするだけ。
ESBuild 自体は ES5 へのトランスパイルをサポートしないが、--target=es5 を指定して、互換性のあるコードを生成できる。

shell

esbuild src/lambda/index.ts --minify --outfile=src/lambda/dist/index.js --target=es5

CloudFront

CloudFront で aws-cloudfrontFunction を追加し、 functionAssociations で指定する。
eventType は FunctionEventType から使用する。さっき書いたように、イベントタイプは Viewer request と Viewer response のみ。
今回は Viewer request でやる。

src/stacks/cloudfront-stack.ts

import {
  Function as CloudFrontFunction,
  FunctionCode,
  FunctionEventType,
  LambdaEdgeEventType,
} from '@aws-cdk/aws-cloudfront'

// これ
const cloudFrontFunction = new CloudFrontFunction(this, 'Redirect', {
  functionName: 'redirectFunction',
  // 後述
  code: FunctionCode.fromInline(
    fs.readFileSync(
      `${path.resolve(__dirname)}/lambda/dist/index.js`,
      'utf8',
    ),
  ),
})

// 〜〜〜〜〜〜省略〜〜〜〜〜〜
this.distribution = new CloudFrontWebDistribution(this, `Distribution`, {
  originConfigs: [
    {
      s3OriginSource: {
        s3BucketSource: this.bucket,
        originAccessIdentity,
      },
      behaviors: [
        {
          compress: true,
          isDefaultBehavior: true,
          // ここ
          functionAssociations: [
            {
              eventType: FunctionEventType.VIEWER_REQUEST,
              function: cloudFrontFunction,
            },
          ],
        },
      ],
    },
  ],
  // 〜〜〜〜〜〜省略〜〜〜〜〜〜
})

注意点は code の記述。現在はインラインしか対応していないので、ファイルの中身を文字列でぶち込む。
今上がっている、fromFile を追加する PR とやってることは同じ。

src/stacks/cloudfront-stack.ts

  code: FunctionCode.fromInline(
    fs.readFileSync(
      `${path.resolve(__dirname)}/lambda/dist/index.js`,
      'utf8',
    ),
  ),

デプロイすると、コードが CloudFront Functions の develop と live ステージに反映されるはず。
コンソール上でテストして、正しく動作していることを確認した。

AWS コンソール上で、デプロイした CloudFront Function のテストを実行。foo/ にアクセスすると、ステータスコード 301 で、bar/ にリダイレクトされているのが確認できる。