CDKで本番トラブルを防ぐ。AssertionsとSnapshotテストで3年間守ってきた実装パターン

CDK歴3年で何度も本番障害を防げたテスト戦略。AssertionsとSnapshotテストの使い分けから実装例まで、実務で本当に効いた知見を共有します。

CDKテストで本番トラブルを防ぐまでの道のり

先日プロジェクトで、デプロイ直前に設定ミスが検出されて事なきを得た。その時思ったのは「もしテストがなかったら本番で火が噴いてた」ということ。うちのチームではCDKをガリガリ書いてるんだけど、正直な話、最初は「インフラコードなんてテストいるか?」って感じだった。でも3年やってみたら、むしろテストがないと怖くて新機能を追加できなくなる。

CDKにはAssertionsとスナップショットテストという2つの主要なテスト方式がある。これらを組み合わせることで、想像以上に多くのバグを開発段階で発見できる。今回は実装例を交えながら、実務で効いた知見を共有したい。

CDK AssertionsとSnapshotの使い分け

正直最初、この2つの違いがよく分からなかった。でも半年運用すると、それぞれの役割が明確になる。

Assertions は「このIAMロールにこの権限があるはずだ」「このセキュリティグループはこのポートを開いているはずだ」みたいな、特定の条件を明示的にテスト する方式。つまり、生成されたCloudFormationテンプレートに対して、あるべき状態を検証する。

Snapshot は「このテンプレート全体が前回と同じ形だ」を確認する方式。個別条件ではなく、構造全体の変化を追跡 する。変更を加えたときに「意図しない副作用が出てないか」を一目で確認できるんだ。

個人的には、Assertionsで「守るべき重要なポイント」をしっかり押さえて、スナップショットで「うっかり変わってしまった部分」をキャッチするという組み合わせが最強だと感じてる。

実装例:AssertionsでセキュリティをCheck

実際に動かしてみた例をいくつか見てみよう。

import { Template, Match } from 'aws-cdk-lib/assertions';
import { Stack } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // VPC作成
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      cidr: '10.0.0.0/16',
      maxAzs: 3,
    });

    // セキュリティグループ作成
    const sg = new ec2.SecurityGroup(this, 'MySecurityGroup', {
      vpc,
      allowAllOutbound: false,
    });

    sg.addIngressRule(
      ec2.Peer.ipv4('0.0.0.0/0'),
      ec2.Port.tcp(443),
      'Allow HTTPS'
    );
  }
}

// テストコード
import * as cdk from 'aws-cdk-lib';

const stack = new MyStack(new cdk.App(), 'test-stack');
const template = Template.fromStack(stack);

// Assertionで特定のリソースを検証
test('SecurityGroup has HTTPS ingress rule', () => {
  // SecurityGroupが存在することを確認
  template.hasResourceProperties('AWS::EC2::SecurityGroup', {
    SecurityGroupIngress: Match.arrayWith([
      Match.objectLike({
        IpProtocol: 'tcp',
        FromPort: 443,
        ToPort: 443,
        CidrIp: '0.0.0.0/0',
      }),
    ]),
  });
});

// 複数の条件を同時にチェック
test('VPC has expected CIDR and AZs', () => {
  template.resourceCountIs('AWS::EC2::Subnet', 3); // 3つのAZ = 3つのSubnet
  template.hasResourceProperties('AWS::EC2::VPC', {
    CidrBlock: '10.0.0.0/16',
  });
});

// アウトバウンドトラフィックが明示的に制限されていることを確認
test('SecurityGroup denies outbound by default', () => {
  template.hasResourceProperties('AWS::EC2::SecurityGroup', {
    SecurityGroupEgress: Match.absent(),
    // またはSecurityGroupEgressが空の配列であることを確認
  });
});

実際にこれ走らせると、意図した設定がちゃんと生成されているか一発でわかる。特に複雑なセキュリティ設定やIAM権限周りは、Assertionで明示的にテストしておくと、後からハマる確率がぐっと減るんだよね。

スナップショットテストで全体の変化を追跡

次にスナップショット。これはCDKの全体的な変更を一気に確認する方式だ。

import { Template, Match } from 'aws-cdk-lib/assertions';

test('Stack template snapshot', () => {
  const stack = new MyStack(new cdk.App(), 'test-stack');
  const template = Template.fromStack(stack);
  
  // テンプレート全体のスナップショットを取得
  expect(template.toJSON()).toMatchSnapshot();
});

この1行で、CloudFormationテンプレート全体が前回と同じかどうかを確認できる。初回実行時は__snapshots__ディレクトリにスナップショットが保存されて、以降の実行では変更を検出する。

例えば、誰かが「ちょっとセキュリティグループの設定変えました」って書いたコードを追加したら、テストが落ちる。そしたらそのスナップショットファイルを見ると「あ、こうなったんだ」って一目瞭然。予期しない変更なら修正できるし、意図した変更なら-uフラグで更新できるわけだ。

# テスト実行
npm test

# 変更が意図したものなら更新
npm test -- -u

これ地味だけど、めちゃ便利。チームの誰かが無意識に構成を変えてしまうのを防げる。

AWS構成図とテスト戦略の関係

うちのチームでテストしてる構成をMermaidで示すと、こんな感じになる。

graph TB
  subgraph dev["Development Account"]
    subgraph vpc1["VPC (10.0.0.0/16)"]
      subgraph az1["AZ1"]
        sub1["Subnet 10.0.1.0/24"]
      end
      subgraph az2["AZ2"]
        sub2["Subnet 10.0.2.0/24"]
      end
      subgraph az3["AZ3"]
        sub3["Subnet 10.0.3.0/24"]
      end
    end
    
    alb["Application Load Balancer"]
    sg["SecurityGroup:HTTPS Only"]
    
    sub1 --> alb
    sub2 --> alb
    sub3 --> alb
    alb --> sg
  end
  
  subgraph test["CDK Test Suite"]
    assert1["Assertion: VPC CIDR"]
    assert2["Assertion: SecurityGroup Rules"]
    assert3["Assertion: Subnet Count"]
    snap["Snapshot: Full Template"]
  end
  
  dev --> test
  assert1 --> result["Test Result"]
  assert2 --> result
  assert3 --> result
  snap --> result

こういう構成をコードで定義するわけだけど、テストがあるとこの構成が本当に意図通りに作られているか、デプロイ前に検証できるんだ。

実務で気づいたテスト戦略のコツ

正直まだ完璧じゃないけど、3年運用してると「これは有効」ってパターンが見えてくる。

重要なポイントはAssertionで、全体はSnapshotで という原則にしてる。例えば、本番環境のセキュリティグループは「ここは絶対に変わってはいけない」という部分があるじゃない。そういう部分をAssertionで明示的にテストする。一方、スナップショットはCI/CDパイプラインで自動化して、意図しない変更を検出する防衛ラインにするわけだ。

// 本番環境の絶対守るべき部分
test('Production SecurityGroup is locked', () => {
  const prodStack = new ProductionStack(new cdk.App(), 'prod');
  const template = Template.fromStack(prodStack);
  
  // HTTPは許可しない(HTTPSのみ)
  template.resourceCountIs('AWS::EC2::SecurityGroupIngress', 1);
  template.hasResourceProperties('AWS::EC2::SecurityGroupIngress', {
    IpProtocol: 'tcp',
    FromPort: 443,
    ToPort: 443,
  });
  
  // パブリックIPは割り当てない
  template.resourceCountIs('AWS::EC2::Instance', 0);
  // またはAssociatePublicIpAddressがfalseであることを確認
});

// 全体の構成変化を監視
test('Production stack matches snapshot', () => {
  const prodStack = new ProductionStack(new cdk.App(), 'prod');
  const template = Template.fromStack(prodStack);
  expect(template.toJSON()).toMatchSnapshot();
});

これをCI/CDに組み込むと、デプロイ前に「あ、設定が勝手に変わってる」って気づける。実際、このおかげでセキュリティグループの設定漏れを本番デプロイ前に防いだことが何度もあるんだ。

テストと本番環境のズレを埋める

ここで一つ気をつけなきゃいけないのが、テストが通ったからといって本番でも動くとは限らないってこと。CDKのテストはCloudFormationテンプレートの検証なので、ネットワーク接続とか、依存リソースの存在とか、そういった実行時の問題は検出できない。

だから、うちのチームでは「ステージング環境で本当にデプロイできるか試す」というステップをCI/CDに入れてる。

# CI/CD Pipeline
stages:
  - test:cdk
    script: npm test
  - deploy:staging
    script: cdk deploy --require-approval never --profile staging
  - integration:test
    script: npm run test:integration
  - deploy:production
    script: cdk deploy --require-approval never --profile production

Assertionで設定の正確性を、スナップショットで構造の一貫性を、そして実際のデプロイでランタイムの動作を確認する。この3段構え。

Assertionで引っかかるよくある落とし穴

個人的に何度も引っかかったパターンをいくつか紹介しよう。

1. 順序依存

Assertionで配列の順序を気にしすぎるとテストが脆くなる。Match.arrayWith() を使って、存在することだけを確認するほうが堅牢だ。

// これはダメ。順序が完全に一致しないと失敗
template.hasResourceProperties('AWS::S3::Bucket', {
  Tags: [
    { Key: 'Name', Value: 'my-bucket' },
    { Key: 'Environment', Value: 'prod' },
  ],
});

// これが正解。要素が存在すればOK
template.hasResourceProperties('AWS::S3::Bucket', {
  Tags: Match.arrayWith([
    Match.objectLike({ Key: 'Name', Value: 'my-bucket' }),
    Match.objectLike({ Key: 'Environment', Value: 'prod' }),
  ]),
});

2. デフォルト値の落とし穴

CDKは多くの設定でデフォルト値を自動的に生成する。テストで予期しない値が出現することがある。

// VPCを作成するとデフォルトでNATゲートウェイが作られる
const vpc = new ec2.Vpc(this, 'MyVpc');
// テストでは想定より多くのNATゲートウェイが生成されていることに気づく
template.resourceCountIs('AWS::EC2::NatGateway', 3); // 想定外?

3. リソースの参照と依存関係

リソースが別のリソースを参照する場合、テンプレート内では相対参照(Ref、GetAtt)になる。その形式を理解してないとテストが難しくなってしまう。

// テンプレート内では自動的にRef形式になる
template.hasResourceProperties('AWS::EC2::Instance', {
  SubnetId: Match.objectLike({
    Ref: 'MyVpcPublicSubnet1Subnet12345678',
  }),
});

スナップショット管理のベストプラクティス

スナップショットテストを本番運用すると、スナップショットファイル自体の管理が重要になってくる。

うちのチームで決めてることは:

  1. スナップショットファイルはGitで管理する。スナップショットの変更をコミットメッセージに残すことで、「なぜこのテンプレートが変わったのか」の履歴が残る。

  2. PRレビューでスナップショット変更をチェック。差分を見て「あ、こんなリソース増えてた?」って気づけることが多い。

  3. 定期的にスナップショットを整理する。古い環境設定の痕跡が残ってることがあるから、3ヶ月ごとに見直してる。

# スナップショット更新時は明示的にコミット
git add __snapshots__/
git commit -m "feat: Add NAT Gateway to staging VPC

Snapshot updated:
- Added AWS::EC2::NatGateway (1 instance)
- Route tables updated for NAT egress"

Matchの種類と使い分け

Matchには結構いろいろあって、最初は全部わかんなかった。けど使い分けがテストの質を左右する。

Match方式用途使う場面
Match.exact()完全一致タグ値、ポート番号など絶対変わらない値
Match.objectLike()部分一致(特定プロパティのみ確認)リソース参照、オブジェクト構造の確認
Match.arrayWith()配列に要素が含まれる(順序無視)セキュリティグループルール、権限一覧
Match.stringLikeRegexp()正規表現マッチARN、リソース名のパターン確認
Match.not()特定の値を含まないHTTPの禁止、パブリックIP割り当てなしの確認
Match.anyValue()任意の値(型問わず)値の内容は問わない、存在だけ確認したい時

実務では、セキュリティ関連はMatch.exact()で厳密に、一般的な構成はMatch.objectLike()で柔軟にするって感じ。

テストの実行パフォーマンス

Assertionは比較的高速なんだけど、スナップショットテストが増えるとテスト全体が遅くなってくる。うちのチームでは開発時間を短縮するために工夫をしてる。

  • 開発中: 特定のテストだけ実行 npm test -- SecurityGroup
  • CI/CD: 全テスト実行
  • pre-commit: 軽いテストのみ

こういう分け方をしてるわけだ。

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:security": "jest --testNamePattern=Security",
    "test:snapshot-update": "jest -u"
  }
}

まとめ

CDKでインフラコードを書く以上、テストは必須だと3年運用して実感してる。

  • Assertionsで「絶対守るべき部分」をロック。セキュリティ、権限、重要な設定値が本当に意図通りに生成されているかの確認。
  • Snapshotで「全体の予期しない変化」を検出。リソースの増減、構成の副作用をキャッチして、意図しない変更を防ぐ。
  • CI/CDパイプラインに組み込む。本番デプロイ前のセーフティネットとして機能する。
  • ステージングでの実デプロイ検証も併用。テンプレートの正確性と実行時の動作は別物だから両方必要。
  • チーム運用なら、スナップショットファイルの変更をしっかりレビュー。テンプレート変更の履歴を見える化することが重要。

これらを組み合わせると、本番環境のトラブルをかなり事前に防げる。正直、以前は「設定ミスして本番で気づく」みたいなことが何度もあったけど、テストを厳密にしてからはそういうケースがぐっと減った。

次のアクションとしては、まず既存のCDKスタックに簡単なAssertionテストを3〜5個追加してみることをお勧めする。セキュリティグループの設定、IAMロールの権限、VPCのCIDR。こういった「絶対に変わってはいけない部分」をテストすることから始めると、テストの価値を一番実感できると思うよ。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事