Terraform planでAI生成IaCの削除・置換事故を止める7チェック — apply前レビューの実務テンプレ
更新日: 2026-07-25
AIにTerraformを書いてもらうと、10分かかっていた記述が1分で出てきます。
でも、コード差分が小さいことと、インフラ変更が小さいことは同じではありません。数行の変更でデータベースが置換される。名前変更のつもりが削除と再作成になる。レビューでHCLだけ眺めていると、ここを見落とします。
結論はシンプルです。AIが書いたHCLではなく、Terraformが計算したplanをapply前に検査します。
この記事では、fmt、validate、終了コード、削除・置換、変更数、秘密情報、人間承認の7チェックを実務テンプレにします。AIは差分説明を手伝いますが、applyの承認は人間が持ちます。
最初に3つの用語だけ
- IaC: サーバーやネットワークなどの構成をコードで管理する考え方です。
- plan: 現在の状態とコードを比較し、applyしたら何が変わるかを計算した結果です。
- replace: 既存リソースを削除して、新しく作り直す変更です。名前や識別子が変わるだけでも起きます。
僕はapply前の境界を 「適用前レッドライン」 と呼びます。機械判定できる危険と、人間だけが判断できる許容損失を分け、apply前で止める境界です。
人間、AI、決定的ツールの役割
| 担当 | 任せること | 任せないこと |
|---|---|---|
| 人間 | 消えてよい資源、許容停止時間、例外、apply承認 | JSONの単純集計 |
| AI | plan要約、確認質問、代替案の草案 | 生planの外部送信、最終承認 |
| Terraform/スクリプト | 構文、差分、件数、delete/replaceの検出 | 変更が業務上妥当か |
「危険そう」はAIの感想です。一方、deleteを含むかは決定的に判定できます。先に機械で止め、そのあと人間が理由を読む。この順番が大事です。
10分の初回成功: replaceを終了コード1で止める
Terraform環境がなくても試せるダミーplan JSONを使います。実際のplan JSONには秘密が含まれ得るため、最初は必ずダミーで動かしてください。
{
"resource_changes": [
{
"address": "aws_db_instance.example",
"change": {
"actions": ["delete", "create"]
}
},
{
"address": "aws_s3_bucket.logs",
"change": {
"actions": ["update"]
}
}
]
}
次のチェッカーは、deleteまたはdelete+createを見つけたら停止します。
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
BLOCKING_ACTIONS = {"delete"}
def inspect_plan(path: str) -> int:
plan = json.loads(Path(path).read_text(encoding="utf-8"))
blocked = []
for item in plan.get("resource_changes", []):
actions = item.get("change", {}).get("actions", [])
if BLOCKING_ACTIONS.intersection(actions):
blocked.append({
"address": item.get("address", "(unknown)"),
"actions": actions,
})
if blocked:
print(json.dumps({"status": "blocked", "changes": blocked},
ensure_ascii=False, indent=2))
return 1
print('{"status":"ok"}')
return 0
if __name__ == "__main__":
raise SystemExit(inspect_plan(sys.argv[1]))
python3 check_plan.py sample-plan.json
echo $?
aws_db_instance.exampleの["delete", "create"]が表示され、終了コード1になれば初回成功です。理解ではなく、危険変更を一度止められた状態。ここまでなら10分で進められます。
チェック1: fmtで読みやすい差分にする
terraform fmt -check -recursive
terraform fmtはTerraformコードを標準形式へ整えます。これは安全性そのものではありません。ただ、無関係な空白差分を減らすと、本当に読むべき変更へ注意を使えます。
AIに整形させる必要はありません。決定的なformatterに任せます。
チェック2: validateで構文と内部整合性を確かめる
terraform init -backend=false
terraform validate
validateは構文や引数、属性名、型などの内部整合性を検査します。公式ドキュメントも、remote stateやprovider APIなどの外部サービスまでは検証しないと説明しています。
つまり、validate成功は「本番で安全」の証明ではありません。文法試験に通った状態です。
チェック3: planの終了コードを3値で扱う
set +e
terraform plan -detailed-exitcode -out=tfplan
code=$?
set -e
case "$code" in
0) echo "変更なし" ;;
1) echo "planエラー"; exit 1 ;;
2) echo "変更あり。レビューへ進む" ;;
*) echo "未知の終了コード"; exit 1 ;;
esac
-detailed-exitcodeでは、0が変更なし、1がエラー、2が変更ありです。 2は失敗ではありません。 ここを普通の成功/失敗だけで処理すると、変更のある正常planを誤って落とします。
なお、-out=tfplanで保存したplanファイルには、画面上で隠される値も平文で含まれ得ます。artifactへ無期限保存したり、チャットへ添付したりしないでください。
チェック4: deleteとreplaceを既定で止める
人間向け画面だけでなく、機械可読JSONを作ります。
terraform show -json tfplan > tfplan.json
python3 check_plan.py tfplan.json
TerraformのJSONでは、actionsが["delete"]なら削除、["delete","create"]または["create","delete"]なら置換です。どちらも既定で停止し、次を人間が確認します。
- なぜ置換が必要なのか
- データは退避されるか
- 停止時間は許容範囲か
- rollbackは可能か
- 同じ目的をupdateで達成できないか
AIには理由の候補を出させてよい。でも、消えてよいかは持ち主が決めます。
チェック5: 変更数の上限で想定外の広がりを止める
小さなPRで100リソースが変わるなら、依存関係やprovider更新を疑う余地があります。固定の万能値はないので、チームの通常変更から上限を決めます。
def count_changed_resources(plan: dict) -> int:
return sum(
1
for item in plan.get("resource_changes", [])
if item.get("change", {}).get("actions") != ["no-op"]
)
limit = 20 # 例。実績から人間が決める
count = count_changed_resources(plan)
if count > limit:
raise SystemExit(f"blocked: {count} resources exceed limit {limit}")
大規模移行では20件を超えることもあります。その場合は上限を消すのではなく、変更を分割するか、理由・期間・承認者つき例外にします。
チェック6: plan JSONを秘密情報として扱う
ここは見落とされやすいです。公式のterraform showドキュメントは、-jsonでsensitive値が平文表示されると警告しています。sensitive = trueは、すべての保存先から値を消す魔法ではありません。
最低限、次を守ります。
- planとJSONをGitへcommitしない
- CI artifactへ保存するなら短い保持期間と限定権限にする
- 外部AIへ生JSONを貼らない
- AIへ渡すなら、アドレス・actions・件数だけのローカル要約にする
- ログと一時ファイルをジョブ終了時に破棄する
AIにレビューさせるために秘密を外へ出したら、本末転倒なんです。
チェック7: applyを人間承認の後ろへ置く
自動化の最後に、責任の境界を残します。
name: terraform-plan
on:
pull_request:
paths:
- "infra/**"
permissions:
contents: read
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4 # 本番運用ではcommit SHA固定を検討
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: terraform init -backend=false
- run: terraform validate
- run: echo "実planは認証済みの隔離環境で生成し、適用は別jobの承認後に行う"
この例は構造を示す最小形で、実クラウドplanとapplyは動作未確認です。provider認証、backend、lock、environment protection、actionのSHA固定は自分の環境に合わせて設計してください。
planとapplyを同じ無承認jobで連続実行すると、レビューは儀式になります。applyはprotected environmentなどの承認後へ分け、レビューした保存planだけを適用します。
AIへ任せるプロンプト3本
プロンプト1: 変更要約
次のTerraform plan要約をレビューしてください。
生の値やsecretは渡していません。
入力:
- resource address
- actions
- replace_paths
- 変更件数
出力:
1. delete / replace / update別の件数
2. 人間が確認すべき質問
3. 影響を小さくする代替案
4. 不明点
applyの可否は決めず、確認できないことを推測で埋めないでください。
プロンプト2: 例外申請レビュー
Terraformの危険変更ゲートを一時的に例外化する申請をレビューしてください。
必須項目:
- 対象resource
- delete / replaceが必要な理由
- データ退避
- 停止時間
- rollback
- 例外の期限
- 承認者
欠落項目だけを質問にしてください。
承認・apply・権限変更は実行しないでください。
プロンプト3: PR差分とplanの照合
HCLのPR差分と、値を除いたplan要約を照合してください。
探すもの:
- PRの意図に書かれていない変更
- delete / replace
- 変更件数の急増
- providerやmodule更新による広範囲変更
- planでは判断できず人間確認が必要な点
事実、推定、未確認を分けて出力してください。
最終判断は人間に残してください。
この方法が効かない条件
planは未来予知ではありません。plan後に外部状態が変わる、provider/APIの挙動が変わる、Terraform管理外の資源へ副作用が出る、といったケースは残ります。
また、terraform testはmoduleやroot moduleを検査できますが、設定によって実リソースを作成し、費用が発生する場合があります。安全な専用アカウントとcleanup確認なしに本番で試すものではありません。
なので高リスク変更では、適用前レッドラインに加えて次も必要です。
- 非本番で先に適用する
- 小さな単位へ分割する
- backupとrollbackを実際に確認する
- apply後の監視と停止条件を決める
逆に、使い捨ての個人検証環境で、消失を許容できる場合は7つ全部が重いこともあります。その場合でも、delete/replaceの確認と秘密情報の扱いだけは残す。軽量化と無防備は別です。
今日10分でやること
まずは記事のダミーJSONとcheck_plan.pyを保存し、終了コード1を確認してください。その次に、手元の 非本番planを1本だけ 同じチェッカーへ通します。
全部を一日で導入しなくても大丈夫です。未来の自分がapplyボタンの前で「これ、ほんまに消してええんやったっけ」と悩む時間を、今日の小さな境界が省いてくれます。
参考リンク(一次情報・2026-07-25確認)
- terraform plan command
- terraform show command
- JSON output format
- terraform validate command
- terraform test command
生成AI活用エンジニア&3児のパパ。AI×開発の実践知を毎日発信しています → X