AWS の生成AI『CodeWhisperer』で CDK のテストを効率化しよう！

# はじめに

本記事では、生成 AI の一種である Amazon CodeWhisperer を活用して、AWS CDK のアサーションテストを効率化する方法についてご紹介します。 

AWS CDK のアサーションテストでは、CDK で生成された CFn テンプレートが想定通りの値になっているか確認できます。[^1] アサーションテストには、以下のようなメリット・デメリットがあります。

- メリット
  - IaC の**変更容易性**が高まる
  - テストコードが信頼性の高い**ドキュメント**になる
- デメリット
  - **CloudFormation** に慣れていないと難しい
  - **ネストの深い**アサーションなど珍しくなく、開発速度に影響する

一方、CodeWhisperer を活用することで、テスト内容を記したテキストからテストコードを生成できます。そのため、アサーションテストのデメリットであった**難易度が下がり**、**開発速度をカイゼン**することができます。

[^1]: アサーションテストの実装例は、[過去記事](https://qiita.com/y_matsuo_/items/bc27db7942179ffb5825)でもご紹介しています。

# Amazon CodeWhisperer とは？

CodeWhisperer とは、AI によるコーディング支援サービスです。

> CodeWhisperer は数十億行のコードでトレーニングされており、コメントや既存のコードに基づいて、スニペットから完全な関数まで、さまざまなコードの提案をリアルタイムで生成できます。時間のかかるコーディングタスクを回避し、使い慣れていない API でも構築が加速できるようにしましょう。

出典：[Amazon CodeWhisperer](https://aws.amazon.com/jp/codewhisperer/)

2023/4/13 に GA されたばかりのサービスで、活用事例が少しずつ増えている印象です。CodeWhisperer の詳細については、[参考文献](#参考文献)で詳しく紹介されていますので、ここでは割愛します。

# Cloud9 で CodeWhisperer を利用する

本記事では CodeWhisperer と統合されている Cloud9 を使って、テストコードを書いていきます。そのため、以下では Cloud9 での設定方法を簡単にご紹介します。

## 環境

- CDK : v2.85.0
- jest : v29.6.0
- OS : Amazon Linux 2

## Cloud9・CDK プロジェクトの準備

1. [AdministratorAccess](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AdministratorAccess.html) 権限相当の Cloud9 を起動します。  
2. CDK の環境を用意します。例えば、```mkdir cdk-sample && cd cdk-sample && cdk init --language typescript```で環境を構築できます。[^2]

[^2]: CDK の初期設定は、[CDK Intro Workshop](https://cdkworkshop.com/ja/) もご参照ください。　

## CodeWhisperer の設定

Cloud9 起動後、すぐに CodeWhisperer から提案を受けることができます。任意タイミングで CodeWhisperer から提案を受けたい場合は、```Alt + C```を実行します。  

もし、提案が表示されない場合は、AWS Toolkit の設定を確認してみてください。以下のようになっていれば、CodeWhisperer を利用できる状態です。[^3]

![codewhisperer-enable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/264dc34f-f457-ecb0-99bb-a952f24cabe6.png)

[^3]: 設定の詳細は、[公式ガイド](https://docs.aws.amazon.com/codewhisperer/latest/userguide/cloud9-setup.html)をご参照ください。

以下のように表示されている場合は、```Resume Auto-Suggestions```をクリックすると、CodeWhisperer が有効化されます。

![codewhisperer-disable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/0131255e-70b6-6351-90ee-4b3548fc6dee.png)

CodeWhisperer 関連の詳細設定は、Preferences から変更できます。

![cloud9-preferences.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/fad76ead-0226-798c-c928-347c0d4bc493.png)

# CodeWhisperer でテスト駆動開発してみる

ここでは、CodeWhisperer を活用して、テストコード→プロダクトコードの順でコーディングしてみます。以下では、Lambda 関数に関する 3 つのテストケースを取り扱います。

## ① Lambda関数の数を確認

**テストコード**

```Lambda関数の数が1である```というテストケースに対して、CodeWhisperer から提案してもらいます。テストケース名を記載してカーソルを置いておくだけで CodeWhisperer が提案してくれます。任意タイミングでの提案表示イメージは、以下をご参照ください。

![lambda-count.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/7d87a259-6927-a319-dac7-7c5f585bb468.png)

Enter を押して CodeWhisperer の提案を受け入れます。実際に出力されたコードは以下です。

```ts: ./test/cdk-sample.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as CdkSample from '../lib/cdk-sample-stack';

test('Lambda関数の数が1である', () => {
  const app = new cdk.App();
  // WHEN
  const stack = new CdkSample.CdkSampleStack(app, 'MyTestStack');
  // THEN
  const template = Template.fromStack(stack);
  template.resourceCountIs('AWS::Lambda::Function', 1);
}); // 末尾の「);」のみCodeWhispererの手動実行で追加
```

惜しいことに末尾で```);```が足りなかったので、```Alt + C``` で CodeWhisperer を手動実行すると無事に追加されました。とはいえ、ほぼ完璧なテストコードです。素晴らしい。

この時点で```npm test```を実行すると、プロダクトコードがないので、勿論失敗します。

**プロダクトコード**

次にプロダクトコードを書いてみます。こちらも以下の状態から、 CodeWhisperer の提案に従って書いていきます。

![lambda-function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/c19cca7c-7b50-aba4-b5a4-770d2b935359.png)

提案通りに実装すると、以下のようになりました。

```ts: ./lib/cdk-sample-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";

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

    const nodeJsFunction = new nodejs.NodejsFunction(this, "NodeJsFunction", {
      entry: "src/lambda/index.ts",
      handler: "handler",
    });
  }
}
```

この状態でテストを実行すると、```Cannot find entry file at src/lambda/index.ts``` というエラーが発生します。  
Lambda 関数のソースコードファイルが存在しないことから本エラーが発生しています。ファイル生成もテキストのプロンプトで実行できればよいのですが、執筆時点では機能がなさそうなので[^4]、以下コマンドでファイルを追加します。

[^4]: 実現方法をご存知の方いらっしゃいましたら、コメントでご教示いただけると幸いです。

```bash
$ mkdir -p src/lambda && touch src/lambda/index.ts
```

```npm test``` を実行すると、テストが pass になりました。[^5]

```Bash: npm test の実行結果
 PASS  test/cdk-sample.test.ts
  ✓ Lambda関数の数が1である (512 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.482 s, estimated 2 s
Ran all test suites.
```

[^5]: Lambda 関数のソースコードファイルは空のため、アプリケーションサイドのテスト・実装は別途必要になります。

## ② Lambda 関数のランタイムを確認

**テストコード**

先ほどはリソース数の確認であったため、今度は Lambda 関数のプロパティチェックをしてみます。  
Lambda 関数のランタイムが NodeJs16 系であることを確認するため、まずテストケース名を入力します。以下をご覧ください。

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/a1d31843-aac2-0722-4adf-1bf1cd4583ed.png)

上記の通り、テストケース入力中にも、提案をしてくれます。サポート期限切れしている Node.js14 系ではなく、16 系のテストにしてくれているのも、賢いです。この辺り、生成 AI らしいなと感じました。

その後も CodeWhisperer の提案に従って、テストコードを生成すると、以下になります。

```ts: ./test/cdk-sample.test.ts
test('Lambda関数のランタイムがNode.js16.xである', () => {
  const app = new cdk.App();
  // WHEN
  const stack = new CdkSample.CdkSampleStack(app, 'MyTestStack');
  // THEN
  const template = Template.fromStack(stack);
  template.hasResourceProperties('AWS::Lambda::Function', {
    Runtime: 'nodejs16.x',
  });
});
```

完璧なテストコードです。アサーションテストでは CloudFormation の知識が求められますが、CodeWhisperer を活用することでかなり負担が減りそうですね・・・！

**プロダクトコード**

次にプロダクトコードへ追記します。元のソースコードを改行するだけで、ランタイムの提案をしてくれました。

![lambda-runtime.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/b5ff57d1-7da1-9f89-2c8a-f7dbd926bd5d.png)

CodeWhisperer の提案を受け入れた結果は、以下になります。

```diff_typescript: ./lib/cdk-sample-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";

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

    const nodeJsFunction = new nodejs.NodejsFunction(this, "NodeJsFunction", {
      entry: "src/lambda/index.ts",
      handler: "handler",
+     runtime: lambda.Runtime.NODEJS_16_X,
    });
  }
}
```

特に Node.js16 系を指定していないのですが、テストコードの内容と一致しています。```npm test```を実行すると、無事にテストが pass になりました。すごい・・・！

```Bash: npm test の実行結果
 PASS  test/cdk-sample.test.ts
  ✓ Lambda関数の数が1である (530 ms)
  ✓ Lambda関数のランタイムがNode.js16.xである (439 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.842 s, estimated 2 s
Ran all test suites.
```

:::note warn  
（余談）
CodeWhisperer はテストコードを踏まえた上で、Node.js16 系を提案したのか気になったのですが、偶然のようです。試しにテストコードを Node.js18 系へ変更してみましたが、CodeWhisperer の提案は Node.js16 系のままでした。この辺りもいずれ対応しそうな気がしますね...！
:::

## ③ Lambda 関数 が Private Subnet で起動されることを確認

**テストコード**

最後にやや複雑なテストケースとして、Lambda 関数 が 2 つの Private Subnet で起動されることを確認してみます。  
テストケース名を入力すると、CodeWhisperer がリッチな提案をしてくれました。

![lambda-vpc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/a351118d-8fbc-3482-f99b-e02a8bb9679e.png)

提案を受け入れた結果は、以下になります。

```ts: ./test/cdk-sample.test.ts (抜粋)
test("Lambda関数が2つのPrivate Subnetで起動される", () => {
  const app = new cdk.App();
  // WHEN
  const stack = new CdkSample.CdkSampleStack(app, "MyTestStack");
  // THEN
  const template = Template.fromStack(stack);
  template.hasResourceProperties("AWS::Lambda::Function", {
    VpcConfig: {
      SubnetIds: [
        {
          Ref: "PrivateSubnet1",
        },
        {
          Ref: "PrivateSubnet2",
        },
      ],
    },
  });
});
```

Ref の部分が仮値となっていますが、ほぼ完璧なテストコードです。ネストが深くなってくると、テストコードの認知負荷が高くなるため、この提案は**とても嬉しい**ですね・・・！

**プロダクトコード**

次にプロダクトコードへ追記します。プロダクトコードでは、VPC の追加、Lambda 関数と VPC の紐づけが必要になります。  
CodeWhisperer の提案を受け入れながら実装すると、以下のようになりました。

```diff_typescript: ./lib/cdk-sample-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
+import * as ec2 from "aws-cdk-lib/aws-ec2";

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

+   const vpc = new ec2.Vpc(this, "VPC");

    const nodeJsFunction = new nodejs.NodejsFunction(this, "NodeJsFunction", {
      entry: "src/lambda/index.ts",
      handler: "handler",
      runtime: lambda.Runtime.NODEJS_16_X,
+     vpc: vpc,
    });
  }
}
```

この状態でテストを実行すると、以下のようにエラーとなります。

```Bash: npm test の実行結果
 FAIL  test/cdk-sample.test.ts
  ✓ Lambda関数の数が1である (570 ms)
  ✓ Lambda関数のランタイムがNode.js16.xである (506 ms)
  ✕ Lambda関数が2つのPrivate Subnetで起動される (504 ms)

  ● Lambda関数が2つのPrivate Subnetで起動される

    Template has 1 resources with type AWS::Lambda::Function, but none match as expected.
    The 1 closest matches:
    NodeJsFunction6DD2A8DD :: {
      "DependsOn": [ ... ],
      "Properties": {
        "Code": { ... },
        "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" } },
        "Handler": "index.handler",
        "Role": { "Fn::GetAtt": [ "NodeJsFunctionServiceRole9AE046B6", "Arn" ] },
        "Runtime": "nodejs16.x",
        "VpcConfig": {
          "SecurityGroupIds": [ { "Fn::GetAtt": [ "NodeJsFunctionSecurityGroup0B3E41AF", "GroupId" ] } ],
          "SubnetIds": [
            {
    !!         Expected PrivateSubnet1 but received VPCPrivateSubnet1Subnet8BCA10E0
              "Ref": "VPCPrivateSubnet1Subnet8BCA10E0"
            },
            {
    !!         Expected PrivateSubnet2 but received VPCPrivateSubnet2SubnetCFCDAA7A
              "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A"
            }
          ]
        }
      },
      "Type": "AWS::Lambda::Function"
    }

      42 |   });
      43 | });
    > 44 |
         | ^

      at Template.hasResourceProperties (node_modules/aws-cdk-lib/assertions/lib/template.js:1:2556)
      at Object.<anonymous> (test/cdk-sample.test.ts:44:12)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        2.536 s, estimated 3 s
Ran all test suites.
```

:::note
!! の ```Expected XX but received XX```部分で親切に差分を表示してくれているため、テストコードを書き換えます。
:::

**テストコード**

テスト実行結果に従って、テストコードを修正します。[Match.stringLikeRegexp](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions.Match.html#static-stringwbrlikewbrregexppattern) を使って部分一致させてもよいですが、ここではシンプルにテスト実行結果で received した値をコピー&ペーストします。

```diff_typescript: ./test/cdk-sample.test.ts (抜粋)
test("Lambda関数が2つのPrivate Subnetで起動される", () => {
  const app = new cdk.App();
  // WHEN
  const stack = new CdkSample.CdkSampleStack(app, "MyTestStack");
  // THEN
  const template = Template.fromStack(stack);
  template.hasResourceProperties("AWS::Lambda::Function", {
    VpcConfig: {
      SubnetIds: [
        {
-         Ref: "PrivateSubnet1",
+         Ref: "VPCPrivateSubnet1Subnet8BCA10E0",
        },
        {
-         Ref: "PrivateSubnet2",
+         Ref: "VPCPrivateSubnet2SubnetCFCDAA7A",
        },
      ],
    },
  });
});
```

再度、```npm test```を実行すると、無事にテストがpass になりました。

```Bash
 PASS  test/cdk-sample.test.ts
  ✓ Lambda関数の数が1である (579 ms)
  ✓ Lambda関数のランタイムがNode.js16.xである (527 ms)
  ✓ Lambda関数が2つのPrivate Subnetで起動される (503 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.528 s, estimated 3 s
Ran all test suites.
```

# おわりに

CodeWhisperer を活用した CDK のアサーションテスト、いかがでしたでしょうか。アサーションテストのハードルがグッと下がったような気がしませんか・・・！？  
[公式ブログのサンプル](https://aws.amazon.com/jp/blogs/news/10-ways-to-build-applications-faster-with-amazon-codewhisperer/#a-writing-unit-tests)ではテストケース名が英語でしたが、日本語でも良い感じにテストコードを生成してくれるとは思わず、驚きました。

今回、ご紹介していませんが、CodeWhisperer にはセキュリティスキャン機能もあります。[cdk-nag の UT](https://qiita.com/y_matsuo_/items/6ef17964ff3c557f6d1e) と併せて開発を行うと、よりセキュアですね！

本記事がどなたかのお役に立てれば幸いです。

# 余談

2023年5月20日に、AWS CDK Conference Japan 2023 で以下を発表しました。

<script async class="speakerdeck-embed" data-id="aae7f83bc1574fc49146cf1547950130" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

https://speakerdeck.com/mhrtech/cdkconjp2023

発表終盤に今後やりたいこととして、**生成 AI の活用** を上げていました。本発表では CDK テストのメリットだけでなく、苦労点も多数紹介しましたので、アサーションテストに対してハードルを感じられた方もいらっしゃると思います。

前述の通り、CodeWhisperer 導入により、アサーションテストのハードルがグッと下がるのでオススメです。

# 参考文献

- [Amazon CodeWhisperer でアプリケーションをより速く構築する10の方法](https://aws.amazon.com/jp/blogs/news/10-ways-to-build-applications-faster-with-amazon-codewhisperer/)
- [Amazon CodeWhispererでどの程度コーディングが効率化できそうか試してみた](https://acro-engineer.hatenablog.com/entry/2023/04/17/120000)
- [Amazon CodeWhisperer試してみた & 所感](https://zenn.dev/takamin55/articles/977a738b1161df)