Terraformドリフト検知プレイブック:本番事故を防ぐCI設計と運用手順

Terraform を導入していても、運用が進むほど「実環境がいつの間にかコードとズレる」問題にぶつかります。いわゆるドリフトです。最初は小さな差分でも、放置すると本番変更時に予期せぬ差分が混ざり、障害やリリース遅延の原因になります。

本記事では、Terraform ドリフト検知を単なる terraform plan 実行で終わらせず、継続運用できる仕組みとして実装するための具体策をまとめます。対象は AWS を例にしますが、考え方は他クラウドでも共通です。

1. ドリフト検知で最初に決めるべきこと

多くのチームが失敗するのは、実装前に運用設計を決めないことです。まず以下を決めます。

  1. どの環境をいつ検知するか(prod は毎日、stg は平日など)
  2. 検知結果をどこに通知するか(Slack/Discord/Issue)
  3. 誰がいつまでに対応するか(当番制、SLA)
  4. 「意図した手動変更」をどう扱うか(例外ラベル、期限付き)

ここを決めずに CI だけ作ると、通知がノイズ化して無視されます。ドリフト検知は技術課題より運用課題です。

2. リポジトリ構成と state 分離

最小限、次のような構成を推奨します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
infra/
  modules/
    vpc/
    ecs/
    rds/
  envs/
    prod/
      main.tf
      backend.hcl
      variables.tf
    stg/
      main.tf
      backend.hcl
.github/
  workflows/
    terraform-drift.yml

環境ごとに backend と state を分けることが重要です。ドリフト検知ジョブが state を誤って参照すると、存在しない差分が出ます。S3 backend + DynamoDB lock を使う場合は、bucket/key/region/table の整合性を必ず固定化します。

3. CI での検知フロー(GitHub Actions)

以下は、毎朝実行してドリフトを検知する最小構成です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
name: terraform-drift

on:
  schedule:
    - cron: "0 22 * * *" # JST 07:00
  workflow_dispatch:

jobs:
  drift-prod:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra/envs/prod
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.8

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-terraform-readonly
          aws-region: ap-northeast-1

      - name: Init
        run: terraform init -backend-config=backend.hcl

      - name: Drift check
        run: terraform plan -detailed-exitcode -out=tfplan

-detailed-exitcode は実務で必須です。終了コードの意味は以下です。

  • 0: 差分なし
  • 1: エラー
  • 2: 差分あり(ドリフト含む)

これを使えば、差分ありと失敗を明確に分けられます。

4. 終了コードを正しく扱う

Actions で単純に terraform plan を実行すると、終了コード 2 が failure 扱いになるため、実際には「検知成功」なのに赤く見えます。以下のように分岐して扱います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
set +e
terraform plan -detailed-exitcode -out=tfplan
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "DRIFT=false" >> $GITHUB_ENV
elif [ "$code" -eq 2 ]; then
  echo "DRIFT=true" >> $GITHUB_ENV
else
  echo "Terraform plan failed"
  exit 1
fi

この実装にすると、ジョブ自体は成功させたまま「ドリフト有無」を後段に渡せます。

5. 通知設計:差分サマリを人間が読める形にする

ドリフト検知で最も大事なのは、通知が読みやすいことです。plan 全文を貼ると誰も読みません。次の 3 つだけ通知します。

  1. どの環境で
  2. 何が(resource type + name)
  3. どう変わるか(create/update/delete)

例: terraform show -json tfplanjq で要約

1
2
3
4
5
terraform show -json tfplan > plan.json
jq -r '
  .resource_changes[] |
  "- \(.type).\(.name): \(.change.actions | join(","))"
' plan.json | head -n 50 > summary.txt

通知文は長すぎると切れるため、冒頭 50 件だけに制限し、全文は artifacts に添付する運用が実践的です。

6. 誤検知を減らす具体策

ドリフト検知の信頼性を下げる典型例は次です。

  • timestamp 的な値を管理対象にしている
  • provider バージョン差で毎回微差分が出る
  • autoscaling 系設定が外部コントローラで更新される

対策として、lifecycle { ignore_changes = [...] } を乱用せず、変化が許容される属性だけを限定的に無視します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resource "aws_ecs_service" "app" {
  name            = "app"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn

  lifecycle {
    ignore_changes = [
      desired_count
    ]
  }
}

ignore_changes は便利ですが、広げすぎると本当に危険なドリフトを見逃します。四半期ごとに見直しを入れてください。

7. 復旧フロー:検知後に迷わない手順

ドリフトを検知したら、毎回議論しないように Runbook 化します。

ステップA: 変更の意図確認

  • 直近の障害対応や緊急作業がなかったか
  • コンソール手動変更のチケットがあるか

ステップB: 差分分類

  • 許容(意図あり)
  • 要コード反映(意図あり、IaC未反映)
  • 要即時修正(意図なし)

ステップC: 実行方針

  • IaC 正とする場合: コード更新 → PR → apply
  • 実環境正としない場合: 手動変更を戻す、または Terraform apply で整合

このフローを当番が 30 分以内に回せるようにすると、検知の価値が跳ね上がります。

8. IAM 最小権限の実例

ドリフト検知専用ロールは read-only を原則にします。plan だけなら多くのサービスで読み取り権限で足ります。apply 権限を同じロールに入れると、漏えい時のリスクが大きくなります。

  • github-terraform-readonly: drift check 用
  • github-terraform-apply: manual approval 後に限定実行

この分離は監査対応でも説明しやすく、セキュリティレビューで通りやすいです。

9. 週次レポートで改善を回す

検知を入れたら終わりではありません。週次で次を見ます。

  • ドリフト検知件数
  • うち誤検知件数
  • MTTR(検知から解消まで)
  • 原因カテゴリ(手動運用、自動スケール、設定ミス)

誤検知率が 30% を超えるなら通知設計か ignore ポリシーが過剰です。逆に検知ゼロが長すぎる場合は、ジョブ自体が死んでいる可能性もあります。

10. 導入チェックリスト

最後に、導入時のチェックリストを置いておきます。

  • env ごとに backend/state 分離済み
  • terraform plan -detailed-exitcode を採用
  • 終了コード 2 を正常系として処理
  • 通知は summary + artifacts の二段構成
  • read-only ロールで drift check を実行
  • 復旧 Runbook を 1 ページ化
  • 週次で誤検知率と MTTR をレビュー

Terraform ドリフト検知は、単なる監視ではなく「IaC の信頼性を維持する運用基盤」です。最初から完璧を狙うより、通知品質と対応フローを小さく回して改善する方が、結果的に強い運用になります。

11. すぐ使える運用コマンド(当番向け)

最後に、当番が迷わず実行できる最小コマンドをまとめます。どれも「まず状況把握」を優先する順序です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1) 直近のドリフトジョブ確認(GitHub CLI)
gh run list --workflow terraform-drift.yml --limit 5

# 2) 失敗/差分あり実行のログ確認
gh run view <run-id> --log

# 3) 対象環境で再現確認(ローカル)
cd infra/envs/prod
terraform init -backend-config=backend.hcl
terraform plan -detailed-exitcode

# 4) 差分要約を作成
terraform plan -out=tfplan
terraform show -json tfplan | jq -r '.resource_changes[] | "- \(.type).\(.name): \(.change.actions | join(","))"'

この 4 ステップを運用手順書の先頭に置くだけで、一次対応の速度と品質が安定します。特に「誰が見ても同じ順序で確認できる」状態を作ることが、夜間障害時の認知負荷を大きく下げます。