AWS CDKテスト戦略|AssertionsとSnapshotで自動化

CDKテストの最新手法を解説。Assertions APIとSnapshotテストでIaC品質を自動化。2026年のベストプラクティスを実装例付きで紹介します。

AWS CDKテスト戦略完全ガイド2026|AssertionsとSnapshotテストで品質を自動化

はじめに:2026年のCDKテスト自動化の現状

2026年時点で、Infrastructure as Code(IaC)の重要性はますます高まっており、AWS CDKはそのデファクトスタンダードとなっています。しかし、CDKコードそのもののテストを適切に実施していないプロジェクトが少なくありません。

2025年のAWS CDK 2.130以降では、テストフレームワークの機能が大幅に強化されました。特にAssertions APIとSnapshotテストの組み合わせにより、単なるクラウドリソース検証にとどまらず、リグレッション防止や意図しない構成変更の早期発見が可能になっています。

この記事では、2026年現在の最新なCDKテスト戦略を、実装例を交えて解説します。

CDKテストの3層構造とAssertions APIの位置付け

AWS CDKのテストは、大きく3つのレイヤーに分けられます。2026年のベストプラクティスでは、これら3層を統合的に設計することが求められます。

flowchart TD
    A[Unit Test: Assertions API] --> B[Integration Test: Stack合成テスト]
    B --> C[E2E Test: 実AWS環境デプロイ]
    A --> D[高速フィードバック]
    B --> E[リグレッション検出]
    C --> F[本番環境検証]
    
    style A fill:#e1f5ff
    style B fill:#f3e5f5
    style C fill:#ffe0b2

これまでのCDKテストは、Assertions APIを使った Unit Testに集中していました。しかし2026年現在、Snapshotテストとの組み合わせにより、以下が実現可能になっています:

テストレイヤー目的ツール実行時間
Unit Testリソース定義の正確性Assertions API< 1秒
Snapshot Test構成変更の意図的/非意図的を区別Snapshot + Assertions< 2秒
Integration TestStack合成の成功確認CDK assert< 5秒
E2E Test実環境でのリソース動作AWS SDK/CLI分単位

Assertions APIを用いた Unit Testの実装

基本的な使い方:テンプレート検証

2026年のAWS CDK最新バージョン(v2.150以上)では、Assertions APIが完全に成熟しています。以下は、Lambda関数とVPC内のRDSを含むスタックのテスト例です。

import * as cdk from 'aws-cdk-lib'
import { Template, Match } from 'aws-cdk-lib/assertions'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as rds from 'aws-cdk-lib/aws-rds'
import * as ec2 from 'aws-cdk-lib/aws-ec2'

// テスト対象のスタック
class DataProcessingStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new ec2.Vpc(this, 'VPC', {
      maxAzs: 3,
      cidrMask: 24,
    })

    const dbCluster = new rds.DatabaseCluster(this, 'DBCluster', {
      engine: rds.DatabaseClusterEngine.auroraPostgres({
        version: rds.AuroraPostgresEngineVersion.VER_16_2,
      }),
      vpc,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })

    const handler = new lambda.Function(this, 'Handler', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      vpc,
      environment: {
        DB_ENDPOINT: dbCluster.clusterEndpoint.hostname,
      },
    })
  }
}

// Assertions APIテスト
test('VPC内にRDSクラスタが作成される', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  // RDSクラスタの存在を確認
  template.hasResourceProperties('AWS::RDS::DBCluster', {
    Engine: 'aurora-postgresql',
  })
})

// Lambda関数の環境変数を確認
test('Lambda関数にDB_ENDPOINTが設定される', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  template.hasResourceProperties('AWS::Lambda::Function', {
    Environment: Match.objectLike({
      Variables: Match.objectLike({
        DB_ENDPOINT: Match.anyValue(),
      }),
    }),
  })
})

// VPCサブネット数の確認
test('VPCに3つのAZにまたがるサブネットが作成される', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  template.resourceCountIs('AWS::EC2::Subnet', 3)
})

高度なマッチング:Match ユーティリティ

2026年のAssertions APIでは、Match クラスが強化されており、複雑な条件マッチングが可能になっています。

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

test('セキュリティグループのルールを詳細検証', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  // ポート5432へのインバウンド許可を確認
  template.hasResourceProperties('AWS::EC2::SecurityGroup', {
    SecurityGroupIngress: [
      Match.objectLike({
        IpProtocol: 'tcp',
        FromPort: 5432,
        ToPort: 5432,
        CidrIp: '10.0.0.0/8',
      }),
    ],
  })
})

test('Lambda実行ロールの信頼ポリシーを検証', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  // AssumeRolePolicyDocumentを検証(複数の条件)
  template.hasResourceProperties('AWS::IAM::Role', {
    AssumeRolePolicyDocument: Match.objectLike({
      Statement: [
        Match.objectLike({
          Effect: 'Allow',
          Principal: {
            Service: 'lambda.amazonaws.com',
          },
          Action: 'sts:AssumeRole',
        }),
      ],
    }),
  })
})

Snapshot テストによるリグレッション防止

Snapshotテストの概念と利点

2026年現在、SnapshotテストはCDKテストの革新的な手法として認識されています。以下の理由により重要です:

  1. 意図しない変更の早期発見:構成が予期せず変わった場合、スナップショット比較で即座に検出
  2. レビュー効率の向上:PR時にテンプレート全体の差分を視覚化
  3. リグレッション防止:古いバージョンのAWS CDKやライブラリへのダウングレード時の変更を検出
  4. ドキュメント化:生成されたCloudFormationテンプレートが自動的にドキュメント化される

Snapshotテストの実装

import { Template } from 'aws-cdk-lib/assertions'
import * as fs from 'fs'
import * as path from 'path'

test('スタックのCloudFormationテンプレートがスナップショットと一致する', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')
  const template = Template.fromStack(stack)

  // テンプレートをJSON形式で取得
  const templateJson = JSON.stringify(template, null, 2)

  // スナップショットディレクトリに保存
  const snapshotDir = path.join(__dirname, '__snapshots__')
  const snapshotFile = path.join(snapshotDir, 'data-processing-stack.json')

  if (!fs.existsSync(snapshotDir)) {
    fs.mkdirSync(snapshotDir, { recursive: true })
  }

  // 初回実行時はスナップショットを作成、以降は比較
  if (fs.existsSync(snapshotFile)) {
    const savedSnapshot = fs.readFileSync(snapshotFile, 'utf-8')
    expect(templateJson).toBe(savedSnapshot)
  } else {
    fs.writeFileSync(snapshotFile, templateJson)
  }
})

Jestネイティブのスナップショット機能との統合

2026年のベストプラクティスでは、Jest標準のスナップショット機能とCDKを統合することが一般的です:

import '@aws-cdk/assert'

test('DataProcessingStackのテンプレートスナップショット', () => {
  const app = new cdk.App()
  const stack = new DataProcessingStack(app, 'TestStack')

  // Jest スナップショット機能を使用
  expect(JSON.parse(JSON.stringify(stack))).toMatchSnapshot()
})

実行時に --updateSnapshot フラグを使用することで、承認されたスナップショットを更新できます:

# スナップショットの初回作成
npm test -- --updateSnapshot

# スナップショットとの比較テスト実行
npm test

AssertionsとSnapshotの組み合わせ:実装パターン

2026年の実務では、以下のような統合テスト戦略が推奨されています。

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

// テスト対象:複数のマイクロサービスを含むスタック
class MicroservicesStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string) {
    super(scope, id)

    // ECS Fargate サービス 1: API
    const apiCluster = new ecs.Cluster(this, 'APICluster')
    const apiService = new ecsPatterns.ApplicationLoadBalancedFargateService(
      this,
      'APIService',
      {
        cluster: apiCluster,
        image: ecs.ContainerImage.fromRegistry('my-api:latest'),
        desiredCount: 2,
      }
    )

    // ECS Fargate サービス 2: Worker
    const workerCluster = new ecs.Cluster(this, 'WorkerCluster')
    const workerService = new ecs.FargateService(this, 'WorkerService', {
      cluster: workerCluster,
      taskDefinition: new ecs.FargateTaskDefinition(this, 'WorkerTask', {
        memoryLimitMiB: 512,
        cpu: 256,
      }),
    })

    // SQS キュー
    const queue = new sqs.Queue(this, 'JobQueue', {
      visibilityTimeout: cdk.Duration.seconds(300),
      retentionPeriod: cdk.Duration.days(14),
    })
  }
}

// ===== テストスイート =====
describe('MicroservicesStack', () => {
  let stack: MicroservicesStack
  let template: Template

  beforeEach(() => {
    const app = new cdk.App()
    stack = new MicroservicesStack(app, 'TestStack')
    template = Template.fromStack(stack)
  })

  describe('Assertions による詳細検証', () => {
    test('2つのECSクラスタが作成される', () => {
      template.resourceCountIs('AWS::ECS::Cluster', 2)
    })

    test('APIサービスの望ましいカウントが2である', () => {
      template.hasResourceProperties(
        'AWS::ECS::Service',
        Match.objectLike({
          DesiredCount: 2,
        })
      )
    })

    test('SQSキューの表示タイムアウトが300秒である', () => {
      template.hasResourceProperties(
        'AWS::SQS::Queue',
        Match.objectLike({
          VisibilityTimeout: 300,
          MessageRetentionPeriod: 1209600, // 14日
        })
      )
    })

    test('ALBが存在する', () => {
      template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1)
    })
  })

  describe('Snapshot による全体構成の検証', () => {
    test('スタック全体がスナップショットと一致する', () => {
      expect(stack).toMatchSnapshot()
    })

    test('CloudFormationテンプレート全体がスナップショットと一致する', () => {
      const templateObj = JSON.parse(JSON.stringify(template.toJSON()))
      expect(templateObj).toMatchSnapshot()
    })
  })

  describe('リグレッション検出シナリオ', () => {
    test('ECS タスク定義にログドライバーが設定されている', () => {
      template.hasResourceProperties(
        'AWS::ECS::TaskDefinition',
        Match.objectLike({
          ContainerDefinitions: [
            Match.objectLike({
              LogConfiguration: Match.objectLike({
                LogDriver: 'awslogs',
              }),
            }),
          ],
        })
      )
    })

    test('SQSキューの暗号化が有効である', () => {
      template.hasResourceProperties(
        'AWS::SQS::Queue',
        Match.objectLike({
          KmsMasterKeyId: Match.anyValue(),
        })
      )
    })
  })
})

AWS構成図:マルチレイヤー構成のテスト対象

以下は、上記のマイクロサービススタックがデプロイされたAWS環境の構成図です。CDKテストでは、この全体的なリソース配置が正確に生成されることを検証します。

graph TB
    subgraph "VPC"
        subgraph "Availability Zone 1"
            ALB["🔀 Application Load Balancer"]
            API1["🐳 ECS Fargate Task<br/>(API)"]
            Worker1["🐳 ECS Fargate Task<br/>(Worker)"]
            SG1["🔐 Security Group"]
        end
        
        subgraph "Availability Zone 2"
            API2["🐳 ECS Fargate Task<br/>(API)"]
            Worker2["🐳 ECS Fargate Task<br/>(Worker)"]
            SG2["🔐 Security Group"]
        end
        
        subgraph "Availability Zone 3"
            API3["🐳 ECS Fargate Task<br/>(API)"]
            SG3["🔐 Security Group"]
        end
    end
    
    subgraph "Queue Service"
        SQS["📬 SQS Queue<br/>(JobQueue)"]
    end
    
    ALB -->|Route| API1
    ALB -->|Route| API2
    ALB -->|Route| API3
    API1 --> SG1
    API2 --> SG2
    API3 --> SG3
    Worker1 -->|Consume| SQS
    Worker2 -->|Consume| SQS
    
    style ALB fill:#FF9900
    style API1 fill:#FF9900
    style API2 fill:#FF9900
    style API3 fill:#FF9900
    style Worker1 fill:#FF9900
    style Worker2 fill:#FF9900
    style SQS fill:#FF9900

2026年のベストプラクティス:テスト設計パターン

パターン1:段階的なテスト設計

2026年のCDK運用では、以下の段階的なテスト設計が推奨されています:

// ステップ 1: 基本的なリソース存在確認
test('必須リソースが存在する', () => {
  template.resourceCountIs('AWS::SQS::Queue', 1)
  template.resourceCountIs('AWS::ECS::Cluster', 2)
})

// ステップ 2: リソース設定の詳細確認
test('SQSキューが適切に設定されている', () => {
  template.hasResourceProperties(
    'AWS::SQS::Queue',
    Match.objectLike({
      VisibilityTimeout: 300,
      MessageRetentionPeriod: 1209600,
      KmsMasterKeyId: Match.anyValue(),
    })
  )
})

// ステップ 3: リソース間の関連性を確認
test('ECSタスクがSQSキューにアクセス可能なIAMロールを持つ', () => {
  template.hasResourceProperties(
    'AWS::IAM::Policy',
    Match.objectLike({
      PolicyDocument: Match.objectLike({
        Statement: [
          Match.objectLike({
            Effect: 'Allow',
            Action: ['sqs:ReceiveMessage', 'sqs:DeleteMessage'],
            Resource: Match.anyValue(),
          }),
        ],
      }),
    })
  )
})

// ステップ 4: スナップショットによる全体構成の固定化
test('スタック全体の構成が安定している', () => {
  expect(stack).toMatchSnapshot()
})

パターン2:セキュリティ検証テスト

2026年の本番環境では、セキュリティ検証が必須です:

test('すべてのECSタスクがログドライバーを設定している', () => {
  const template = Template.fromStack(stack)
  template.allResources('AWS::ECS::TaskDefinition', (resource: any) => {
    resource.Properties.ContainerDefinitions.forEach((container: any) => {
      expect(container.LogConfiguration).toBeDefined()
      expect(container.LogConfiguration.LogDriver).toBe('awslogs')
    })
  })
})

test('すべてのSQSキューが暗号化されている', () => {
  const template = Template.fromStack(stack)
  template.allResources('AWS::SQS::Queue', (resource: any) => {
    expect(resource.Properties.KmsMasterKeyId).toBeDefined()
  })
})

test('IAMロールの信頼ポリシーがAWSサービスのみに制限されている', () => {
  template.allResources('AWS::IAM::Role', (resource: any) => {
    const statement = resource.Properties.AssumeRolePolicyDocument.Statement[0]
    expect(statement.Principal.Service).toBeDefined()
    // 特定のサービスのみを許可
    const allowedServices = ['ecs-tasks.amazonaws.com', 'lambda.amazonaws.com']
    expect(allowedServices).toContain(statement.Principal.Service)
  })
})

パターン3:リグレッション防止テスト

test('CDK v2のリグレッション:ECSサービスの必須プロパティ', () => {
  // 2026年のAWS CDK v2.150では、ECSサービスに`enableECSManagedTags`が
  // デフォルトで有効になった。この変更を検出
  template.hasResourceProperties(
    'AWS::ECS::Service',
    Match.objectLike({
      EnableECSManagedTags: true,
    })
  )
})

test('ライブラリアップグレード後の構成確認', () => {
  // 例:aws-ec2 v3.5で、デフォルトのセキュリティグループルールが変更された
  // これをスナップショットテストで検出
  expect(stack).toMatchSnapshot()
})

テストスイートの構成と実行

2026年の推奨テスト構成を示します:

{
  "scripts": {
    "test": "jest --coverage",
    "test:watch": "jest --watch",
    "test:update-snapshot": "jest --updateSnapshot",
    "test:assertions": "jest --testNamePattern='Assertions'",
    "test:snapshot": "jest --testNamePattern='Snapshot'",
    "test:ci": "jest --ci --coverage --maxWorkers=4"
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "roots": [
      "<rootDir>/test"
    ],
    "testMatch": [
      "**/__tests__/**/*.test.ts",
      "**/?(*.)+(spec|test).ts"
    ],
    "collectCoverageFrom": [
      "lib/**/*.ts",
      "!lib/**/*.d.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 70,
        "functions": 70,
        "lines": 70,
        "statements": 70
      }
    }
  }
}

テスト実行時間とカバレッジの目安

graph LR
    A["Unit Tests<br/>(Assertions)"]
    B["Snapshot Tests"]
    C["Integration Tests"]
    D["E2E Tests"]
    
    A -->|<1s| A1["✅ 100+ tests"]
    B -->|<2s| B1["✅ 20+ tests"]
    C -->|<5s| C1["✅ 10+ tests"]
    D -->|>60s| D1["⚠️ 5+ tests"]
    
    E["Coverage Goal"]
    E -->|Statements| E1["80%+"]
    E -->|Branches| E2["75%+"]
    E -->|Functions| E3["80%+"]
    
    style A fill:#c8e6c9
    style B fill:#bbdefb
    style C fill:#ffe0b2
    style D fill:#ffccbc
    style E1 fill:#fff9c4

トラブルシューティング:よくある問題と対策

問題1:Snapshotテストが頻繁に失敗する

原因:ランダムな物理ID生成やタイムスタンプが含まれている

対策

// スナップショットを実行する前に、非決定的な値をマスク
const masked = JSON.stringify(stack, (key, value) => {
  if (key === 'PhysicalResourceId' || key === 'CreationTime') {
    return '[MASKED]'
  }
  return value
})
expect(masked).toMatchSnapshot()

問題2:テスト実行が遅い

原因:テストごとにスタック全体を再構築している

対策

describe('MicroservicesStack', () => {
  let stack: MicroservicesStack
  let template: Template

  // beforeAll で一度だけ実行
  beforeAll(() => {
    const app = new cdk.App()
    stack = new MicroservicesStack(app, 'TestStack')
    template = Template.fromStack(stack)
  })

  test('...', () => {
    // 共有されたスタックとテンプレートを使用
  })
})

問題3:Assertions APIが期待する値を見つけられない

原因:テンプレート内の値の形式や位置が異なっている

対策:デバッグで実際のテンプレートを確認

test('デバッグ:テンプレート全体を確認', () => {
  console.log(JSON.stringify(template.toJSON(), null, 2))
  // 実際の構造を確認してAssertionsを調整
})

2026年のCI/CD統合例

# GitHub Actions ワークフロー例
name: CDK Tests

on:
  pull_request:
    paths:
      - 'lib/**'
      - 'test/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - run: npm ci
      
      - run: npm run test:assertions
        name: Run Unit Tests
      
      - run: npm run test:snapshot
        name: Run Snapshot Tests
      
      - run: npm run test -- --coverage
        name: Generate Coverage Report
      
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
      
      - name: Comment Coverage on PR
        if: github.event_name == 'pull_request'
        uses: romeovs/lcov-reporter-action@v0.3.1
        with:
          lcov-file: ./coverage/lcov.info

まとめ:2026年のCDKテスト戦略

2026年現在、AWS CDKのテスト自動化は以下の要点を押さえることが重要です:

実装項目推奨事項
Assertions APIリソース存在確認と詳細プロパティ検証に使用
Snapshot Testリグレッション防止と構成の意図的変更追跡に使用
統合テストStack合成成功確認とリソース間の依存関係検証
E2E テスト本番環境相当での機能確認(本番前段階)
カバレッジ目標70~80%以上(段階的な向上)
実行時間ローカル:< 10秒、CI:< 30秒を目標
セキュリティテストIaC品質と同等の重要度で実施

これらの実装により、IaC品質の自動化が実現され、本番環境への展開前にリスクを大幅に低減できます。

U

Untanbaby

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

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

関連記事