Claude Media
Claude CodeでTerragruntのrun --allを絞る — plan許可とapply確認

Claude CodeでTerragruntのrun --allを絞る — plan許可とapply確認

Claude CodeにTerragruntを触らせるとき、run --allの対象を--filterで絞らせ、planだけ許可・applyは確認・destroyは拒否にする権限設定とフックの書き方です。

Claude CodeにTerragruntを任せるときの危険は、terragrunt run --allが1コマンドで複数のユニットに同じ操作を流す点にあります。対策は3つです。対象を--filterで絞らせる。planは許可し、applyは毎回確認、destroyは拒否する。そして--filterの付いていない--allはフックで止める。この記事では、それぞれの設定をsettings.jsonとCLAUDE.mdの断片で示します。単一スタックのterraform plan差分を検証する手順はTerraformのコードを書く手順にあり、ここでは複数ユニットへの波及を扱います。

run --allのどこが危ないのか

terragrunt run --allは、現在のスタックにあるすべてのユニットに指定のOpenTofu/Terraformコマンドを実行します。terragrunt run --all planなら、全ユニットのplanです。Claudeが「全体を確認しておきます」と書いた1行が、そのまま数十ユニットの実行になります。

危険を増やすのは、applyとdestroyの挙動です。

注意点

--allで効いてしまう3つの挙動

  • 確認なしで走る

    run --allでapplyかdestroyを実行すると、Terragruntは-auto-approveを自動で付けます。ユニットごとの承認が標準入力を共有する都合で成り立たないためです。Terragrunt側の確認画面は安全装置になりません。

  • 依存先まで巻き込む

    run --all destroyは、現在のディレクトリ以下のユニットだけでなく、その依存先も既定で破棄します。守りたいディレクトリは、除外の指定(--exclude-dirをディレクトリごとに付ける)で外します。

  • 失敗しても続ける指定がある

    --queue-ignore-errorsは、依存先が失敗しても残りのユニットを処理し続けます。applyやdestroyと組み合わせると、状態が不整合になりうると警告されています。

つまり、Claude Code側の権限設定が事実上の最後の確認になります。ここを緩くしたまま--allを使わせるのは避けたいところです。

権限は「planを許可、applyはask、destroyはdeny」で組む

Claude Codeの権限ルールは、denyが最優先で、次がask、最後がallowの順に評価され、最初に一致したもので結果が決まります。詳細な具体性は順序を変えません。したがって、allowを少し広めに書いても、applyをaskに入れておけば確認が先に出ます。

.claude/settings.jsonの例です。

{
  "permissions": {
    "allow": [
      "Bash(terragrunt find *)",
      "Bash(terragrunt plan *)",
      "Bash(terragrunt run --all * -- plan*)",
      "Bash(terragrunt run --all plan*)"
    ],
    "ask": [
      "Bash(terragrunt *apply*)"
    ],
    "deny": [
      "Bash(terragrunt *destroy*)"
    ]
  }
}

書き方のポイントは3つあります。

  • terragrunt planはterragrunt run planの短縮形で、どちらの形でもClaudeが書きうるため、applyとdestroyは*apply*のようにサブコマンドの位置を固定しない形にします。誤ってapplyを含む名前のフィルタ(--filter 'apply-svc'のような指定)が確認に回る場合がありますが、安全側に倒れる誤りです
  • allowの-- plan*は、末尾の空白なしの*なので-- plan -no-colorのような引数付きにも一致します。一方、terragrunt run --all -- planのように--allの直後に--が続く形は、中間の*に当たる文字がなく一致しないため、確認が出ます。その場合はBash(terragrunt run --all -- plan*)を足してください
  • ルール文字列は、実際にClaudeが書くコマンドで通るかを/permissionsで確かめます。ここでの一致の取り方は、Bash権限ルールが複合コマンドを評価する仕組みも参考になります

cd infra && terragrunt run --all applyのような複合コマンドは、&&で分割した各部分が個別に評価され、askやdenyはどれか1つの部分に一致すれば適用されます。また、TG_NO_AUTO_APPROVE=true terragrunt ...のように先頭に環境変数を置かれても、deny・askのルールはその代入を飛ばして一致します。allowのほうは、既知の安全な変数以外が先頭にあると一致しません。

この設定で止められない書き方

Bashルールが見るのはClaudeが書いたコマンドの文字列だけです。/usr/local/bin/terragrunt run --all applyのようにパスで呼ぶ形や、bash -c 'terragrunt ...'の中身は、プログラム名の一致でルールを通り抜けます。この種の抜け道は、次の節のフックと、Claudeを動かす環境に変更権限を持たせない運用で補います。

--filterを付けない--allをフックで止める

権限ルールは「何を許すか」を決めます。「--allを付けるなら対象を絞る」という条件は、コマンド全文を読むPreToolUseフックのほうが書きやすい領域です。.claude/hooks/guard-terragrunt-all.shを作ります。

#!/bin/sh
cmd=$(jq -r '.tool_input.command')
case "$cmd" in *terragrunt*) ;; *) exit 0 ;; esac
case "$cmd" in
  *--all*)
    case "$cmd" in
      *--filter*) ;;
      *)
        echo "terragrunt run --all には --filter か --filter-affected を付けてください" >&2
        exit 2 ;;
    esac ;;
esac
exit 0

--filter-affectedも--filterの文字列を含むため、同じ分岐を通ります。終了コード2はフックでツール呼び出しを止める合図で、標準エラーの文面がClaudeに返ります。登録はmatcherをBashにして行います。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/guard-terragrunt-all.sh" }
        ]
      }
    ]
  }
}

登録する前に、標準入力へJSONを流して単体で判定を確かめられます。

for c in "terragrunt run --all -- plan" \
  "terragrunt run --all --filter 'prod/**' -- plan" \
  "terragrunt run --all --filter-affected -- plan" \
  "terragrunt plan" "ls -la"; do
  jq -n --arg c "$c" '{tool_input:{command:$c}}' | .claude/hooks/guard-terragrunt-all.sh 2>/dev/null
  echo "$c => exit $?"
done

手元で実行すると、次の結果になります。

terragrunt run --all -- plan => exit 2
terragrunt run --all --filter 'prod/**' -- plan => exit 0
terragrunt run --all --filter-affected -- plan => exit 0
terragrunt plan => exit 0
ls -la => exit 0

--filterの付いていない--allだけが止まり、単一ユニットのterragrunt planは素通りします。--graphも現在のユニットの依存グラフ全体を対象にする複数ユニット実行なので、必要なら同じ形で*--graph*の分岐を足します。

対象の絞り方をClaudeに覚えさせる

止めるだけでは、Claudeは同じ失敗を繰り返します。絞り込みの語彙をCLAUDE.mdに書いておくと、止められる前に正しい形で書くようになります。

# Terragrunt 作業ルール
 
- 複数ユニットを対象にするときは run --all に必ず --filter を付ける
- まず terragrunt find --filter '<式>' で対象ユニットを列挙し、一覧を示してから plan する
- 変更したブランチの影響だけを見るときは --filter-affected を使う
- あるユニットに依存するユニットも見たいときは --filter '...<名前>' を使う
- apply と destroy は実行しない。plan を --out-dir に保存し、実行コマンドを提示して止まる

使う絞り込みの式は、Terragrunt側の仕様に沿っています。

目的書き方の例
ディレクトリで絞る書き方の例--filter 'prod/**'
除外する書き方の例--filter '!./test/**'
変更の影響だけ書き方の例--filter-affected([main...HEAD]と同じ)
依存する側も含める書き方の例--filter '...vpc'
依存先も含める書き方の例--filter 'service...'
条件を重ねる書き方の例--filter './prod/** | type=unit | !name=legacy'

--filter-affectedはplanかapplyのどちらかと組み合わせる必要があり、-destroyは付けられません。--filterを複数回書くとOR条件になるので、「AとBだけ」を指定するときは--filter 'app1' --filter 'app2'と並べます。

findは実行ではなく対象の列挙なので、allowに入れておくと「対象を確かめてからplan」の順を踏ませやすくなります。同じ「影響範囲だけ検証する」考え方は、dbtのbuild --selectを回す手順にもあります。

planを保存し、applyは同じ--filterで人が実行する

applyを人に残す場合、Claudeにplanを保存させておくと、実行するコマンドが明確になります。--out-dirは、スタック実行でユニットごとのplanファイルを書き出し、applyがそれを読むためのディレクトリです。

# Claudeが実行する(plan と保存)
terragrunt run --all --filter 'prod/**' --out-dir /tmp/tfplan -- plan
 
# 人が内容を読んでから実行する(同じ --filter と --out-dir を付ける)
terragrunt run --all --filter 'prod/**' --out-dir /tmp/tfplan -- apply

注意点は、--out-dirがapplyの対象ユニットを絞らないことです。planを--filterで絞ったなら、applyにも同じ--filterを付けないと、絞られていないユニットまで探索され、plan保存先に見つからず失敗します。人が実行する行をClaudeに一緒に提示させると、フィルタの付け忘れを防げます。

planが失敗するときの原因

run --all planは、依存先のユニットが一度もデプロイされていないと失敗します。依存先の出力を解決する元のstateがないためです。対処はdependencyブロックにモック出力を置くことです。Claudeに「planが落ちたので --allを外す」と判断させる前に、この原因を疑う順をCLAUDE.mdに書いておくとよいでしょう。

もう1つはTF_PLUGIN_CACHE_DIRです。run --allのときは設定しないよう案内されていて、同時アクセスの問題が起きるため、Terragruntのプロバイダーキャッシュサーバーを使います。環境変数をシェルの初期化ファイルで設定している環境では、Claudeの実行が不安定になる原因になりえます。

並列数は既定で制限がない

--parallelismを指定しない--allでは、依存先が終わったユニットから制限なく同時に起動します。ユニットが多いスタックでは、クラウドAPIのレート制限に当たったり、Claudeの待ち時間が長引いたりします。planの実行には--parallelismで上限を置くことをルールに入れると、予測しやすくなります。

- run --all の plan では --parallelism 4 を付ける

数値はスタックの大きさとAPIの制限に合わせて決める値で、ここでの4は例です。

変更権限は実行環境に持たせない

権限ルールもフックも、Claudeが書いたコマンドの文字列を見る仕組みです。文字列を組み立てる呼び出しまでは網羅できません。最後の砦として、Claudeを動かすシェルの認証情報にはplanに必要な読み取り権限だけを持たせ、applyできる権限はCIのパイプラインに置く運用が確実です。Terraform側での同じ考え方はOpenTofuの権限設計でも扱っています。フックの書き方全般はフックの使い方にまとまっています。

くらべる

3つの仕組みの守備範囲

settings.json

権限ルール

Claudeが普通に書くコマンドを、allow・ask・denyで振り分けます。設定は数行で済みますが、パス指定やbash -cの中身は対象外です。

コマンド全文の検査

PreToolUseフック

「--allなら--filterが要る」のような条件を書けます。スクリプトは自分で保守します。

最後の砦

実行環境の認証

変更権限そのものを渡さないので、文字列の抜け道に左右されません。

よくあるつまずき

エラーを無視して先へ進んでしまう

planが一部のユニットで失敗したとき、Claudeが全体を通そうとして--queue-ignore-errorsを付けることがあります。planの段階なら全エラーを一覧する用途で使えますが、applyやdestroyでは状態が不整合になりえます。CLAUDE.mdに「--queue-ignore-errorsはplanのみ」と書き、apply側は権限のaskで人が見るようにします。

--graphで依存先が含まれない

--graphの実行は、現在の作業ディレクトリの外にある依存ユニットを自動では含みません。含めたいときは--queue-include-externalを明示します。Claudeが「依存先も更新されたはず」と思い込まないよう、対象一覧はfindで確かめさせます。

Terragruntの確認を戻したい

--no-auto-approveを付けると、run --allでも-auto-approveが自動では付きません。環境変数ではTG_NO_AUTO_APPROVEで指定できます。ただしClaude Codeから実行する経路では、この確認を人が受けられるとは限りません。確認は権限ルールのaskで受ける設計のままにして、Terragrunt側の確認には頼らないほうが確実です。

まとめ

run --allは、applyとdestroyで確認なしに走る点が最大の落とし穴です。そのため、Claude Code側では「planを許可、applyはask、destroyはdeny」の権限と、--filterのない--allを止めるフックを併用します。絞り込みの式はCLAUDE.mdに書いてClaudeに覚えさせ、実際のapplyは同じ--filterを付けて人が実行する形にすると、影響範囲が読める運用になります。

この記事を共有:XはてブLinkedIn