弊社で AWS CDK を使って TypeScript Lambda 管理の運用が始まりました。運用に乗せるまでに試行錯誤して作った Lambda 管理サンプルコードをまとめておきます。

とりあえず TypeScript な Lambda

Lambda 自体を TypeScript で書きたいので @aws-cdk/aws-lambda-nodejs パッケージの NodejsFunction を使って Lambda を定義しています。これの便利なところはビルド含めて全部勝手にやってくれるところです。普通の JavaScript であれば @aws-cdk/aws-lambda パッケージの Function で同じように書くことができます。

import * as core from '@aws-cdk/core';
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
import {RemovalPolicy} from '@aws-cdk/core';
import {LogRetention} from '@aws-cdk/aws-lambda';
import {RetentionDays} from '@aws-cdk/aws-logs';

export class SimpleLambdaStack extends core.Stack {
  constructor(scope: core.App, id: string, props?: core.StackProps) {
    super(scope, id, props);

    const fn = new NodejsFunction(this, 'SimpleLambda', {
      entry: 'lambda/simple-lambda/index.ts',
      currentVersionOptions: {
        removalPolicy: RemovalPolicy.RETAIN,
      },
      memorySize: 256,
      timeout: core.Duration.seconds(180),
      environment: {
        'TZ': 'Asia/Tokyo',
      },
    });

    // CloudWatch Logs の保持期間を指定(デフォルトだと無期限)
    new LogRetention(this, 'SaveSessionLogRetention', {
      logGroupName: `/aws/lambda/${fn.functionName}`,
      retention: RetentionDays.ONE_WEEK,
    });
  }
}

currentVersionOptions は Lambda のバージョンを残す設定です。常に LATEST だけで良いのであれば指定しなくて良いです。その他にメモリやタイムアウトなど Lambda のオプションは全て指定できます。

1つ注意点としてはデフォルトで作成される CloudWatch Logs の保持期間は無期限になっています。ログが大量に出力される場合はコストが厳しくなるので要件に合わせて期間指定をした方が良いです。

npm パッケージを含めた Lambda

これは普通に Lambda のコードの中で import すれば自動でやってくれます。

npm install uuid
import {v4 as uuidv4} from 'uuid';

export async function handler() {
  console.log(uuidv4());
}

既存のリソースに接続する Lambda

この例はすでに作ってある Kinesis Data Stream をトリガーとする Lambda の例です。

AWS CDK では既存リソースの取得メソッドは fromXXX と from で始まるようになっているので、欲しいリソースパッケージのリファレンスを見ると大抵見つかります。リソースによって arn 指定するものもあれば他の識別子になっていることもあります。見つからない場合は AWS CDK GitHub リポジトリの Issue を検索すると、大体要望が上がっているので、それをウオッチするといいでしょう。

import * as core from '@aws-cdk/core';
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
import {RemovalPolicy} from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';

export class ExistsResourceLambdaStack extends core.Stack {
  constructor(scope: core.App, id: string, props?: core.StackProps) {
    super(scope, id, props);

    const fn = new NodejsFunction(this, 'ExistsResourceLambda', {
      entry: 'lambda/exists-resource-lambda/index.ts',
      currentVersionOptions: {
        removalPolicy: RemovalPolicy.RETAIN,
      },
    });

    // Kinesis をトリガーにするために Lambda ポリシーを調整
    fn.addToRolePolicy(new iam.PolicyStatement({
      resources: [
        'arn:aws:kinesis:ap-northeast-1:11111111:stream/sample',
      ],
      actions: [
        'kinesis:DescribeStream',
        'kinesis:DescribeStreamSummary',
        'kinesis:GetRecords',
        'kinesis:GetShardIterator',
        'kinesis:ListShards',
        'kinesis:ListStreams',
        'kinesis:SubscribeToShard',
      ]}
    ));

    const kinesis = 'arn:aws:kinesis:ap-northeast-1:11111111:stream/sample';
    fn.addEventSourceMapping('KinesisDataTrigger', {
      eventSourceArn: kinesis,
      batchSize: 10,
      startingPosition: lambda.StartingPosition.LATEST,
    });
  }
}

addToRolePolicy で Lambda に Kinesis にアクセスできる権限を追加します。そして addEventSourceMapping で指定リソースをイベントソースに設定します。

Lambda のエイリアスとバージョンを設定

一つの Lambda を開発やステージングとプロダクションなど複数の環境で使い分けるためにエイリアスを使いたくなる時があります。エイリアスを使うと各環境ごとの切り替えコストが低いのでオススメです。

余談ですが、エイリアス単位で環境変数を持つことはできないので、そこは Lambda 関数内で自身のエリアスを取得して設定を切り替える必要があるところが面倒くさいです…。

import * as core from '@aws-cdk/core';
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
import {RemovalPolicy} from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';

export class AliasWithVersionLambdaStack extends core.Stack {
  constructor(scope: core.App, id: string, props?: core.StackProps) {
    super(scope, id, props);

    const fn = new NodejsFunction(this, 'AliasWithVersionLambdaStack', {
      entry: 'lambda/alias-with-version-lambda/index.ts',
      currentVersionOptions: {
        removalPolicy: RemovalPolicy.RETAIN,
      },
    });

    // 最新バージョンを取得
    const currentVersion = fn.currentVersion;
    // development エイリアスは最新バージョンを指定
    const development = new lambda.Alias(this, 'DevelopmentAlias', {
      aliasName: 'development',
      version: currentVersion,
    });

    // production エイリアスはバージョン1を指定
    // バージョンを切り替えたい場合はここを修正してデプロイする
    const prodVersion = lambda.Version.fromVersionArn(this, 'ProductionVersion', `${fn.functionArn}:1`);
    prodVersion.addAlias('production');

    // もし production エイリアスに特定のリソーストリガーを設定したい場合はこちら
    // const prodAlias = lambda.Alias.fromAliasAttributes(this, 'ProductionAlias', {
    //   aliasName: 'production',
    //   aliasVersion: prodVersion
    // });
    // prodAlias.addEventSourceMapping('ProductionKinesisDataTrigger', {
    //   eventSourceArn: 'arn:aws:kinesis:ap-northeast-1:11111111:stream/sample-prod',
    //   batchSize: 10,
    //   startingPosition: lambda.StartingPosition.LATEST
    // })

    const stgVersion = lambda.Version.fromVersionArn(this, 'StagingVersion', `${fn.functionArn}:2`);
    stgVersion.addAlias('staging');
  }
}

Lambda@Edge をデプロイ

Lambda@Edge もいい感じにデプロイができます。この例だと新しく CloudFront と Origin の S3 を新しく作り、そこに Lambda を ORIGIN_REQUEST として設定をしています。S3 や ACM は既存のリソースが使えますが、CloudFront は現時点ではまだ使えません。必ず新規作成になります。

import * as core from '@aws-cdk/core';
import {Bucket} from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as certificatemanager from '@aws-cdk/aws-certificatemanager';
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
import {RemovalPolicy} from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';

export class CloudfrontLambdaEdgeStack extends core.Stack {
  constructor(scope: core.App, id: string, props?: core.StackProps) {
    super(scope, id, props);

    const fn = new NodejsFunction(this, 'LambdaEdge', {
      entry: 'lambda/cloudfront-lambda-edge/index.ts',
      currentVersionOptions: {
        removalPolicy: RemovalPolicy.RETAIN,
      },
      role: new iam.Role(this, 'AllowLambdaServiceToAssumeRole', {
        assumedBy: new iam.CompositePrincipal(
            new iam.ServicePrincipal('lambda.amazonaws.com'),
            new iam.ServicePrincipal('edgelambda.amazonaws.com'),
        ),
      }),
    });

    const fnVersion = new lambda.Version(this, 'LambdaEdgeVersion', {
      lambda: fn,
    });

    const sourceBucket = new Bucket(this, 'ZaruCDKBucket');

    // 既存の S3 バケットを利用したい場合はこちら
    // const arn = 'arn:aws:s3:::...';
    // const sourceBucket = Bucket.fromBucketArn(this, 'ExistsBucket', arn);

    // ACM の承認は手動になるため、メールチェックして approve しないとデプロイ進行しない
    const certificate = new certificatemanager.Certificate(this, 'Certificate', {
      domainName: 'cdk.sunaba.sh',
      subjectAlternativeNames: ['cdk.sunaba.sh'],
    });

    // 既存の ACM を利用したい場合はこちら
    // const arn = 'arn:aws:...';
    // const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn);

    const distribution = new cloudfront.CloudFrontWebDistribution(this, 'ZaruCDKDistribution', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: sourceBucket,
          },
          behaviors: [{
            isDefaultBehavior: true,
            lambdaFunctionAssociations: [
              {
                eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
                lambdaFunction: fnVersion,
              },
            ],
          }],
        },
      ],

      viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
          certificate,
          {
            aliases: ['cdk.example.com'],
            securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1,
            sslMethod: cloudfront.SSLMethod.SNI,
          },
      ),
    });
  }
}

こんな感じで AWS CDK で Lambda を管理するサンプルコード集でした。また運用していく中で使えるものがあれば反映していきます。