CDK で Lambda バージョンが作成されない問題対応の一例

# はじめに
AWS CDK を使って Lambda 関数をデプロイしている際に、Lambda バージョンが正しく作成されない問題に直面しました。ワークアラウンド的ではありますが、そのときの解決策を備忘録としてまとめておきます。

# 検証環境
- aws-cdk: 2.171.1
- typescript: 5.6.3

# いきなり結論

`Function` と `Version` にタイムスタンプ付きの `description` を付けます。
タイムスタンプはユニークな文字列となり差分を強制的に発生させられるので、毎回必ずバージョンが作成されるようになりました。
なお、本記事では `Function`, `Version` を使用していますが、`CfnFunction`, `CfnVersion` でも同じ挙動となりました。
[AWS CDK - class Version (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Version.html)
[AWS CDK - class CfnVersion (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.CfnVersion.html#construct-props)

:::note info
Lambda 関数にも付けている理由は、Lambda 関数コード以外部分のみの変更の場合、同じ断面で新しいバージョンを作成出来ない旨のエラーとなるための対策です。
:::

[AWS CDK - class Function (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)
[AWS CDK - class CfnFunction (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.CfnFunction.html)

以下、最小構成のサンプルです。
```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';

export class SampleLambdaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda 実行用 IAM ロールを作成
    const lambdaRole = new iam.Role(this, 'LambdaExecutionRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    lambdaRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
    );

    const timeStamp = new Date().toLocaleString();
    
    // Lambda 関数を作成
    const sampleLambdaFunction = new lambda.Function(this, 'SampleLambdaFunction', {
      code: lambda.Code.fromInline(`
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda" };
        };
      `),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      role: lambdaRole,
      description: `Updated at ${timeStamp}`, // <- タイムスタンプを含む description を追加
    });

    // Lambda のバージョンを作成
    const sampleLambdaVersion = new lambda.Version(this, 'SampleLambdaVersion', {
      lambda: sampleLambdaFunction,
      description: `Version created at ${timeStamp}`, // <- タイムスタンプを含む description を追加
    });
    // 既存のバージョンが削除されないようにする
    sampleLambdaVersion.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
  }
}

```

# 解説

## 元のコード

以下の CDK コードで作成された Lambda 関数があるとします。

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';

export class SampleLambdaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda 実行用 IAM ロールを作成
    const lambdaRole = new iam.Role(this, 'LambdaExecutionRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    lambdaRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
    );

    const timeStamp = new Date().toLocaleString();
    // Lambda 関数を作成
    const sampleLambdaFunction = new lambda.Function(this, 'SampleLambdaFunction', {
      code: lambda.Code.fromInline(`
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda" };
        };
      `),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      role: lambdaRole,
    });

    // Lambda のバージョンを作成
    const sampleLambdaVersion = new lambda.Version(this, 'SampleLambdaVersion', {
      lambda: sampleLambdaFunction,
    });
    // 既存のバージョンが削除されないようにする
    sampleLambdaVersion.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
  }
}
```

## Lambda コードを修正

Lambda 本体のコード部分に対して適当に修正を加えます。
```diff_javascript
-       return { statusCode: 200, body: "Hello, CDK Lambda" };
+       return { statusCode: 200, body: "Hello, CDK Lambda !!!" };
```
差分を見てみます。
```bash
cdk diff
```

`Function` のみ更新扱いとなっていますね。
```bash
Stack SampleLambdaStack
Resources
[~] AWS::Lambda::Function SampleLambdaFunction SampleLambdaFunction430BF4AE
 └─ [~] Code
     └─ [~] .ZipFile:
         ├─ [-] 
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda" };
        };
      
         └─ [+] 
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda !!!" };
        };

✨  Number of stacks with differences: 1

```
これだとこのままデプロイしても当然バージョンは作成されません。

## CDK コードを修正

```diff_typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';

export class SampleLambdaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda 実行用 IAM ロールを作成
    const lambdaRole = new iam.Role(this, 'LambdaExecutionRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    lambdaRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
    );

+   const timeStamp = new Date().toLocaleString();
    
    // Lambda 関数を作成
    const sampleLambdaFunction = new lambda.Function(this, 'SampleLambdaFunction', {
      code: lambda.Code.fromInline(`
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda !!!" };
        };
      `),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      role: lambdaRole,
+     description: `Updated at ${timeStamp}`, // <- タイムスタンプを含む description を追加
    });

    // Lambda のバージョンを作成
    const sampleLambdaVersion = new lambda.Version(this, 'SampleLambdaVersion', {
      lambda: sampleLambdaFunction,
+     description: `Version created at ${timeStamp}`, // <- タイムスタンプを含む description を追加
    });
    // 既存のバージョンが削除されないようにする
    sampleLambdaVersion.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
  }
}

```

:::note info
前述にもありますが、Lambda 関数にも `description` を付けているのは、Lambda 関数コード以外部分のみの変更の場合、同じ断面で新しいバージョンを作成出来ない旨のエラーとなるための対策です。
:::


上記でもう一度 `cdk diff` 確認

```bash
Stack SampleLambdaStack
Resources
[~] AWS::Lambda::Function SampleLambdaFunction SampleLambdaFunction430BF4AE
 ├─ [~] Code
 │   └─ [~] .ZipFile:
 │       ├─ [-] 
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda" };
        };
      
 │       └─ [+] 
        exports.handler = async (event) => {
          console.log('Event:', JSON.stringify(event));
          return { statusCode: 200, body: "Hello, CDK Lambda !!!" };
        };
      
 └─ [+] Description
     └─ Updated at 2024/12/6 20:07:38
[~] AWS::Lambda::Version SampleLambdaVersion SampleLambdaVersion22694689 replace
 └─ [+] Description (requires replacement)
     └─ Version created at 2024/12/6 20:07:38

✨  Number of stacks with differences: 1

```
`Version` も差分が検出され更新対象となっているのが分かります。

デプロイすると
```bach
cdk deploy
```
ちゃんとバージョンが作成されているのが確認出来ました！！
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2080620/6e0153d4-ebcc-b4f3-c949-4198a71d8f53.png)

# おわりに

Lambda 関数自体に更新がなくてもバージョンが作成されるのはアレですが、バージョンを作成出来ないほうが問題だったので、他にも方法はあるかとは思いますが、このときは一番手っ取り早そうなこの方法で乗り切りました。

そもそも Lambda 関数の更新を検知して、良しなにバージョンを作成しれくれればありがたいんですけどね。。