AWS CDKでのTestを実行するときにLambdaのスタックがあると実行が途中で止まる問題の解決法

ミロゴス開発Gでは基本的にインフラにAWSを使うことが多いです。古くから存在するシステム等もあるものの、新規のシステムに関しては、なるべくサーバーレスな環境で設計するよう心がけています。

インフラ部分はAWSのIaCツールであるAWS CDKを使っています。CDK PipelinesでCI/CDも手軽に組めるので大変気に入っているのですが、Lambdaのコードのユニットテストは書いているものの、AWS CDK自体のテストは今まで避けてきました。

ちょうどこのあたりに手を付けられるタイミングだったので、いざ書いてみたものの、表題の通りLambdaのスタック作成でエラーにハマり、だいぶ時間を溶かしてしまいました。
あまり日本語でまとまった記事もなかったので、問題の詳細と解決方法をしたためておこうと思います。

- サーバーレスな弊社環境の例
![スクリーンショット 2022-11-16 14.36.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141823/043bcdee-127c-2e8c-5509-edf51cd71542.png)


## tl;dr
- Lambdaのスタック作成時(NodeJsFunction)にビルド周りでエラーも出ずに実行が止まる。
- NestedStackであるとかConstructorであるとかは関係ない。
- テスト時のcontextを書き換えてビルドをスキップする設定を入れると解決する。

## 前提
- 環境
```bash:
"aws-cdk-lib": "2.40.0",

% node -v
v16.14.0
% npm -v
y8.3.1
% yarn -v
1.22.17
```

今回のCDKの構成としては、フル`TypeScript`で(Lambdaのコードも含めて)、libフォルダの下にstackフォルダを切り、`NestedStack`を継承してリソースごとに定義し、それをmainスタックでnewする形としています。

この記事では触れませんが、mainスタックと同階層にpipeline用のスタックも定義しています。

```bash:
 lib
 ├── deploy-stage.ts
 ├── pipeline-stack.ts
 ├── main-stack.ts
 ├── stack
 │   ├── api-gateway-stack.ts
 │   ├── dynamodb-stack.ts
 │   ├── event-bridge-stack.ts
 │   ├── guard-duty-chatbot-stack.ts
 │   ├── guard-duty-stack.ts
 │   ├── index.ts
 │   ├── lambda-stack.ts
 │   ├── mock-api-gateway-stack.ts
 │   ├── monitoring-stack.ts
 │   ├── remote-output.ts
 │   ├── s3-stack.ts
 │   └── waf-stack.ts
...
```

## テストの実装
CDKには大きく2種類のテストがあります。
- `Fine-grained assertions`
    - stackやconstructorが想定通りのリソースを作成するかなどをテストする方法。
    - 一般的なユニットテストの感覚に近い。
- `Snapshot tests`
    - CDKをある時点でビルドしたときの結果と、現在の結果を比べて差分を検出する方法。
    - 意図した差分であれば、スナップショットを更新しアップデートしておく。

今回は`lib/stack`配下の`NestedStack`に対して`Fine-grained assersions`によるテストを行い、mainスタックに対して`Snapshot tests`を行うことにしました。

- 参考：
[Testing constructs - AWS Cloud Development Kit (AWS CDK) v2](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/testing.html)

## Lambdaスタックでテストの実行が止まる問題
私ははじめmainスタックのSnapshot testsから書き始めて、この問題にぶち当たりました。
以下が問題が発生するテストコードと実行結果になります。

```typescript:テストコード
test('MainStack Snapshot Test', () => {
  const app = new cdk.App();

  const stack = new MainStack(
    app,
    'MyTestStack',
    {}
  );
  const template = Template.fromStack(stack).toJSON();
  expect(template).toMatchSnapshot();
});
```

結果は以下のように、`RUNS`のまま途中で止まってしまいます。
```bash:結果
% yarn test
yarn run v1.22.17
(中略)

Bundling asset MyTestStack/LambdaStack/XXXFunction/Code/Stage...
esbuild cannot run locally. Switching to Docker bundling.

 RUNS  test/lib/main-stack.test.ts

```

`(中略)` の箇所ではNodeJsFunctionによるビルドが行われており、1つめのLambdaのビルドの途中で止まってしまっています。

この現象にぶち当たったため、mainスタックのSnapshotテストは一旦諦めて、stackごとのFine-grained assertionsを一つずつ書いていきました。DynamoDBやAPI Gatewayのテストは難なく記載できたものの、Lambdaスタックで上記と同じ現象が発生しました。

日本語では記事が見当たらず、英語で検索すると以下のstack overflowのQuestionが見つかるものの解決せず。
- [amazon web services - CDK Unit test using jest and NodeJsFunction - Stack Overflow](https://stackoverflow.com/questions/70590045/cdk-unit-test-using-jest-and-nodejsfunction)

もう少し調べると、aws-cdk自体のissueに上がっていました(これ自体はPythonのもの)。
- [(assertions): argument to skip bundling of assets · Issue #18125 · aws/aws-cdk](https://github.com/aws/aws-cdk/issues/18125)

テストの実行時にcontextにバンドルの無効化設定を入れるというものでした。この通りにテストを書き直すと、無事通るようになりました :tada: 

```typescript:mainスタックのテストコード
...
import baseContext from '../../cdk.json';
...

test('MainStack Snapshot Test', () => {
  const context = {
    ...baseContext,
    'aws:cdk:bundling-stacks': [],
  };
  const app = new cdk.App({ context });

  const stack = new MainStack(
    app,
    'MyTestStack',
    {},
    dummyBuildConfig,
    'dev',
  );
  const template = Template.fromStack(stack).toJSON();
  expect(template).toMatchSnapshot();
});

```

LambdaStackのFine-grained assertionsテストはこんな感じです。

```typescript:
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import baseContext from '../../../cdk.json';
import { LambdaStack } from '../../../lib/stack';
import { BuildConfig } from '../../../lib/type';
import { dummyBuildConfig } from '../../dummyData';

class DummyStack extends Stack {
  public nestedStack: Stack;
  constructor(
    scope: Construct,
    id: string,
    props: { vpc: ec2.Vpc; securityGroup: ec2.SecurityGroup } & StackProps,
    buildConfig: BuildConfig,
    prefix: string,
  ) {
    super(scope, id, props);
    this.nestedStack = new LambdaStack(this, 'LambdaStack', props, buildConfig, prefix);
  }
}

test('LambdaStack', () => {
  const context = {
    ...baseContext,
    'aws:cdk:bundling-stacks': [],
  };
  const app = new cdk.App({ context });
  const vpcStack = new cdk.Stack(app, 'VpcStack');
  const vpc = new ec2.Vpc(vpcStack, 'Vpc');
  const securityGroup = new ec2.SecurityGroup(vpcStack, 'SecurityGroup', {
    vpc,
  });

  const stack = new DummyStack(
    app,
    'MyTestStack',
    { vpc: vpc, securityGroup: securityGroup },
    dummyBuildConfig,
    'dev',
  );

  const template = Template.fromStack(stack.nestedStack);
  template.resourceCountIs('AWS::Lambda::Function', 10);
});

```

これでFine-grained assertionsテストもSnapshot testsも無事通るようになりました。

## まとめ
AWS CDKは日本語の情報がまだまだ少なく、特にv2以降のテストに関してはこのあたりの日本語の記事が足りていないなぁと感じました。公式のリファレンス自体も日本語訳が不足しているので、もっと積極的に知見を発信していこうと思います。