AWS CDK と3つのテスト

この記事は [PLAID Advent Calendar 2019](https://qiita.com/advent-calendar/2019/plaid) の21日目の記事です。

# はじめに

[株式会社プレイド](https://qiita.com/organizations/plaid) にてエンジニアインターンとしてお世話になっている @it-akumi と申します。
プレイドでは [KARTE](https://karte.io/) の開発をさせていただいており、専らJavaScriptと格闘する日々です[^1]。

これまでも他社のインターンに参加したことはありましたが、その際はWebサービスそれ自体の開発ではなく、インフラ周りの業務に従事していました。その際に AWS CDK を導入する機会がありましたので、今回はそのテストについて書きます。

# AWS CDK とは

Infrastructure as Code という概念が登場して久しいですが、AWS 上に構築するリソースをコード化するために提供されているのが AWS CloudFormation (以下 CFn) です。CFn においては JSON または YAML でリソースの定義ファイル (テンプレート) を書き、これに基づいて様々な AWS リソースの構築がなされます。

さて、AWS CDK (以下 CDK[^2]) は CFn の代替ともいうべきツールです。
これは CFn とは異なり、 TypeScript や Python といった言語でリソースの定義を行います。そのため、それぞれの言語でロジックが書けることはもちろんのこと、IDE による補完の恩恵を受けられたり、テストを書いたりすることもできます。

なお、CFn と CDK の関係については [AWS CDK 開発者プレビュー](https://aws.amazon.com/jp/blogs/news/aws-cdk-developer-preview/)に次のように書かれています。

> CDK はクラウドインフラストラクチャの「コンパイラ」であると考えてください。(中略) CDK application を実行することによって AWS の “アセンブリ言語” である、CloudFormation テンプレートが生成されます。これでテンプレートを CloudFormation のプロビジョニングエンジンで処理する準備が整います。

つまりCDK によるリソース構築は、まずコードに基づいて CFn で実行可能なテンプレートが作られ、それが実行されて AWS リソースが作られる、という流れになります。

# 3種類のテスト

[公式ドキュメント](https://docs.aws.amazon.com/cdk/latest/guide/testing.html)には、CDK におけるテストとして以下の3つが示されています。

## Snapshot tests

コードが生成する CFn テンプレートが、前回生成したものと同一であるかをチェックします。つまり、そのコードによって構築されるリソース及びその設定が既存のものと同一であるかをチェックするということです。

コードのリファクタリングを行う際に利用できるようですが、何かしらの変更を加える場合にはそれが意図的なものであってもテスト自体は失敗するということになります。

## Fine-grained assertions

構築するリソースに対して意図した設定がなされているかをテストします。

Snapshot tests は意図的な変更を加えると失敗しますので、リソースの増築や設定変更の際にはこちらのテストが使えるようです。

## Validation tests

あるリソースの設定に対するバリデーションのためのテストです。

# 例

さて、例として以下のリソースを考えます。
ALB にアタッチされた EC2 インスタンス上で Nginx が動作しており、そこに外部からアクセスできるようになっています[^3]。

![aws-cdk-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541496/3f43c68e-1f05-f203-1b48-1a428478d320.png)

このリソースを構築するためのコードを一部示します。

```typescript:lib/network-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");

interface NetworkStackProps extends cdk.StackProps {
    cidrMask: number;
}

export class NetworkStack extends cdk.Stack {
    readonly vpc: ec2.Vpc;
    readonly cidr: string = "10.0.0.0/16";

    constructor(scope: cdk.Construct, id: string, props: NetworkStackProps) {
        super(scope, id, props);

        if(!(props.cidrMask >= 16 && props.cidrMask <= 28)){
            throw new Error("Valid values of cidrMask are 16--28");
        }

        this.vpc = new ec2.Vpc(this, "Vpc", {
            cidr: this.cidr,
            subnetConfiguration: [
                {
                    name: "Example-Public",
                    cidrMask: props.cidrMask,
                    subnetType: ec2.SubnetType.PUBLIC
                },
                {
                    name: "Example-Private",
                    cidrMask: props.cidrMask,
                    subnetType: ec2.SubnetType.PRIVATE
                }
            ]
        });

        this.vpc.node.applyAspect(new cdk.Tag("Name", "Example-Vpc"));
        for (let subnet of this.vpc.publicSubnets) {
            subnet.node.applyAspect(new cdk.Tag("Name", `${subnet.node.id.replace(/Subnet[0-9]$/, "")}-${subnet.availabilityZone}`));
        }
        for (let subnet of this.vpc.privateSubnets) {
            subnet.node.applyAspect(new cdk.Tag("Name", `${subnet.node.id.replace(/Subnet[0-9]$/, "")}-${subnet.availabilityZone}`));
        }
    }
}
```

以後、上記のコードに対するテストを書いていきます。
本記事に記すのは一部ですが、コード及びテスト全体は [GitHub](https://github.com/ta9mi1shi1/aws-cdk-example) にありますのでご確認ください。

使用したパッケージは以下です。

```
$ npm list --depth=0
example@0.1.0 /home/user/aws-cdk-example
├── @aws-cdk/assert@1.19.0
├── @aws-cdk/core@1.19.0
├── @types/jest@24.0.24
├── jest@24.9.0
(以下省略)
```

なお、テストの実行方法はいずれも `$ npm run build && npm test` です[^4]。

## Snapshot tests

テストは以下のようになりました。

```typescript:test/aws-cdk-example.test.ts
import assert = require("@aws-cdk/assert");
import cdk = require("@aws-cdk/core");

import { NetworkStack } from "../lib/network-stack";

test("NetworkStack Snapshot Tests", () => {
    const app = new cdk.App();
    const networkStack = new NetworkStack(app, "NetworkStack", { cidrMask: 24 });
    expect(assert.SynthUtils.toCloudFormation(networkStack)).toMatchSnapshot();
});
```

これを実行すると、`test/__snapshots__` というディレクトリにスナップショットが作られます。内部では Object として `lib/network-stack.ts` から生成される CFn テンプレートが入っており、これが比較対象として用いられることが窺えます。

## Fine-grained assertions

テストは以下のようになりました。
CDK によって構築される VPC の2つのプロパティ (CidrBlock と Tags) の設定値をチェックしています。

```typescript:test/aws-cdk-example.test.ts
import "@aws-cdk/assert/jest";
import cdk = require("@aws-cdk/core");

import { NetworkStack } from "../lib/network-stack";

test("NetworkStack Fine-Grained Assertions", () => {
    const app = new cdk.App();
    const networkStack = new NetworkStack(app, "NetworkStack", { cidrMask: 24 });
    expect(networkStack).toHaveResource("AWS::EC2::VPC", {
        CidrBlock: "10.0.0.0/16",
        Tags: [{ "Key": "Name", "Value": "Example-Vpc" }]
    });
});
```

テストとして書いたプロパティとその値は CFn のテンプレート形式に準じているようですので、必要に応じてそちらのドキュメントも参照する必要があるかもしれません。

なお、 `toHaveResource` メソッドは通常のJestには存在しませんので、 `@aws-cdk/assert/jest` を import しておく必要があります。

## Validation tests

テストは以下のようになりました。

```typescript:test/aws-cdk-example.test.ts
import "@aws-cdk/assert/jest";
import cdk = require("@aws-cdk/core");

import { NetworkStack } from "../lib/network-stack";

test("NetworkStack Validation Tests With Valid CidrMask", () => {
    const app = new cdk.App();
    const networkStack = new NetworkStack(app, "NetworkStack", { cidrMask: 24 });
    expect(networkStack).toHaveResource("AWS::EC2::Subnet", {
        CidrBlock: "10.0.0.0/24"
    });
});

test("NetworkStack Validation Tests With Invalid CidrMask", () => {
    const app = new cdk.App();
    expect(() => {
        new NetworkStack(app, "NetworkStack", { cidrMask: 32 });
    }).toThrowError("Valid values of cidrMask are 16--28")
});
```

NetworkStack に引数として渡すプロパティに対するバリデーションのテストです。
cidrMask が適切な場合にはそれに応じたサブネットが作られ、不適切な場合には例外を投げることを確かめています。



# おわりに

今回書いた CDK は AWS でのみ利用可能なツールですが、プレイドでは AWS, GCP を併用したマルチクラウド構成が取られています[^5]。CDK のマルチクラウド版ともいうべきツールとしては [Pulumi](https://www.pulumi.com/) が思いあたりますが、こちらは触れたことがありませんので機会があればまたいずれ。

# 参考リンク

* https://docs.aws.amazon.com/cdk/latest/guide/testing.html
* https://dev.classmethod.jp/cloud/aws/aws-cdk-testing/

[^1]: KARTEとその開発に用いられる技術スタックについてはPLAID Engineer Blogの [こちらの記事](https://tech.plaid.co.jp/plaid_loves_nodejs/) に詳しいです。
[^2]: CDK は Cloud Development Kit の頭文字です。[Go CDK](https://gocloud.dev/) なるものも存在するようです。
[^3]: この構成について、詳細は https://it-akumi.hatenablog.com/entry/2019/08/26/204310 をご覧ください。
[^4]: このコマンドで実行できるよう、 `package.json` に `scripts` を定義しています。
[^5]: https://codezine.jp/article/detail/10401