はじめに
本記事では、生成 AI の一種である Amazon CodeWhisperer を活用して、AWS CDK のアサーションテストを効率化する方法についてご紹介します。
AWS CDK のアサーションテストでは、CDK で生成された CFn テンプレートが想定通りの値になっているか確認できます。1 アサーションテストには、以下のようなメリット・デメリットがあります。
- メリット
- IaC の変更容易性が高まる
- テストコードが信頼性の高いドキュメントになる
- デメリット
- CloudFormation に慣れていないと難しい
- ネストの深いアサーションなど珍しくなく、開発速度に影響する
一方、CodeWhisperer を活用することで、テスト内容を記したテキストからテストコードを生成できます。そのため、アサーションテストのデメリットであった難易度が下がり、開発速度をカイゼンすることができます。
Amazon CodeWhisperer とは?
CodeWhisperer とは、AI によるコーディング支援サービスです。
CodeWhisperer は数十億行のコードでトレーニングされており、コメントや既存のコードに基づいて、スニペットから完全な関数まで、さまざまなコードの提案をリアルタイムで生成できます。時間のかかるコーディングタスクを回避し、使い慣れていない API でも構築が加速できるようにしましょう。
2023/4/13 に GA されたばかりのサービスで、活用事例が少しずつ増えている印象です。CodeWhisperer の詳細については、参考文献で詳しく紹介されていますので、ここでは割愛します。
Cloud9 で CodeWhisperer を利用する
本記事では CodeWhisperer と統合されている Cloud9 を使って、テストコードを書いていきます。そのため、以下では Cloud9 での設定方法を簡単にご紹介します。
環境
- CDK : v2.85.0
- jest : v29.6.0
- OS : Amazon Linux 2
Cloud9・CDK プロジェクトの準備
- AdministratorAccess 権限相当の Cloud9 を起動します。
- CDK の環境を用意します。例えば、
mkdir cdk-sample && cd cdk-sample && cdk init --language typescript
で環境を構築できます。2
CodeWhisperer の設定
Cloud9 起動後、すぐに CodeWhisperer から提案を受けることができます。任意タイミングで CodeWhisperer から提案を受けたい場合は、Alt + C
を実行します。
もし、提案が表示されない場合は、AWS Toolkit の設定を確認してみてください。以下のようになっていれば、CodeWhisperer を利用できる状態です。3
以下のように表示されている場合は、Resume Auto-Suggestions
をクリックすると、CodeWhisperer が有効化されます。
CodeWhisperer 関連の詳細設定は、Preferences から変更できます。
CodeWhisperer でテスト駆動開発してみる
ここでは、CodeWhisperer を活用して、テストコード→プロダクトコードの順でコーディングしてみます。以下では、Lambda 関数に関する 3 つのテストケースを取り扱います。
① Lambda関数の数を確認
テストコード
Lambda関数の数が1である
というテストケースに対して、CodeWhisperer から提案してもらいます。テストケース名を記載してカーソルを置いておくだけで CodeWhisperer が提案してくれます。任意タイミングでの提案表示イメージは、以下をご参照ください。
Enter を押して CodeWhisperer の提案を受け入れます。実際に出力されたコードは以下です。
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 の提案に従って書いていきます。
提案通りに実装すると、以下のようになりました。
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、以下コマンドでファイルを追加します。
$ mkdir -p src/lambda && touch src/lambda/index.ts
npm test
を実行すると、テストが pass になりました。5
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.
② Lambda 関数のランタイムを確認
テストコード
先ほどはリソース数の確認であったため、今度は Lambda 関数のプロパティチェックをしてみます。
Lambda 関数のランタイムが NodeJs16 系であることを確認するため、まずテストケース名を入力します。以下をご覧ください。
上記の通り、テストケース入力中にも、提案をしてくれます。サポート期限切れしている Node.js14 系ではなく、16 系のテストにしてくれているのも、賢いです。この辺り、生成 AI らしいなと感じました。
その後も CodeWhisperer の提案に従って、テストコードを生成すると、以下になります。
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 を活用することでかなり負担が減りそうですね・・・!
プロダクトコード
次にプロダクトコードへ追記します。元のソースコードを改行するだけで、ランタイムの提案をしてくれました。
CodeWhisperer の提案を受け入れた結果は、以下になります。
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 になりました。すごい・・・!
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.
(余談)
CodeWhisperer はテストコードを踏まえた上で、Node.js16 系を提案したのか気になったのですが、偶然のようです。試しにテストコードを Node.js18 系へ変更してみましたが、CodeWhisperer の提案は Node.js16 系のままでした。この辺りもいずれ対応しそうな気がしますね...!
③ Lambda 関数 が Private Subnet で起動されることを確認
テストコード
最後にやや複雑なテストケースとして、Lambda 関数 が 2 つの Private Subnet で起動されることを確認してみます。
テストケース名を入力すると、CodeWhisperer がリッチな提案をしてくれました。
提案を受け入れた結果は、以下になります。
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 の提案を受け入れながら実装すると、以下のようになりました。
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,
});
}
}
この状態でテストを実行すると、以下のようにエラーとなります。
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.
!! の Expected XX but received XX
部分で親切に差分を表示してくれているため、テストコードを書き換えます。
テストコード
テスト実行結果に従って、テストコードを修正します。Match.stringLikeRegexp を使って部分一致させてもよいですが、ここではシンプルにテスト実行結果で received した値をコピー&ペーストします。
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 になりました。
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 のアサーションテスト、いかがでしたでしょうか。アサーションテストのハードルがグッと下がったような気がしませんか・・・!?
公式ブログのサンプルではテストケース名が英語でしたが、日本語でも良い感じにテストコードを生成してくれるとは思わず、驚きました。
今回、ご紹介していませんが、CodeWhisperer にはセキュリティスキャン機能もあります。cdk-nag の UT と併せて開発を行うと、よりセキュアですね!
本記事がどなたかのお役に立てれば幸いです。
余談
2023年5月20日に、AWS CDK Conference Japan 2023 で以下を発表しました。
発表終盤に今後やりたいこととして、生成 AI の活用 を上げていました。本発表では CDK テストのメリットだけでなく、苦労点も多数紹介しましたので、アサーションテストに対してハードルを感じられた方もいらっしゃると思います。
前述の通り、CodeWhisperer 導入により、アサーションテストのハードルがグッと下がるのでオススメです。
参考文献
- Amazon CodeWhisperer でアプリケーションをより速く構築する10の方法
- Amazon CodeWhispererでどの程度コーディングが効率化できそうか試してみた
- Amazon CodeWhisperer試してみた & 所感