- 1 AWS ConfigとTerraformパラメーターシートを突合する単体テスト自動化 — tflint/plan/Config 3層パイプライン
- 1.1 1. この記事について
- 1.2 2. 業務背景: 構築後の突合テストの実務価値
- 1.2.1 2-1. 「設計時の突合」と「構築後の突合」の違い
- 1.2.2 2-2. エンタープライズ案件における受入試験書の現実
- 1.2.3 2-3. 手動突合の工数と「500 チェック問題」
- 1.2.4 2-4. なぜ CI/CD ではなく「単体テスト」として位置付けるのか
- 1.2.5 2-5. 運用フロー Before/After
- 1.2.6 2-6. AWS Config を初めて使う方へ
- 1.2.7 2-7. 本記事で扱わない領域の明示
- 1.2.8 2-8. 受入試験自動化の副次効果
- 1.2.9 2-9. 本章のまとめと次章への接続
- 1.2.10 2-10. 「受入試験書の自動化」というゴールを再確認する
- 1.3 3. AWS Config 基礎 — Recorder / Aggregator / Advanced Query
- 1.4 4. tflint による静的検査 — パイプライン入口のゲート
- 1.5 Section 5. terraform plan JSON からの期待値抽出(第1弾スキル再利用)
- 1.6 Section 6. AWS Config Advanced Query による現状値取得
- 1.7 7. 突合ロジック — 型正規化・差分判定・Verdict 設計
- 1.7.1 7-1. 設計思想: 型正規化を挟む3段構成
- 1.7.2 7-2. Verdict enum
- 1.7.3 7-3. DiffRow dataclass
- 1.7.4 7-4. normalize_value() — 型正規化ユーティリティ
- 1.7.5 7-5. 型正規化ルール一覧表
- 1.7.6 7-6. compare() — 突合メイン関数
- 1.7.7 7-7. 欠測判定の設計
- 1.7.8 7-8. SG ingress/egress の特殊処理
- 1.7.9 7-9. exit code 設計
- 1.7.10 7-10. 動作確認: compare() を単独実行する
- 1.7.11 本セクションのまとめ
- 1.8 Section 8. pytest によるテスト構成 — モック・フィクスチャ・assertion 粒度
- 1.8.1 8-1. DiffRow / Verdict — Section 7 の dataclass 契約(再掲)
- 1.8.2 8-2. pytest 実行階層の全体像
- 1.8.3 8-3. fixture 設計 — conftest.py
- 1.8.4 8-4. assertion 粒度の使い分け
- 1.8.5 8-5. moto3 によるモック — ユニットテストと結合テストの使い分け
- 1.8.6 8-6. parametrize — resourceType 別テスト
- 1.8.7 8-7. skip 条件 — Config Recorder 未有効時
- 1.8.8 8-8. pytest 設定ファイル
- 1.8.9 8-9. テスト戦略まとめ
- 1.9 Section 9. マルチ環境対応 — environments/ 分離と環境別 drift レポート
- 1.10 Section 10. ハンズオン実行と成果物確認
- 1.11 Section 11. まとめと次の発展(CI/CD 組込み導線)
AWS ConfigとTerraformパラメーターシートを突合する単体テスト自動化 — tflint/plan/Config 3層パイプライン
AWS パラメーターシート自動化シリーズ
- 第1弾: Terraformコードから AWS パラメーターシート(Excel)を自動生成する
- 第2弾(本記事・最終回): AWS Config と Terraform パラメーターシートを突合する単体テスト自動化
前提知識(必読):
- 第1弾: Terraformコードから AWS パラメーターシート(Excel)を自動生成する (ID:1269) — 本記事の期待値抽出部はここで実装済のスキルを再利用
- Terraform基礎 (ID:1120) — init/plan/apply・変数
- Terraform実践 (ID:1208) — module/state/複数環境
- Python 3.11+・pytest 8.x・boto3・tflint 0.52+ の動作環境
- AWS Config Recorder が最低1環境で有効(未有効でもハンズオン前半は進行可)
関連シリーズ:
AWS×Terraform 複数人開発シリーズ(全3弾):
1. この記事について
1-1. 本記事で達成できること
この記事を最後まで読んで手を動かすと、次のものが手元に揃う。
- tflint → terraform plan → AWS Config Advanced Query を組み合わせた 3 層パイプライン
compare()関数:plan JSON(期待値)と Config 記録値(現状値)を突合し、差分をDiffRowリストで返す- pytest によるドリフト検知テストスイート:
pytest tests/test_drift.pyの 1 コマンドで受入検証が走る - Sheet2「差分一覧」を自動転記する Excel 出力:第1弾 Excel を受入証跡として完成させる
「構築が終わったあと、設計書(パラメーターシート)と実際の AWS 環境が一致しているか確認したい」——その要望を、pytest 単体テストとして実装する。本番環境への定期実行は行わない。構築完了後の受入検証フェーズで 1 回実行する ユースケースを想定している。

上の図が本記事の全体像だ。tflint による静的検査をゲートに置き、terraform plan JSON で期待値を抽出、AWS Config Advanced Query で現状値を取得、2 者を突合して pytest で差分を報告する。この 3 層が揃うことで、手動チェックに頼ってきたエンタープライズ受入試験が自動化される。
1-2. 本シリーズの全体構成と「最終回」の意味
本シリーズは 2 弾構成で、AWS 案件で繰り返し発生する「パラメーターシート管理」の煩雑さを段階的に解消する。
| 弾 | テーマ | 主な技術スタック | 概要 |
|---|---|---|---|
| 第1弾 | Terraform → Excel 自動生成 | Terraform 1.9 / Python 3.11 / openpyxl | plan JSON を解析し Excel パラメーターシートを生成 |
| 第2弾(本記事・最終回) | AWS Config との突合テスト自動化 | AWS Config / pytest / boto3 / tflint | 実環境の Config 記録と TF コードの差分を単体テストで検出 |
第1弾は「コードから仕様書を作る(設計フェーズ)」、第2弾は「仕様書と実環境のズレを検知する(検証フェーズ)」。2 弾合わせて 設計 → 検証のサイクルが完成する。
本記事を読み終えたとき、読者はこのサイクルをチームの標準プロセスとして組み込む準備が整っているはずだ。「Excel の作成 → pytest の実行 → NG 行をチームで確認 → TF コード修正 → 再実行」——これが最終形だ。
1-3. 第1弾の振り返り(未読者向け)
第1弾では次のパイプラインを実装した。
Terraform HCL → terraform plan -out=tfplan
→ terraform show -json tfplan → plan.json
→ tf_plan_parser.parse_plan()
→ openpyxl → param-sheet.xlsx(マルチ環境・2段ヘッダ)
核心は parse_plan() 関数で、plan JSON の resource_changes[].change.after を走査してリソースアドレスごとの属性値を抽出する。第1弾では コード記載値を期待値として Excel に書き出す ことが目標だった。
出力した Excel は ENV_SUBCOLS = ['期待値', '現状値', '判定'] の 3 列構成のうち、第1弾時点では「期待値」列のみ埋まった状態だ。第2弾では「現状値」を AWS Config から取得し、「判定」列に OK/NG/UNKNOWN を書き込んで Excel を完成させる。
第1弾の詳細は こちら(第1弾記事) を参照してほしい。本記事では parse_plan() や write_excel() の実装を再掲せず、第1弾のコード資産を import して再利用する前提 で話を進める。
1-4. 関連シリーズとの役割分担
本シリーズは、同じ Terraform を扱う複数人開発シリーズと直交補完の関係にある。
| 軸 | シリーズ | キャッチフレーズ |
|---|---|---|
| 継続デプロイ軸 | AWS×Terraform 複数人開発シリーズ(全3弾) | “TF コードを継続的に作って回す” |
| 設計整合検証軸 | 本シリーズ(全2弾・完結) | “作ったものを仕様書と突合して検証する” |
本記事では pytest を 単体テストとして実行する 設計に絞っており、CI/CD パイプラインへの組み込みはスコープ外とする。「定期実行したい」「プルリクエスト時に自動で走らせたい」という要望は 複数人開発シリーズ第2弾(GitHub Actions+OIDC) を参照してほしい。この役割分担は意図的なものだ。受入試験を自動化する価値は、CI 組み込みの前でも十分に大きい。
1-5. 対象読者とペルソナ
本記事は次のような読者を想定している。
主要ペルソナ: エンタープライズ DevOps / インフラエンジニア
- 第1弾で
parse_plan()と Excel 出力を実装済みで、「次は実環境と突合したい」と思っている - 金融・製造・公共系の AWS 案件で構築後の受入試験書作成に数日かけている
- pytest は知っているが AWS テスト自動化での活用経験はない
- AWS Config は名前を聞いたことはあるが、実際に使ったことはない(本記事の §3 で基礎から解説する)
副次ペルソナ: テックリード / QA エンジニア
- インフラ構築の受入証跡として「テスト実行ログ」を残したい
- 手動チェックリストを機械的に実行する仕組みを探している
- 差分管理を Excel ではなく pytest の PASS/FAIL で管理したい
前提知識チェックリスト
以下をすべて満たせば、本記事をスムーズに進められる。
[ ] 第1弾完了: param-sheet-tf-config-excel-generator.md を読んでコードを動かした
[ ] Terraform init / plan / apply の基本操作
[ ] Python 3.11+ の基礎(型ヒント・辞書操作)
[ ] pytest の基礎(test_xxx 関数・assert 文)
[ ] AWS CLI v2 の設定(aws configure 済み・Config Recorder は §3 で有効化手順を案内)
AWS Config 未経験の方へ
AWS Config は「実環境のリソース構成を記録し続けるサービス」だ。使ったことがなくても心配はいらない。§3「AWS Config 基礎」で Recorder の有効化から Advanced Query の実行まで丁寧に解説する。Config Recorder を有効化していない環境でも、§5 までの terraform plan 期待値抽出は動作する。
1-6. 本記事で登場するツール・バージョン一覧
| ツール | 本記事で使うバージョン | 役割 |
|---|---|---|
| Terraform | 1.9.x | plan JSON 生成・HCL 解析 |
| tflint | 0.52.x | HCL 静的検査(パイプライン入口ゲート) |
| Python | 3.11+ | パーサ・突合ロジック・Excel 出力 |
| pytest | 8.x | 単体テスト実行・PASS/FAIL 判定 |
| boto3 | 1.34+ | AWS Config Advanced Query 呼び出し |
| moto3 | 5.x | boto3 Config クライアントのモック(テスト用) |
| openpyxl | 3.1+ | Excel 生成(第1弾から継続利用) |
| AWS Config | — | 実環境のリソース構成記録・Advanced Query |
サンプルコードで使う AWS 環境の定数は以下で統一する(実際の環境では適宜置き換えること)。
AWSアカウントID : 123456789012
リージョン: ap-northeast-1
S3 バケット名: myorg-terraform-state
DynamoDB テーブル: terraform-state-lock
ARN 形式 : arn:aws:*:ap-northeast-1:123456789012:*
1-7. 本記事の構成とナビゲーション
11 のセクションで構成する。§1(本記事)→ §2(業務背景)→ §3(Config 基礎)→ §4(tflint)→ §5(期待値抽出)→ §6(現状値取得)→ §7(突合ロジック)→ §8(pytest 構成)→ §9(マルチ環境)→ §10(ハンズオン)→ §11(まとめ)の順に読み進めることを推奨する。
ただし、各セクションの依存関係は以下のとおりで、部分的に先読みも可能だ。
§1(背景) → §2(背景)
§3(Config 基礎)────────────────────────────────→ §6(Config fetch)
§4(tflint)─────────────────────────────────────→ §8(pytest に組込み)
§5(期待値抽出)────────────────────────────────→ §7(突合ロジック)
§6(現状値取得)────────────────────────────────→ §7(突合ロジック)
§7(compare)───────────────────────────────────→ §8(pytest)
§8(pytest)────────────────────────────────────→ §10(統合実行)
§9(マルチ環境)─(独立)──────────────────────→ §10(統合実行)
Config Recorder の有効化(§3-1)に時間がかかる場合、先に §4〜§5 を実施して期待値の抽出だけ先行することも可能だ。
1-8. 読者タイプ別の推奨読み方
読者の状況に応じて、読み進め方を変えることを推奨する。
パターン A: 第1弾を完了済みで、すぐに実装に入りたい
§1(ざっと)→ §2(ざっと)→ §3(Config 有効化手順のみ)→ §4 → §5 → §6 → §7 → §8 → §10
所要時間の目安: 4〜6 時間(ハンズオン含む)
§3 の AWS Config 概念説明(§3-2〜§3-3)は一度読んでから §6 を読むと理解が深まるが、
急いでいる場合は §6 に到達してから必要に応じて戻り読みするスタイルでも追える。
パターン B: Config を初めて使う・じっくり学びたい
§1 → §2 → §3(全読必須)→ §4 → §5 → §6 → §7 → §8 → §9 → §10 → §11
所要時間の目安: 8〜10 時間(ハンズオン含む)
§3 の Config 基礎を丁寧に読むことで、§6 の Advanced Query と §7 の突合ロジックへの
理解がスムーズになる。Config 未経験者はこちらを推奨する。
パターン C: コードだけ欲しい、解説文は不要
- §8 の
conftest.pyとtest_drift.pyから読む - 動作が理解できたら §7 の
compare()関数と §6 のconfig_fetch()を逆引き - §5 の
load_expected()wrapper で期待値の形式を確認
いずれのパターンでも、§2-4「なぜ CI/CD ではなく単体テストか」は一読することを勧める。
本記事の設計判断の根拠がここに集約されている。
1-9. ディレクトリ構成の予告
本記事で作成するファイルの構成を先に示しておく。第1弾からの継続性がわかるように、
第1弾で作成済みのファイルも含めて示す。
param-sheet-project/
├── environments/
│├── dev/
││├── main.tf
││├── variables.tf
││└── terraform.tfvars
│├── stg/
│└── prod/
├── tf_plan_parser.py ← 第1弾で作成済み(parse_plan() 実装)
├── tf_to_excel.py ← 第1弾で作成済み(write_excel() 実装)
├── param-sheet.xlsx ← 第1弾で出力済み(期待値列のみ埋まった状態)
│
├── config_fetcher.py ← 第2弾: AWS Config から現状値を取得
├── comparator.py ← 第2弾: compare() 関数・DiffRow・Verdict
├── tests/
│├── conftest.py← 第2弾: pytest fixture
│└── test_drift.py ← 第2弾: ドリフト検知テスト本体
├── .tflint.hcl ← 第2弾: tflint 設定
└── Makefile ← 第2弾: make drift-check / make drift-all
第1弾から継続して同じリポジトリで作業することを前提としているが、
第2弾のファイルだけを新しいディレクトリに置いて第1弾を pip install 経由で参照する構成も可能だ。
本記事では前者(同一リポジトリ)で解説する。
2. 業務背景: 構築後の突合テストの実務価値
2-1. 「設計時の突合」と「構築後の突合」の違い
第1弾は 設計フェーズの突合を自動化した。Terraform コードが確定した時点で parse_plan() を実行し、コード記載の期待値を Excel に書き出す。設計者がレビューし、顧客に提出するための一次資料を自動生成するユースケースだ。
本記事は 構築後の突合を自動化する。AWS 環境への terraform apply が完了した後、「コードに書いてあった設定が本当に反映されているか」を AWS Config の記録値と照合する。設計レビューではなく、受入試験・結合テストのフェーズに位置する。
| フェーズ | 突合の目的 | 使うデータ | 本シリーズでの担当 |
|---|---|---|---|
| 設計フェーズ | コードレビュー・顧客提出用 Excel | Terraform plan JSON | 第1弾 |
| 構築後フェーズ | 受入試験・設計整合確認 | AWS Config 記録値 | 第2弾(本記事) |
この区別は重要だ。第1弾の突合は「コードが正しいか」を確認する。第2弾の突合は「コードどおりに環境が構築されたか」を確認する。ドリフト(TF コードと実環境のズレ)の検知には、第2弾のアプローチが必要になる。
2-2. エンタープライズ案件における受入試験書の現実
金融・製造・公共系の AWS 案件では、構築完了後に顧客への提出物として 受入試験書 を作成するケースが多い。典型的な受入試験書は次の構成を持つ。
【受入試験書(抜粋)】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No. リソース名 属性期待値 実測値 結果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
001 aws_instance.web[0] instance_type t3.medium t3.medium OK
002 aws_instance.web[0] amiami-0abcdef12345 ami-0abcdef12345 OK
003 aws_instance.app[0] instance_type t3.larget3.smallNG ← 差分!
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
この表を手で埋めることが、現在も多くのプロジェクトで行われている。Console の画面をスクリーンショットして期待値と目視比較するか、AWS CLI を手動実行して確認するかのいずれかだ。
本記事の最終目標は、この「実測値」列を AWS Config Advanced Query で自動取得 し、「結果」列の OK/NG 判定を pytest で自動化 することだ。受入試験書は第1弾の Excel がそのまま受入証跡になる。
2-3. 手動突合の工数と「500 チェック問題」
手動突合の工数を具体的に試算してみよう。典型的なエンタープライズ案件で、以下の規模を想定する。
環境数 : 3(dev / stg / prod)
リソース種別数 : 約 15 種(EC2, RDS, ALB, S3, IAM, SG, VPC, Subnet ...)
各リソース種別の属性数: 約 10 属性(instance_type, ami, tags, security_groups ...)
インスタンス数 : 各種別平均 3〜5 個
→ チェック総数: 3 環境 × 15 種別 × 10 属性 × 4 インスタンス = 1,800 チェック
実務ではリソース数をもっと絞っても、10 環境 × 50 項目 = 500 チェック は珍しくない。これを担当エンジニアが 1 件ずつ Console で確認すると、集中力が続く前提でも 2〜3 日を要する。確認ミスや記入漏れも避けられない。
pytest で自動化したあとの所要時間はどうなるか。
tflint 実行 : 約 2 秒
terraform plan 実行 : 約 10 秒(リソース数次第)
AWS Config 取得: 約 5 秒(Advanced Query 1〜2 本)
突合・判定 : 約 1 秒
pytest 全体 : 約 20 秒
500 チェックを 20 秒で完了。再実行も同じ 20 秒。
この差が本記事の価値だ。「500 チェックを 20 秒で実行し、差分があれば NG として即座にレポートする」——これを pytest 単体テストとして実装する。
2-4. なぜ CI/CD ではなく「単体テスト」として位置付けるのか
本記事では pytest を 構築完了後に手動で 1 回実行する単体テスト として位置付けている。GitHub Actions や EventBridge による定期実行はスコープ外だ。この設計判断には理由がある。
理由 1: 受入試験は「イベント駆動」であるべき
定期実行は「いつドリフトが発生したかを検知する運用監視」に向いている。受入試験は「構築完了という特定のイベントの後に 1 回実施する品質ゲート」だ。両者は目的が異なる。本記事は後者に特化する。
理由 2: AWS Config の記録タイミングとコスト
AWS Config は記録頻度に応じてコストが発生する。定期実行の場合、Recorder の有効化期間・記録頻度・Aggregator の設定が運用コストに直結する。受入試験用途であれば、必要なタイミングだけ有効化して終わったら無効化する 運用も現実的だ(§3-1 で扱う)。
理由 3: 「テストの責任範囲」の明確化
CI/CD に組み込んだドリフト検知は「インフラ運用の継続的監視」になる。受入試験として pytest で実行するアプローチは「インフラ構築の品質保証」になる。後者は QA エンジニアや PM が受入証跡として理解しやすく、プロジェクト完了の判定基準に組み込みやすい。
CI 組み込みに発展させたい場合は、複数人開発シリーズ第2弾(GitHub Actions+OIDC) を参照してほしい。本記事で実装した pytest テストスイートはそのまま GitHub Actions の jobs[].steps[].run に置き換えられる。
2-5. 運用フロー Before/After
手動突合から pytest 自動化への変化を図で示す。

図の内容をテキストで補足する。
Before: 手動突合フロー
① terraform apply 完了
↓
② 担当者が AWS Console / CLI で各リソースを確認
↓
③ Excel の「実測値」列を手入力(500 チェック × 数日)
↓
④ 期待値と目視比較 → 差分を NG としてコメント記入
↓
⑤ NG 箇所を TF コードで修正 → 再 apply → ②に戻る
↓(数サイクル後)
⑥ 受入試験書を顧客に提出
After: pytest 自動化フロー
① terraform apply 完了
↓
② make drift-check(tflint + plan + Config fetch + compare + pytest)
↓(約 20 秒)
③ pytest が PASS → 受入試験書(Excel)の判定列が自動転記される
pytest が FAIL → NG リストが標準出力に表示される
↓(NG があれば)
④ NG 箇所を TF コードで修正 → 再 apply → ②に戻る
↓(1〜2 サイクルで解決)
⑤ pytest PASS → 受入試験書を顧客に提出
サイクル 1 回あたりの確認時間が「数日」から「20 秒」になる。修正サイクルを素早く回せるため、手戻りの発見が遅くなるリスクも減る。
2-6. AWS Config を初めて使う方へ
本記事の核心の 1 つは AWS Config Advanced Query だ。Config を使ったことがない読者のために、ここで概要だけ紹介しておく(詳細は §3 で扱う)。
AWS Config とは何か
AWS Config は「AWS リソースの構成変更を継続的に記録し、現時点の構成スナップショットを照会できるサービス」だ。EC2 インスタンスの instance_type、セキュリティグループのルール、RDS の Multi-AZ 設定——こうした属性の変更履歴と現状値を Config が保持している。
本記事での使い方
本記事では Config を「現在の実環境構成を一括取得するデータソース」として使う。具体的には select_resource_config という boto3 API を呼び出して、SQL に近い構文でリソースの構成情報を取得する。
-- Config Advanced Query の例(§6 で詳説)
SELECT
resourceId,
resourceType,
configuration.instanceType,
configuration.imageId,
tags
WHERE
resourceType = 'AWS::EC2::Instance'
このクエリを実行すると、現在の EC2 インスタンス一覧とその属性が JSON で返ってくる。これが「現状値」側のデータになる。
Config Recorder を有効化しないとどうなるか
Config Recorder が有効でない環境では select_resource_config が空を返す。ただし、本記事の §1〜§5(tflint・plan による期待値抽出)は Config なしで動作する。§3 の手順に従って Recorder を有効化してから §6 以降を実施することを推奨するが、学習目的であれば moto による boto3 モック(§8 で扱う)を使ってローカルのみで完結させることも可能だ。
費用感についても正直に書いておく。AWS Config Recorder は記録対象リソース数に応じて課金される。東京リージョン(ap-northeast-1)で 100 リソースを記録した場合、概算で 月額 3〜5 USD 程度だ。受入試験用途であれば、テスト期間だけ有効化して終了後に無効化することでコストを最小化できる(§3-1 で手順を示す)。
2-7. 本記事で扱わない領域の明示
本記事は意図的にスコープを絞っている。以下は本記事では扱わない。
| 領域 | 理由 | 参照先 |
|---|---|---|
| GitHub Actions / CodePipeline による定期実行 | 受入試験と定期監視は目的が異なる | 複数人開発シリーズ第2弾 |
| AWS Config Conformance Pack | スコープ外(Advanced Query で十分) | AWS 公式ドキュメント |
| マルチアカウント Aggregator の詳細設計 | 別記事候補 | §9 で概要のみ触れる |
| tflint ルールのカスタム開発 | スコープ外(組込みルール + aws plugin で十分) | tflint 公式ドキュメント |
| Config による変更履歴の追跡・通知 | 運用監視ユースケース(本記事外) | AWS Config 公式ドキュメント |
「定期監視まで含めた完全自動化が目標」という読者は、本記事で pytest テストスイートを実装した後、複数人開発シリーズ第2弾で GitHub Actions に組み込む手順を踏むとよい。本記事で作るテストコードはそのまま流用できる。
2-8. 受入試験自動化の副次効果
pytest による受入試験自動化には、工数削減以外にも副次効果がある。
副次効果 1: 修正サイクルが速くなる
手動突合では「確認 → NG 発見 → 報告 → 修正 → 再確認」のサイクルに半日〜1 日かかることがある。pytest であれば 20 秒でサイクルが回る。TF コードを修正して make drift-check を再実行するまでのリードタイムが劇的に縮まる。
副次効果 2: 受入証跡が自動生成される
pytest の -v --tb=short オプションを使うと、各テストケースの PASS/FAIL が一覧で出力される。この出力を受入試験書の付録として添付することが可能だ。さらに本記事では Excel の Sheet2「差分一覧」に自動転記する機能も実装するため、顧客への提出ドキュメントも自動で更新される。
副次効果 3: 「属人的チェック」から脱却できる
手動突合は「チェックリストを誰が担当するか」に依存する。熟練エンジニアが担当すると漏れが少ないが、ジュニアエンジニアが担当すると見落としが増える。pytest による自動チェックは担当者のスキルに依存しない。チーム全員が同じ品質で受入試験を実施できるようになる。
副次効果 4: 差分の「種類」が見えるようになる
手動突合では「差分がある」という事実しかわからない場合が多い。pytest の DiffRow には expected(期待値)と actual(現状値)と verdict(OK/NG/UNKNOWN)が入っている。差分の原因が型不一致なのか、値そのものが異なるのか、リソースが未検出なのかを区別して報告できる。これがトラブルシュートの時間を短縮する。
2-9. 本章のまとめと次章への接続
本章では本記事の「業務背景」を整理した。
- 第1弾の「設計時の突合(Excel 自動生成)」に対し、本記事は「構築後の突合(受入試験自動化)」
- エンタープライズ案件では 10 環境 × 50 項目 = 500 チェックの手動突合が現実に発生している
- pytest を 単体テスト として位置付け、CI/CD 定期実行はスコープ外とする
- AWS Config は「現状値取得のデータソース」として使う(§3 で Recorder 有効化から解説)
次章(§3)では AWS Config の基礎を解説する。Recorder の有効化、Configuration Item の構造、
Advanced Query の SQL 構文まで、Config 未経験者でも追体験できるように丁寧に進める。
2-10. 「受入試験書の自動化」というゴールを再確認する
本章の最後に、本記事のゴールイメージを具体化しておく。
第1弾で生成した Excel(param-sheet.xlsx)は、現時点では「期待値」列だけが埋まった状態だ。
【param-sheet.xlsx — Sheet1(期待値列のみ)】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
リソース名 属性期待値 現状値 判定
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
aws_instance.web[0] instance_typet3.medium (空欄) (空欄)
aws_instance.web[0] ami ami-0abcdef12345 (空欄) (空欄)
aws_instance.app[0] instance_typet3.large (空欄) (空欄)
aws_rds_cluster.mainengine aurora-mysql(空欄) (空欄)
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
本記事の実装を完了すると、Excel が次の状態になる。
【param-sheet.xlsx — Sheet1(全列埋まり・完成形)】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
リソース名 属性期待値 現状値 判定
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
aws_instance.web[0] instance_typet3.medium t3.medium OK
aws_instance.web[0] ami ami-0abcdef12345 ami-0abcdef12345 OK
aws_instance.app[0] instance_typet3.large t3.smallNG
aws_rds_cluster.mainengine aurora-mysqlaurora-mysqlOK
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
さらに Sheet2「差分一覧」には NG 行と UNKNOWN 行だけが自動転記される。
【param-sheet.xlsx — Sheet2(差分一覧)】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
リソース名 属性期待値現状値判定 備考
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
aws_instance.app[0] instance_typet3.large t3.small NG 値が異なる
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Sheet2 が顧客への指摘書・修正確認書として機能する。この状態の Excel が本記事の 最終成果物 だ。
pytest の出力としては次のようになる。
$ pytest tests/test_drift.py -v
============================== test session starts ==============================
platform darwin -- Python 3.11.9, pytest-8.1.1, pluggy-1.4.0
collected 12 items
tests/test_drift.py::test_drift_ec2_instance_type PASSED [ 16%]
tests/test_drift.py::test_drift_ec2_ami PASSED [ 25%]
tests/test_drift.py::test_drift_app_instance_type FAILED [ 33%]
tests/test_drift.py::test_drift_rds_engine PASSED [ 41%]
...
=================================== FAILURES ===================================
_________________ test_drift_app_instance_type _________________
AssertionError: NG diff found:
resource: aws_instance.app[0]
attribute: instance_type
expected: t3.large
actual:t3.small
verdict: NG
============================== 1 failed, 11 passed in 18.42s ==============================
NG が 1 件でも発生すると pytest は exit code 1 を返す。NG がゼロになるまで修正サイクルを回し、
全 PASS になったタイミングで Excel を受入証跡として顧客に提出する。
これが本記事のゴールだ。§3 から実装に入っていこう。
3. AWS Config 基礎 — Recorder / Aggregator / Advanced Query
本章は AWS Config 未経験者向けの入門章である。第1弾では boto3 の select_resource_config を使うコードが登場したが、「AWS Config をまだ有効にしたことがない」という読者のために、Recorder の有効化からコスト感・Aggregator の判断軸・Advanced Query の使い方まで一気に解説する。Config 経験者は §3-4(Advanced Query SQL)から読んでも構わない。
本記事サンプルコードの読み替え規則(本章のみ記載・以降省略)
– AWSアカウントID:123456789012(自アカウントIDに読み替え)
– リージョン:ap-northeast-1(使用リージョンに読み替え)
– S3バケット:myorg-terraform-state(自環境のバケット名に読み替え)
3-1. AWS Config とは何か
AWS Config は 「いつ・誰が・何を変えたか」を記録し続けるサービスである。EC2 インスタンス・セキュリティグループ・RDS クラスター・IAM ポリシーなど、あらゆるリソースの設定変更履歴が ConfigurationItem(CI) として S3 に保存される。
本記事で Config を使う目的は 「現時点の設定値を Advanced Query で一括取得し、Terraform の plan 期待値と突合する」ことだ。受入検証(構築完了後の単体テスト)として、設計書と実環境の整合性を確認する。
AWS Config の3大機能:
[Recorder] [Rules][Advanced Query]
│ │ │
設定変更を記録 コンプライアンス評価 SQLで現状値を一括取得
│ │ │
└──── S3 (Configuration History) ───────────┘
3-2. Recorder の有効化とコスト感
コスト感(正直に)
AWS Config の課金は主に 「設定項目の記録件数」 で決まる。東京リージョン(ap-northeast-1)の場合:
| リソース数 | 変更頻度(目安) | 月額概算 |
|---|---|---|
| 〜50リソース | 低(週1〜2回変更) | 数ドル未満 |
| 〜300リソース | 中(デプロイ週3〜5回) | 5〜15ドル |
| 〜1000リソース | 高(CI/CD 日次デプロイ) | 20〜60ドル |
- 1 CI あたり $0.003(2025年時点、東京リージョン)
- 変更がなければ課金はほぼゼロ
- Advanced Query 自体は無料(Recorder 有効化の費用のみ)
エンタープライズ案件で「Config を試したいが費用が怖い」場合は、dev 環境の単一アカウントから始めるのが現実解だ。
Terraform で Recorder を有効化する
# terraform/config.tf
# Terraform 1.9.x / provider hashicorp/aws ~> 5.0
terraform {
required_version = "~> 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Config 用 IAM ロール
resource "aws_iam_role" "config_recorder" {
name = "aws-config-recorder-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "config.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "config_recorder" {
role = aws_iam_role.config_recorder.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"
}
# Configuration Recorder — 全リソースタイプを記録
resource "aws_config_configuration_recorder" "main" {
name = "default"
role_arn = aws_iam_role.config_recorder.arn
recording_group {
all_supported = true
include_global_resource_types = true # IAM等グローバルリソースも対象
}
}
# Delivery Channel — 記録先 S3 バケット
resource "aws_config_delivery_channel" "main" {
name = "default"
s3_bucket_name = aws_s3_bucket.config_bucket.id
s3_key_prefix = "config"
snapshot_delivery_properties {
delivery_frequency = "TwentyFour_Hours" # 日次スナップショット
}
depends_on = [aws_config_configuration_recorder.main]
}
# Recorder のステータスを有効にする
resource "aws_config_configuration_recorder_status" "main" {
name = aws_config_configuration_recorder.main.name
is_enabled = true
depends_on = [aws_config_delivery_channel.main]
}
# Config ログ保存用 S3 バケット
resource "aws_s3_bucket" "config_bucket" {
bucket = "myorg-terraform-state-config-logs"
}
resource "aws_s3_bucket_policy" "config_bucket" {
bucket = aws_s3_bucket.config_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AWSConfigBucketPermissionsCheck"
Effect = "Allow"
Principal = { Service = "config.amazonaws.com" }
Action = "s3:GetBucketAcl"
Resource = "arn:aws:s3:::myorg-terraform-state-config-logs"
},
{
Sid = "AWSConfigBucketDelivery"
Effect = "Allow"
Principal = { Service = "config.amazonaws.com" }
Action = "s3:PutObject"
Resource = "arn:aws:s3:::myorg-terraform-state-config-logs/config/AWSLogs/123456789012/Configplan.json environments/*/tfplan
rm -rf $(REPORT_DIR) output/drift-report-summary.json
実行例:
# 全環境 plan → Excel → drift テスト → サマリレポート を一気通貫で実行
make plan-all && make excel && make drift-all
9-6. 環境別レポート集約スクリプト(drift_report.py)
make drift-all の最終ステップで呼ばれる drift_report.py は、
3環境の pytest JSON レポートを読み込み、差分行を Excel の ENV_SUBCOLS(期待値・現状値・判定)へ書き戻します。
#!/usr/bin/env python3
"""drift_report.py — 環境別 pytest JSON レポートを集約して Excel に書き戻す。"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
import openpyxl
from openpyxl.styles import PatternFill
# 第1弾 §7 の色定数(再利用)
COLOR_OK= "C6EFCE"
COLOR_NG= "FFC7CE"
COLOR_UNKNOWN = "FFEB9C"
ENV_SUBCOLS = ["期待値", "現状値", "判定"]
def load_pytest_report(path: Path) -> list[dict[str, Any]]:
"""pytest-json-report 形式の JSON から drift 行を抽出する。"""
with path.open(encoding="utf-8") as f:
report = json.load(f)
drift_rows: list[dict[str, Any]] = []
for test in report.get("tests", []):
if test.get("outcome") == "failed":
drift_rows.append({
"nodeid": test["nodeid"],
"message": test.get("call", {}).get("longrepr", ""),
})
return drift_rows
def fill_excel_env_cols(
wb: openpyxl.Workbook,
env: str,
drift_rows: list[dict[str, Any]],
) -> None:
"""Sheet1 の ENV_SUBCOLS(現状値・判定列)を drift テスト結果で埋める。"""
ws = wb["Sheet1"]
# ENV_SUBCOLS の列インデックスは §7(第1弾 write_excel)で確定済みのヘッダ行を検索して解決
# ここでは簡略化のため env 別列オフセットを定数とする
env_col_offset = {"dev": 0, "stg": 3, "prod": 6}.get(env, 0)
ACTUAL_COL = 5 + env_col_offset# 「現状値」列(1-indexed)
VERDICT_COL = 6 + env_col_offset# 「判定」列
ng_nodeids = {r["nodeid"] for r in drift_rows}
for row_idx in range(3, ws.max_row + 1):# Row3 以降がデータ行
resource_cell = ws.cell(row=row_idx, column=2) # リソース名列
if resource_cell.value is None:
continue
verdict_cell = ws.cell(row=row_idx, column=VERDICT_COL)
actual_cell = ws.cell(row=row_idx, column=ACTUAL_COL)
has_ng = any(resource_cell.value in nid for nid in ng_nodeids)
if has_ng:
verdict_cell.value = "× NG"
verdict_cell.fill = PatternFill("solid", fgColor=COLOR_NG)
actual_cell.fill = PatternFill("solid", fgColor=COLOR_NG)
else:
verdict_cell.value = "✓ OK"
verdict_cell.fill = PatternFill("solid", fgColor=COLOR_OK)
def main() -> None:
parser = argparse.ArgumentParser(description="drift レポート集約")
parser.add_argument("--reports", nargs="+", required=True)
parser.add_argument("--excel",required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
wb = openpyxl.load_workbook(args.excel)
summary: dict[str, Any] = {"environments": {}}
for report_path in args.reports:
p = Path(report_path)
env = p.stem.replace("_drift", "")# dev_drift.json → dev
drift_rows = load_pytest_report(p)
fill_excel_env_cols(wb, env, drift_rows)
summary["environments"][env] = {
"ng_count": len(drift_rows),
"report": str(p),
}
print(f"[{env}] drift NG: {len(drift_rows)} 件")
wb.save(args.excel)
print(f"Excel 更新完了: {args.excel}")
Path(args.output).write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"サマリレポート出力: {args.output}")
if __name__ == "__main__":
main()
9-7. 第1弾 ENV_SUBCOLS との連携
第1弾の write_excel() では、各環境列のヘッダを ENV_SUBCOLS = ['期待値', '現状値', '判定'] の3列で構成しています。
Row1: サービス | リソース名 | 属性 | === 期待値 ===| === 現状値 === | === 判定 ===
Row2:| | | dev | stg | prod | dev | stg | prod | dev | stg | prod
本記事で drift_report.py が fill_excel_env_cols() を呼ぶことで、
第2弾の drift テスト結果が第1弾の Excel に自動的に書き戻される構成になります。
第1弾 Excel(output/param-sheet.xlsx)と第2弾の drift_report.py は、
この ENV_SUBCOLS ヘッダ位置を契約として共有します。
第1弾 §7 で列インデックスを変更した場合は、drift_report.py 側の env_col_offset も合わせて更新してください。
Section 9 執筆完了。 environments/ 分離・Config 環境差・make drift-all・除外ルール・ENV_SUBCOLS 連携を網羅。
Section 10. ハンズオン実行と成果物確認
いよいよ本記事で実装したパイプライン全体を一気に走らせます。Section 2〜9 で構築した tflint → terraform plan → AWS Config Advanced Query → 突合 → pytest の5段階を、1つのシェルスクリプトで連続実行し、差分検知の流れを体験します。
Config 未有効の読者へ: Section 5(plan JSON 期待値抽出)と Section 6(Config 現状値取得)までは AWS Config Recorder なしでも動作確認できます。Config Recorder が未有効の場合は、後半の
config_fetch()呼び出しでpytest.skipが発火します。その場合でも tflint・plan 解析・compare()ロジック部分のユニットテストは通過しますので、Config 有効化前のウォームアップとして活用してください。
10-1. 事前準備チェックリスト
ハンズオンを始める前に、以下の準備が整っているか確認します。
# 動作環境チェック
tflint --version # 0.52 以上
terraform --version # 1.9.x 系
python --version # 3.11 以上
pytest --version # 8.x 系
aws --version # 2.x 系(CLI v2)
必要な AWS 権限(IAM ポリシー):
- config:SelectResourceConfig
- config:DescribeConfigurationRecorders
- config:DescribeConfigurationRecorderStatus
- ec2:DescribeInstances(リソースタイプ確認用)
- s3:GetObject(terraform state 参照)
- iam:PassRole(Config Recorder 用 IAM ロール設定時)
これらを持つ IAM ユーザー or ロールで AWS CLI の認証が通っていること:
$ aws sts get-caller-identity
{
"UserId": "AIDAXXXXXXXXXXXXXXXXX",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/param-sheet-tester"
}
# Python 依存ライブラリのインストール
pip install pytest boto3 openpyxl moto[config]
# プロジェクトディレクトリ構成確認
ls -la
# 期待する構成:
# param-sheet-drift-test/
# ├── environments/
# │├── dev/
# ││├── main.tf
# ││└── terraform.tfvars
# │├── stg/
# │└── prod/
# ├── tests/
# │├── conftest.py
# │└── test_drift.py
# ├── src/
# │├── config_fetch.py
# │├── compare.py
# │└── normalize.py
# ├── .tflint.hcl
# ├── Makefile
# └── pytest.ini
10-2. エンドツーエンド実行スクリプト
以下のスクリプトを run_drift_check.sh として保存し、プロジェクトルートから実行します。
#!/usr/bin/env bash
# run_drift_check.sh — tflint → plan → Config → compare → pytest の一気通貫実行
set -euo pipefail
ENVIRONMENT="${1:-dev}"
PLAN_FILE="plan_${ENVIRONMENT}.json"
echo "=========================================="
echo " Drift Check Pipeline — ${ENVIRONMENT}"
echo "=========================================="
# ── Step 1: tflint 静的検査(~2秒)─────────────────────────────
echo ""
echo "▶ [1/5] tflint 静的検査..."
cd "environments/${ENVIRONMENT}"
tflint --init --config ../../.tflint.hcl 2>&1
TFLINT_EXIT=$?
if [ $TFLINT_EXIT -ne 0 ]; then
echo "✗ tflint FAILED — HCL 構文エラーまたは lint 違反があります"
echo " pytest を実行しても無意味なため、ここで中断します"
exit 1
fi
echo "✓ tflint PASSED"
# ── Step 2: terraform plan JSON 生成(~10秒)────────────────────
echo ""
echo "▶ [2/5] terraform plan JSON 生成..."
terraform init -backend-config="key=${ENVIRONMENT}/terraform.tfstate" -input=false -no-color 2>&1
terraform plan \
-var-file="terraform.tfvars" \
-out="${PLAN_FILE}.bin" \
-no-color 2>&1
terraform show -json "${PLAN_FILE}.bin" > "../../${PLAN_FILE}"
echo "✓ terraform plan JSON 生成完了: ${PLAN_FILE} ($(wc -c < ../../${PLAN_FILE}) bytes)"
cd ../../
# ── Step 3: AWS Config Advanced Query で現状値取得(~5秒)───────
echo ""
echo "▶ [3/5] AWS Config 現状値取得..."
python3 -c "
from src.config_fetch import config_fetch
import json
resource_types = [
'AWS::EC2::Instance',
'AWS::RDS::DBInstance',
'AWS::ElasticLoadBalancingV2::LoadBalancer',
'AWS::S3::Bucket',
]
actual = config_fetch(resource_types)
print(f'取得リソース数: {sum(len(v) for v in actual.values())}件')
with open('actual_${ENVIRONMENT}.json', 'w') as f:
json.dump({k: {rk: dict(rv) for rk, rv in v.items()} for k, v in actual.items()}, f, indent=2)
"
echo "✓ Config 現状値取得完了: actual_${ENVIRONMENT}.json"
# ── Step 4: 突合(~1秒)────────────────────────────────────────
echo ""
echo "▶ [4/5] 期待値 vs 現状値 突合..."
python3 -c "
from src.compare import compare
from src.config_fetch import load_expected
import json
expected = load_expected('${PLAN_FILE}')
with open('actual_${ENVIRONMENT}.json') as f:
actual_raw = json.load(f)
# DiffRow リスト生成
diff_rows = compare(expected, actual_raw)
ng_count= sum(1 for r in diff_rows if r.verdict.value == 'NG')
ok_count= sum(1 for r in diff_rows if r.verdict.value == 'OK')
unk_count = sum(1 for r in diff_rows if r.verdict.value == 'UNKNOWN')
print(f'OK={ok_count} NG={ng_count} UNKNOWN={unk_count} TOTAL={len(diff_rows)}')
with open('diff_${ENVIRONMENT}.json', 'w') as f:
import dataclasses
json.dump([dataclasses.asdict(r) for r in diff_rows], f, indent=2, default=str)
"
echo "✓ 突合完了: diff_${ENVIRONMENT}.json"
# ── Step 5: pytest 実行(~20秒)────────────────────────────────
echo ""
echo "▶ [5/5] pytest 実行..."
PLAN_PATH="${PLAN_FILE}" \
AWS_ENV="${ENVIRONMENT}" \
pytest tests/ -v --tb=short \
--junit-xml="reports/drift_${ENVIRONMENT}_$(date +%Y%m%d_%H%M%S).xml"
PYTEST_EXIT=$?
echo ""
echo "=========================================="
if [ $PYTEST_EXIT -eq 0 ]; then
echo " ✓ 全テスト PASSED — drift なし"
else
echo " ✗ FAILED — drift 検知。reports/ を確認してください"
fi
echo "=========================================="
exit $PYTEST_EXIT
実行方法:
chmod +x run_drift_check.sh
# dev 環境の drift チェック
./run_drift_check.sh dev
# stg 環境の drift チェック
./run_drift_check.sh stg
# Makefile 経由(全環境一括)
make drift-all
10-3. 実行結果サンプル
PASS 時(drift なし)
以下は全リソースが設計値と一致している正常ケースの出力例です。
==========================================
Drift Check Pipeline — dev
==========================================
▶ [1/5] tflint 静的検査...
✓ tflint PASSED
▶ [2/5] terraform plan JSON 生成...
✓ terraform plan JSON 生成完了: plan_dev.json (84512 bytes)
▶ [3/5] AWS Config 現状値取得...
取得リソース数: 23件
✓ Config 現状値取得完了: actual_dev.json
▶ [4/5] 期待値 vs 現状値 突合...
OK=46 NG=0 UNKNOWN=3 TOTAL=49
✓ 突合完了: diff_dev.json
▶ [5/5] pytest 実行...
tests/test_drift.py::test_no_ng_verdict PASSED [ 8%]
tests/test_drift.py::test_ec2_instance_type[aws_instance.web[0]] PASSED[ 16%]
tests/test_drift.py::test_ec2_instance_type[aws_instance.web[1]] PASSED[ 25%]
tests/test_drift.py::test_rds_engine_version[aws_db_instance.main] PASSED [ 33%]
tests/test_drift.py::test_rds_multi_az[aws_db_instance.main] PASSED [ 41%]
tests/test_drift.py::test_alb_scheme[aws_lb.frontend] PASSED [ 50%]
tests/test_drift.py::test_s3_versioning[aws_s3_bucket.artifacts] PASSED[ 58%]
tests/test_drift.py::test_all_resources_covered PASSED [ 66%]
tests/test_drift.py::test_unknown_count_within_threshold PASSED [ 75%]
tests/test_drift.py::test_excel_sheet2_written PASSED[ 83%]
tests/test_drift.py::test_drift_report_generated PASSED [100%]
=================== 11 passed in 18.43s ====================
==========================================
✓ 全テスト PASSED — drift なし
==========================================
FAIL 時(drift 検知)
本番環境で EC2 の instance_type が変更されたケースです。pytest は赤色で FAIL を表示します。
==========================================
Drift Check Pipeline — prod
==========================================
▶ [1/5] tflint 静的検査...
✓ tflint PASSED
▶ [2/5] terraform plan JSON 生成...
✓ terraform plan JSON 生成完了: plan_prod.json (112088 bytes)
▶ [3/5] AWS Config 現状値取得...
取得リソース数: 61件
✓ Config 現状値取得完了: actual_prod.json
▶ [4/5] 期待値 vs 現状値 突合...
OK=118 NG=2 UNKNOWN=5 TOTAL=125
✓ 突合完了: diff_prod.json
▶ [5/5] pytest 実行...
tests/test_drift.py::test_no_ng_verdict FAILED [ 8%]
tests/test_drift.py::test_ec2_instance_type[aws_instance.api[0]] FAILED[ 16%]
...
================================ FAILURES =================================
test_no_ng_verdict
------------------
AssertionError: 2 件の NG が検出されました:
[NG] aws_instance.api[0] / instance_type
expected='m5.large' actual='t3.medium'
note='Terraformコード値と異なる — 手動変更または apply 未反映の可能性'
[NG] aws_instance.api[1] / instance_type
expected='m5.large' actual='t3.medium'
test_ec2_instance_type[aws_instance.api[0]]
-------------------------------------------
AssertionError:
Expected : m5.large
Actual: t3.medium
Verdict : NG
=================== 2 failed, 9 passed in 21.07s ====================
==========================================
✗ FAILED — drift 検知。reports/ を確認してください
==========================================

10-4. Excel Sheet2「差分一覧」自動転記
drift が検知された場合、test_excel_sheet2_written テストが第1弾で生成した Excel ファイルの Sheet2「差分一覧」に結果を自動転記します。転記後の Sheet2 は以下の形式になります。
Sheet2: 差分一覧(2026-04-19 03:15:22 更新)
| リソースアドレス | リソースタイプ | 属性 | 期待値(TF) | 現状値(Config) | 判定 |
|------------------------------|-------------------------|-------------------|--------------|------------------|---------|
| aws_instance.api[0] | AWS::EC2::Instance| instance_type | m5.large | t3.medium | NG|
| aws_instance.api[1] | AWS::EC2::Instance| instance_type | m5.large | t3.medium | NG|
| aws_instance.web[0] | AWS::EC2::Instance| instance_type | t3.small | t3.small| OK|
| aws_instance.web[1] | AWS::EC2::Instance| instance_type | t3.small | t3.small| OK|
| aws_db_instance.main| AWS::RDS::DBInstance | instance_class | db.r6g.large | db.r6g.large | OK|
| aws_db_instance.main| AWS::RDS::DBInstance | multi_az | true| true | OK|
| aws_lb.frontend | AWS::ELBV2::LoadBalancer| scheme| internal | internal| OK|
| aws_s3_bucket.artifacts| AWS::S3::Bucket| versioning_status | Enabled| Enabled | OK|
| aws_instance.batch[0] | AWS::EC2::Instance| tags.Environment | prod| prod | OK|
| aws_instance.batch[0] | AWS::EC2::Instance| tags.Team| —| backend | UNKNOWN |
UNKNOWN の解釈: Terraform コードに tags.Team の記述がなく、Config 側にのみ値が存在するケースです。設計書に記載のないタグが手動で付与された状態を意味します。厳密な管理方針の組織では NG 扱いにする運用も選べます(Section 7 の Verdict 設計を参照)。
10-5. トラブルシュート
ケース 1: AWS Config Recorder が未有効
SKIPPED tests/test_drift.py::test_no_ng_verdict
— reason: AWS Config Recorder is not enabled in this region (ap-northeast-1).
Enable it with: terraform apply -target=aws_config_configuration_recorder.main
(Section 3 参照)
========================= 11 skipped, 0 passed in 0.82s ========================
対処法: Section 3 の手順で aws_config_configuration_recorder を Terraform で作成・適用してください。ハンズオン中の月額コストは数ドル程度です。Recorder を有効化したくない場合は、Section 5 までのユニットテスト(plan 解析・型正規化・突合ロジック)のみ実行できます:
# Config なしで動くユニットテストのみ実行
pytest tests/ -m "not requires_config" -v
SKIP = FAIL ルール: 本プロジェクトでは pytest の SKIP は未完了と同義です。Config Recorder を有効化せずに「テスト全件通過」とはなりません。受入試験として使う場合は、必ず Config Recorder を有効にした環境で実施してください。
ケース 2: IAM 権限不足
FAILED tests/test_drift.py::test_no_ng_verdict
botocore.exceptions.ClientError: An error occurred (AccessDeniedException) when
calling the SelectResourceConfig operation:
User: arn:aws:iam::123456789012:user/param-sheet-tester is not authorized to
perform: config:SelectResourceConfig
対処法: 実行 IAM ユーザー/ロールに以下のポリシーを追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowConfigQuery",
"Effect": "Allow",
"Action": [
"config:SelectResourceConfig",
"config:DescribeConfigurationRecorders",
"config:DescribeConfigurationRecorderStatus"
],
"Resource": "*"
}
]
}
インラインポリシーとして IAM ユーザーに直接付与するか、Section 3 の Terraform ハンズオンで用意した IAM ロールを使用してください。
ケース 3: resourceType 未対応
WARNING: config_fetch — resourceType 'AWS::Lambda::Function' は
Advanced Query でサポートされていないため、スキップしました。
(ConfigService.SelectResourceConfig: UnsupportedResourceTypeException)
対処法: AWS Config Advanced Query がサポートするリソースタイプは随時拡大されていますが、すべての resourceType が対応しているわけではありません。未対応の場合は Verdict.UNKNOWN として処理し、テストを SKIP させます。
# src/config_fetch.py の該当ハンドリング
try:
response = client.select_resource_config(Expression=sql)
except client.exceptions.UnsupportedResourceTypeException:
logger.warning(
f"resourceType '{rt}' は Advanced Query 非対応 — UNKNOWN 扱いにします"
)
result[rt] = {} # 空辞書 → compare() 側で UNKNOWN 判定
対応リソースタイプの最新リストは AWS 公式ドキュメント: Supported resource types for AWS Config Advanced queries を参照してください。
10-6. 実行時間の目安と最適化
パイプライン全体の実行時間目安(リソース数が数十台規模の一般的なエンタープライズ案件の場合):
Step 1: tflint 静的検査 ─── ~2 秒
Step 2: terraform plan JSON─── ~10 秒(backend 接続 + state ダウンロード込み)
Step 3: Config Advanced Query ─── ~5 秒(ページネーション 1〜2 ページ)
Step 4: 突合処理(compare)─── ~1 秒(Python インメモリ)
Step 5: pytest 実行 ─── ~20 秒(フィクスチャ初期化 + 全テスト)
─────────────
合計 ─── ~38 秒
最適化のヒント:
– terraform state が S3 にある場合、terraform plan は S3 アクセスを伴うため、VPC エンドポイントを設定するとレイテンシが改善します
– Config Advanced Query の取得対象 resourceType を最小限に絞ることで Step 3 を短縮できます
– CI 環境での実行はキャッシュ(~/.terraform.d/plugins)を活用することで plan 時間を短縮できます
Section 11. まとめと次の発展(CI/CD 組込み導線)
本記事では、AWS Config と Terraform パラメーターシートを突合する単体テスト自動化パイプラインを構築しました。このセクションでは、シリーズ2本を通して得た成果を振り返り、次のステップへの道筋を示します。
11-1. シリーズ2本で完成した「設計→検証」の循環
第1弾・第2弾を通して構築したパイプラインを図で整理します。
【第1弾】設計の可視化
─────────────────────────────────────────────────────────────────
Terraform コード
│
▼
terraform plan -json
│
▼
parse_plan()─── plan JSON から期待値を抽出
│
▼
write_excel() ─── Excel パラメーターシート生成
(期待値列・現状値列・判定列)
│
▼
関係者への配布・設計レビュー ✓ (第1弾の成果)
【第2弾(本記事)】構築後の検証自動化
─────────────────────────────────────────────────────────────────
┌─ tflint ─────────────── HCL 静的検査(構文・best practice)
│
├─ parse_plan()(再利用)─ 期待値 load_expected()
│
├─ config_fetch()──────── AWS Config Advanced Query → 現状値
│
├─ compare()───────────── 型正規化・差分判定・Verdict
│
├─ pytest──────────────── 単体テストとして合否判定
│
└─ write_excel()(拡張)─ Sheet2「差分一覧」自動転記 ✓ (第2弾の成果)
─────────────────────────────────────────────────────────────────
↑ 第1弾と第2弾が組み合わさって
「設計書(Excel)の作成 → 実環境との突合 → 差分検知」
という完全なループが完成する
シリーズ2本で達成したこと:
| 項目 | 達成内容 |
|---|---|
| 設計の可視化 | Terraform コードから Excel パラメーターシートを自動生成(第1弾) |
| 受入検証の自動化 | AWS Config で実環境値を取得し、設計値と pytest で比較(第2弾) |
| 差分の記録 | Sheet2「差分一覧」に NG/OK/UNKNOWN を自動転記(第2弾拡張) |
| 静的品質チェック | tflint をパイプライン入口のゲートに組込み(第2弾) |
| マルチ環境対応 | environments/{dev,stg,prod} で環境別 drift チェック(第2弾) |
エンタープライズ案件で典型的な「構築完了 → パラメーターシートの手動突合(500チェック / 数日)」という作業が、コマンド1本・数十秒で完了するようになりました。
11-2. 本記事で意図的に踏み込まなかった領域
技術的に実現可能でも、スコープや設計方針から除外した項目があります。次のステップを検討する際の参考にしてください。
定期実行(cron / EventBridge)
本記事の位置づけは「構築完了後の受入検証(単体テスト)」です。毎夜自動実行や EventBridge でのトリガーは設計外です。定期監視が必要な場合は AWS Config のマネージドルール(例: EC2_INSTANCE_TYPE)や AWS Security Hub との組合せを検討してください。
CI/CD 組込み
pytest の終了コードを CI の fail 条件として使えば、「設計外変更があったらパイプラインを止める」という運用が可能です。ただし、本記事ではその実装を cmd_040 第2弾(GitHub Actions+OIDC)に委譲しています。
マルチアカウント Aggregator
本記事の Section 9 では単一アカウント内のマルチ環境を対象としました。AWS Organizations + Config Aggregator を用いた複数 AWS アカウントの横断 drift チェックは別記事候補です。
Config Custom Rule / Conformance Pack
compare() で実装した突合ロジックを AWS Lambda + Config Custom Rule に変換すれば、Config のマネージドサービスとして継続監視が可能です。ただし、実装コストが高く、Terraform コードとのバージョン管理が複雑になるため、本記事ではスコープ外としました。
11-3. 読者別の発展ルート
ルート A: CI/CD に組み込みたい
本記事の run_drift_check.sh を GitHub Actions の jobs.drift-check ステップに追加し、terraform apply の後続ジョブとして実行する構成を取ります。
# GitHub Actions ステップ例
- name: Drift Check
run: ./run_drift_check.sh ${{ env.ENVIRONMENT }}
env:
AWS_REGION: ap-northeast-1
インフラと CI/CD パイプラインの完全な構成は cmd_040 第2弾(GitHub Actions+OIDC で PR駆動 CI/CD) で解説しています。本記事のスクリプトはそのまま流用できます。
ルート B: Config Custom Rule に発展させたい
compare() のロジックを AWS Lambda に移植し、aws configservice put-config-rule で登録することで、Config のマネージドサービスとして継続的に drift を監視できます。ただし、Terraform のリソースアドレスと Config の resourceId の紐付けロジックをクラウド側で維持する必要があり、実装コストは本記事の3〜5倍程度を見込んでください。
ルート C: 社内展開・勉強会材料として使いたい
本シリーズ2本(第1弾 Excel 生成 + 第2弾 drift テスト)のスクリプトセットは、社内インフラチームの勉強会教材として使いやすい構成になっています。特に「手動突合の前/後 比較」(Section 2)と「実行時間 38 秒の実演」(Section 10)が聴衆への説得力が高いポイントです。
11-4. おわりに
「AWS パラメーターシート自動化シリーズ」全2弾を通して、次のことが実現できました:
- 第1弾: Terraform コードを Single Source of Truth として、設計書(Excel)を自動生成する仕組みを構築
- 第2弾(本記事): 構築後の実環境を AWS Config で取得し、設計書と pytest で突合する受入検証を自動化
この2本が揃うことで、「設計 → 実装 → 受入検証」のループがコード化されました。Terraform コードを修正すれば設計書が更新され、デプロイ後に drift チェックを走らせれば設計値との差分が自動検出されます。
エンタープライズ案件の IaC 化を進める上で、「設計書と実環境の乖離」は避けられない課題です。本シリーズが、その課題をコードで解決する1つのアプローチとして参考になれば幸いです。
CI/CD に組み込んで「デプロイ → 自動 drift チェック」のパイプラインへ発展させる場合は、以下のシリーズが役立ちます。
AWS×Terraform 複数人開発シリーズ(全3弾):