🤔

AWS CDKのテストはどこまで書くべきか?

2024/03/04に公開

皆さん、AWS Cloud Development Kit (以下、CDK)は使っていますでしょうか?
私達が開発している「Cariot」というSaaSでは、新しいAWSリソースを構築する時はCDKが第一の候補になりつつあり、IaCを実現するのに非常に便利なツールだと思っています。

CDKでは豊富なテスト手段が用意されています。そのため、エンジニアは「何をテストすべきか?」「テストはどこまで書くべきか?」などを考える必要があります。
正解がないトピックスではありますが、効果的な自動テストを目指して、私達のプロジェクトでどのようにCDKを使った自動テストと向き合っているのか、その裏にある考えについてご紹介します。

前提

今回の前提として、私達が開発しているプロダクトでCDKをどのように取り入れているか、説明します。

対象としては、Lambda, DynamoDB, S3, SNS, SQS, Kinesis Data Streams, Data Firehose, Step Functions等、いわゆるサーバレスな構成で定番のAWSサービスの構築部分がメインとなっています。

一方、以下のものはCDKで管理していません。
まずはVPC, RDS, ElastiCacheなど、開発初期に構築するような骨格部分のサービスです。これは、CDKのリリース次点ですでに構築済みだったためです[1]
また、Security Hubなど、構築は1回限りのことが多く、ドキュメントで経緯や手順を残したほうが、取り回しがよいものも、利点が少ないので対象外としています。
最後に、CDKのリリース状況が実験的なモジュールについても、基本的に本番環境での投入は避けています[2]

CDKアプリケーションは独立した単位で細かく分けており、数としては 30ちょっと あります。そのため、CloudFormationスタックもそれと同じ以上の数が存在していることになります。
一般によしとされる単一スタックのプラクティスからは外れていますよね。単一スタックがよいとされている理由は、複数スタックで分けた場合、「スタック間参照」の面倒事が発生するためでしょう。ただ私達の場合、スタック間参照の必要のないような管理の仕方になっており、このデメリットをそこまで感じていません。逆にスタックが分かれていることで、変更をした時に、仮に問題があっても、影響範囲が極小化されていますし、機能の切り離しも行いやすいメリットもあると感じています。

AWS CDKとそのテスト

CDKにおけるテストと一口に言っても、以下のような種類があります。

  • Fine-grained assertion Test
    • 生成されたCFnのリソースやアウトプットの特定の側面をテストする
  • Snapshot Test
    • 生成されたCFnテンプレートを以前のものと比較し、両者に差分があるかをテストする
  • Policy Validation
    • ルール群に準拠しているかどうかを検証
  • Integration Test
    • その名の通り、インテグレーションテストを実行する

詳しくは下記の資料をご参照ください。

Testing constructs
AWS CDK policy validation at synthesis time
AWS CDKにおける開発とテスト (Advanced #1)
AWS CDK アプリケーションのためのインテグレーションテストの作成と実行

CDKを使ったInfrastructure as Codeのテスト戦略・目的を考える

さて、「どれを使っていけばいいんだろう」と考える前に、いったん原点に戻って、何のためにCDKのコードをテストしたいのかを考えてみます。

私は、アプリケーションでもインフラでも、自動テストによって果たしたい目的、そしてその裏にあるテスト戦略というのは本質的に大きな違いはないと思っていて、「開発体験をよくし、チームとしての生産性を最大化すること」だと考えています。

なので、プロジェクトに自動テストを取り入れる際は、タイムパフォーマンスを重視しています。具体的には、次の3つの要件を意識しています。

  • 設定や記述が間違っていた場合に、テストが失敗すること
  • 正しい振る舞いを記述していた場合に、テストコードが修正不要でpassすること
  • テストコードを書くことそのものに、本体コードと比較して時間がかからないこと

1つ目と2つ目の要件は、すなわち、検知漏れ(偽陰性: false negative)と誤検知(偽陽性: false positive)をしないをしないことであり、自動テストがデグレ防止、安全ネットとしての役目を果たすためには必要不可欠なものです。

2つ目について補足すると、私は(反省も含めて)過去の経験から、テストコードのメンテナンス性は非常に重要だと感じており、仕様が変わったりバグがあった時を除き、「テストコードが修正不要(壊れない)」ということを意識しています。プロダクトコードをリファクタリングする度にテストコードが壊れてしまうようだと、自動テストの信用度も下がり、開発体験も損なわれてしまうためです。

テストコードのメンテナンス性を意識する上で、3つ目の「テストコードを書くことそのものに、本体コードと比較して時間がかからないこと」というのもポイントです。
この点においては、アプリケーションのコードよりも、CDKを使ったインフラのテストは、テストコードが書きやすいと感じます。(相対的に)コードの自由度が少ないので一度書き方を覚えれば使えますし、異常系テストの必要性がほとんどなく、ただ一つのあるべき形をそれぞれテストコードとして比較していけばよいからです。

AWS CDKのテストに対するモヤモヤ

CDKを使った効果的な自動テストのためには、何をどこまでテストしていくのか、「テスト観点」の洗い出しに、一番頭を悩ませてきました。
CDKコードにおけるインフラの「間違いやバグ」とは、単なるコード上のtypoもあるかもしれませんが、他にも、オプション(パラメータ)の解釈が間違っていたり、セキュリティ上で問題のある設定をしたり、権限の付与が漏れていたり、多種多様なものがあります。

先に挙げた3つの要件を守りつつ、インフラの「間違いやバグ」を必要十分にテストしようとすると、提供されているテスト手段のそれぞれで、大きな不満はないものの、うまい落とし所が見つけられていないんですよね…。

Fine-grained assertion Test

私達が一番よく使っているテストの手段ですが、本体のコードと表裏一体のテストを書くだけになりがち、というところがモヤモヤのポイントです。
つまり、リソースの構成が本来の「正しい」動作であることではなく、本体のコードと「整合している」ことの保証しかしないような書き方になりがちでした。そのため、権限の付与が漏れていたり、オプション(パラメータ)の解釈が間違っていたり、といった間違いや勘違いに対して、テストコード上の観点から漏れてしまうため、テストできていません。

また、どこまでアサーションするかの「深さ」についても、頭を悩ませています。
アサーションを深くすればするほどに、本体のコードと同じような構造になり、CDKを使わずに素のCFnテンプレートで書いた方が二重管理の必要もなくなりシンプルな気がしてしまうのです。まぁとは言え、CDKを使うメリットは、単なる設定ファイル(YAML)をプログラムコードとして記述できることだけでなく、ポリシーの抽象化などの恩恵が強いので、すぐにYAMLに戻ることはないとは思いますが…。

Snapshot Test

大前提として、Snapshot Testは、特別な理由がない限りは導入した方がよいと思います。導入のハードルも低いですし。

Snapshot Testを導入すべき理由は、リファクタリングやバージョンアップの時などに予期せぬ変更がされていないかを確かめるためです。

一方で、構成変更のため意図的に本体コードを修正した場合でも、必ずテストが失敗し、テストコードの修正が発生してしまうというのがモヤモヤのポイントです。これは2つ目の要件の「テストコードが修正不要」という点から外れてしまいます。

ただしこのモヤモヤは、私達のプロジェクトの特殊な事情なのかもと考えています。前提条件のところで説明した通り、私達は、単一スタックではなく、小さい複数のスタックでCDKを活用しています。リファクタリングのためだけにCDKのコードを修正する機会は少なく、実験的なモジュールを使わない・リリースノートはきちんと読む、といった方針で回しているため、Snapshot Testに助けられた、という機会が多くなかったのです。

AWS CDKのテストはどこまで書くべきか?

万能なモノサシというのはないと思うので、例として私達のチームでの運用について紹介します。

私達のチームでは、CDKのテストをどこまで書くかのルールを現状定めていません。
現時点では、個人個人の裁量でテストコードを書いてもらって、レビューを通しながらテストひいてはチームとしてのナレッジを育てていくというスタイルにしています。

私個人としては、以下のように書き、進めるのがよいかなと思っています。あくまで現時点ですが。

  1. 浅めのアサーションテストを書き、CIを回す
  2. 開発環境に実際にデプロイして正しいことを確認する
  3. 変更やミスがあった箇所はアサーションを深めにしていく
  4. 不安な箇所も適宜、アサーションを深めにする

つまり、Fine-grained assertion Testに集中し、最初は最低限のテストを書き、必要に応じてテストを厚くしていくというアプローチです。

最初のステップの「浅め」のアサーションテストとは、具体的には以下のようなテストです。

mystack-test.ts
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { HogeStack } from "../lib/hoge-stack";

test("DynamoDB Table Created", () => {
  const app = new cdk.App();
  // WHEN
  const stack = new HogeStack(app, "MyTestStack");
  // THEN
  const template = Template.fromStack(stack);
  template.hasResourceProperties("AWS::DynamoDB::Table", {
    TableName: "hoge-table",
  });
});

重視しているのは、テストコードの中身よりも、最初から自動テスト・CIを組み込むという点です。テストコードがないところに後から追加する方が、最初に追加するよりもずっとハードルが高くなるためです。
私達は全てのCDKアプリケーションを1つのGitHubリポジトリで管理する、いわゆる「モノレポ」構成ですが、GitHub Actionsの on.push.paths フィルターを使って、変更のあったCDKアプリケーションだけがテスト&ビルドされるようにしています。
ものによってはCDKスタックだけでなく、Lambda関数およびそのテストコードを含むものもありますが、それら全てで例外なく以下のコマンドでテストが走らせられるようにしています。

npm install
npm test

上記の準備が整えば、実際にデプロイして正しいことが確認できるようになります。

極端な話ですが、「デプロイして動くのを確認するまでは不安」という考え方があるため、少なくとも最初の1回はテスト環境で確認をします。
その際に、変更やミスがあればその部分のアサーションを追加したり、不安な箇所も適宜テストを追加することで、タイパのよいテストコードに育っていくと考えています。

「デプロイして動くのを確認する」部分を自動化するのに、CDK Integration Testや、サードパーティ製のテストツールとしてgoss, inspec, awspecなどがあります。これらは、試験的な評価を行っただけで、まだ実プロジェクトで導入はできていませんが、機会があれば、導入を検討したいと思っています。

おわりに

AWS CDKにおけるテストについて、取り組みや考えを紹介していきました。
あくまで現時点でのものであり、時間が経ったら考え方や戦略が変わっているかもしれませんし、ツール側の進化によって全く状況が変わっているかもしれません。

CDKのリリースが2018年で、Integration Testのためのinteg-tests モジュールがまだ実験段階であることからも、これからも色々な進化・アップデートが生まれてくる領域だと思うので、今後も要注目ですね。
それとCDK conferenceも7月にありますし、様々なプロジェクトでの事例から、これからも色々と学んでいきたいと思います!

脚注
  1. 今では既存リソースをCDKアプリケーション化する手段も提供されてはいますが、変更の機会も少なく、変更作業も手動で事足りており、CDK管理に至っていません。 ↩︎

  2. CDK v1の時はドキュメントを参照して実験的なモジュールかどうかを気にする必要がありましたが、CDK v2になってから実験的なモジュールは分離され、意図せず使用してしまう可能性はほとんどなくなりました。 ↩︎

Cariot開発チーム

Discussion