Terraform 変更を安全にレビューする — plan の読み方・CI destroy 検知・承認フロー設計
Terraform のレビューはコードレビューではない
Terraform の PR レビューを「コードレビュー」と同じ感覚でやると本番が壊れる。
plan 出力に埋もれた destroy 行、resource 追加に紛れた IAM 権限の昇格、セキュアな値の漏洩。これらは「コードが綺麗か」を見る目では捕まらない。
差分を読む順序、CI で機械的に弾く仕組み、誰がどこを承認するかというフロー設計。3つが揃って初めてレビューが機能する。
なぜ Terraform 変更は普通の PR レビューでは守れないのか
理由は3つある。
plan 出力が長すぎて危険な行が埋もれる: 本番環境の terraform plan は数百行を超えることがある。+ resource ... の山の中に - aws_db_instance.main の1行が紛れ込む。レビュアーは出力を見落とす可能性が高まる。
コード差分と plan 出力は別物: prevent_destroy を外した1行のコード変更が、plan 上では無関係に見える destroy 連鎖を引き起こすことがある。コード差分だけを見ても影響範囲が読めない。
承認フローが曖昧だと止まらない: 「誰でも apply できる」「特定の1人が承認すれば全領域に通る」状態では、本番リソースの変更を止める壁がない。チーム全体の合意なしに本番が変わる。
これらを踏まえて、レビューを次の3層で組み立てる。
- plan 出力を人間が読むときの観点
- CI で機械的に弾く仕組み
- チームの承認フロー
plan 出力の読み方 — 上から順に読まない
plan を最初の行から最後まで通読するのは効率が悪い。重要なのは「何を見落とすと事故になるか」を逆算してから読むことだ。
優先して確認するのは4つ。
1. destroy と replace を最初に探す
terraform plan の summary 行(Plan: X to add, Y to change, Z to destroy.)を最初に見る。Z が 0 でなければ内訳を全て確認する。
terraform plan -no-color | grep -E "^\s+# .* will be destroyed$"
terraform plan -no-color | grep -E "^\s+# .* must be replaced$"
will be destroyed は明示的な削除、must be replaced は force_new 属性の変更による削除+再作成だ。RDS や EBS のような状態を持つリソースが replaced に入っていれば、データが消える。
2. IAM の権限拡張を見る
aws_iam_policy や aws_iam_role_policy の差分は、文字列1つ違うだけで権限範囲が大きく変わる。
"Resource": [
- "arn:aws:s3:::my-bucket/*"
+ "arn:aws:s3:::*"
]
この変更は plan の ~ jsonencode(...) 行で1文字差として表現される。レビュアーは「以前と何が変わったか」を decode して読む必要がある。
IAM 差分は次の観点で見る。
-
Resourceのワイルドカードが広がっていないか -
Actionに*や管理系 API(iam:PassRoleiam:CreateAccessKeyなど)が追加されていないか -
Conditionブロックが弱まっていないか(IP 制限が外れる等)
3. sensitive 値の漏洩を確認する
(sensitive value) でマスクされていない秘密情報がコード差分に紛れていないか確認する。RDS のパスワード、API キー、SSH 鍵がリポジトリに混入する事故は今でも起きる。
git diff origin/main...HEAD -- '*.tf' '*.tfvars' | \
grep -E "(password|secret|token|api_key)\s*=\s*\""
*.tfvars を gitignore に入れるのが基本だが、PR 段階で機械的に検知する仕組みがあれば漏洩リスクが下がる。
4. count / for_each の変化を見る
count = var.enabled ? 1 : 0 のような条件分岐は、変数が変わるとリソースごと消える。for_each の map キー変更も既存リソースの destroy / create を引き起こす。
count や for_each の元になる変数の変更は、plan summary で意図しない destroy として現れる。コード差分で「count や for_each に渡している変数が変わっていないか」を確認する。
CI で destroy を機械的に弾く
人間のレビューは見落とす。CI で機械的に弾く仕組みを置く。
GitHub Actions で plan の destroy を検知して PR にコメントを残す例。
name: terraform-plan-check
on:
pull_request:
paths:
- '**.tf'
- '**.tfvars'
jobs:
plan:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.11.0
- name: terraform init
run: terraform init
- name: terraform plan
run: terraform plan -no-color -out=tfplan > plan.txt 2>&1 || true
- name: detect destroy
id: detect
run: |
DESTROY=$(grep -cE "^\s+# .* will be destroyed$" plan.txt || true)
REPLACE=$(grep -cE "^\s+# .* must be replaced$" plan.txt || true)
echo "destroy=${DESTROY}" >> $GITHUB_OUTPUT
echo "replace=${REPLACE}" >> $GITHUB_OUTPUT
- name: comment on PR
if: steps.detect.outputs.destroy != '0' || steps.detect.outputs.replace != '0'
uses: actions/github-script@v7
with:
script: |
const destroy = '${{ steps.detect.outputs.destroy }}';
const replace = '${{ steps.detect.outputs.replace }}';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ destroy: ${destroy} / replace: ${replace}\n破壊的変更が含まれています。レビュアーは plan を確認してください。`
});
これで PR を開いた瞬間に「破壊的変更があるか」が PR コメントに残る。レビュアーは plan を全文読まなくても、まずコメントを見て確認すべきポイントを掴める。
terraform:destroy のような label を自動付与すれば、branch protection の require-approval 数を引き上げる仕組みと組み合わせられる。「破壊的変更が入っている PR は2人承認」という運用が機械的に強制できる。
チームの承認フロー設計
技術的な検知ができても、誰が承認するかの設計がないと止まらない。
CODEOWNERS で領域別の承認者を決める
.github/CODEOWNERS でディレクトリ別に承認者を分ける。
# .github/CODEOWNERS
# 本番環境(SRE チーム必須)
/environments/production/ @org/sre-team
# ステージング(バックエンドチームでも可)
/environments/staging/ @org/sre-team @org/backend-team
# モジュール(プラットフォームチーム)
/modules/ @org/platform-team
これと branch protection の Require review from Code Owners を組み合わせると、本番ディレクトリの変更には SRE チームの承認が必須になる。チーム外の人が apply まで通せなくなる。
Environment 別の承認ゲート
GitHub Environments を使うと、apply ジョブが起動する前に追加の手動承認を入れられる。
jobs:
apply:
runs-on: ubuntu-latest
environment:
name: production # protection rules で承認者を指定する
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.11.0
- run: terraform init
- run: terraform apply -auto-approve tfplan
Settings > Environments > production で「Required reviewers」を設定すると、apply ジョブが動く前に承認画面が出る。CODEOWNERS が PR レビュー時の壁、Environment が apply 直前の壁、と二重化できる。
plan と apply を分離する
PR マージ=apply ではなく、plan は PR で確認、apply は明示的なトリガーで実行する。
- PR で
terraform planを実行 → 出力をコメントで残す - main にマージしても apply は走らない
- workflow_dispatch(手動実行)か tag push で apply 専用の workflow を起動 → Environment 承認 → apply
merge と apply の間に「人間が apply を起動する」一手間が入る。これが最終承認の壁になる。コードがマージされた瞬間に本番反映される設計は、レビューが甘くなったときに逃げ場がない。
自動化と人間の判断、両方がいる
CI の destroy 検知は「機械的に弾けるところ」を担当する。plan の精読と承認は「機械では判断できないところ」を担当する。
destroy が出ているとき、それが意図したマイグレーションなのか事故なのかは、文脈を知る人間しか判断できない。逆に、人間は数百行の plan を毎回精読するのは事故の元となる。CI の補助なしに「全 PR を完全にレビューする」のは現実的ではない。
3層を整えると、PR 単位で「誰の目を通って何を承認したか」が記録に残る。事故が起きてもなぜ通ったかが追える。
レビューフローの設計は、設計の良し悪しと同じくらい、事故が起きたときに学びを残せるかを左右する。
Discussion