この記事は PLAID Advent Calendar 2019 の21日目の記事です。
はじめに
株式会社プレイド にてエンジニアインターンとしてお世話になっている @it-akumi と申します。
プレイドでは KARTE の開発をさせていただいており、専らJavaScriptと格闘する日々です1。
これまでも他社のインターンに参加したことはありましたが、その際はWebサービスそれ自体の開発ではなく、インフラ周りの業務に従事していました。その際に AWS CDK を導入する機会がありましたので、今回はそのテストについて書きます。
AWS CDK とは
Infrastructure as Code という概念が登場して久しいですが、AWS 上に構築するリソースをコード化するために提供されているのが AWS CloudFormation (以下 CFn) です。CFn においては JSON または YAML でリソースの定義ファイル (テンプレート) を書き、これに基づいて様々な AWS リソースの構築がなされます。
さて、AWS CDK (以下 CDK2) は CFn の代替ともいうべきツールです。
これは CFn とは異なり、 TypeScript や Python といった言語でリソースの定義を行います。そのため、それぞれの言語でロジックが書けることはもちろんのこと、IDE による補完の恩恵を受けられたり、テストを書いたりすることもできます。
なお、CFn と CDK の関係については AWS CDK 開発者プレビューに次のように書かれています。
CDK はクラウドインフラストラクチャの「コンパイラ」であると考えてください。(中略) CDK application を実行することによって AWS の “アセンブリ言語” である、CloudFormation テンプレートが生成されます。これでテンプレートを CloudFormation のプロビジョニングエンジンで処理する準備が整います。
つまりCDK によるリソース構築は、まずコードに基づいて CFn で実行可能なテンプレートが作られ、それが実行されて AWS リソースが作られる、という流れになります。
3種類のテスト
公式ドキュメントには、CDK におけるテストとして以下の3つが示されています。
Snapshot tests
コードが生成する CFn テンプレートが、前回生成したものと同一であるかをチェックします。つまり、そのコードによって構築されるリソース及びその設定が既存のものと同一であるかをチェックするということです。
コードのリファクタリングを行う際に利用できるようですが、何かしらの変更を加える場合にはそれが意図的なものであってもテスト自体は失敗するということになります。
Fine-grained assertions
構築するリソースに対して意図した設定がなされているかをテストします。
Snapshot tests は意図的な変更を加えると失敗しますので、リソースの増築や設定変更の際にはこちらのテストが使えるようです。
Validation tests
あるリソースの設定に対するバリデーションのためのテストです。
例
さて、例として以下のリソースを考えます。
ALB にアタッチされた EC2 インスタンス上で Nginx が動作しており、そこに外部からアクセスできるようになっています3。
このリソースを構築するためのコードを一部示します。
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 にありますのでご確認ください。
使用したパッケージは以下です。
$ 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
テストは以下のようになりました。
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) の設定値をチェックしています。
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
テストは以下のようになりました。
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://docs.aws.amazon.com/cdk/latest/guide/testing.html
- https://dev.classmethod.jp/cloud/aws/aws-cdk-testing/
-
KARTEとその開発に用いられる技術スタックについてはPLAID Engineer Blogの こちらの記事 に詳しいです。 ↩
-
この構成について、詳細は https://it-akumi.hatenablog.com/entry/2019/08/26/204310 をご覧ください。 ↩
-
このコマンドで実行できるよう、
package.json
にscripts
を定義しています。 ↩