Claude CodeでPackerのAMI定義を書く — validateとfmtのhookとbuildのdeny
Claude CodeにPackerのHCL2テンプレートを書かせるとき、編集直後にpacker fmt -checkとvalidateをPostToolUse hookで回し、buildはdenyで止める設定をまとめます。
Packerのテンプレートは、書き間違えてもその場では何も起きません。間違いが表に出るのはpacker buildでEC2インスタンスが起動し、プロビジョナーが走り、AMIが作られる段階です。Claude Codeにテンプレートを書かせるなら、buildの前に静的に確かめる層と、build自体を人の手に残す層の2つを先に置いておくのが安全です。
この記事は、packer fmt -checkとpacker validateを編集直後のPostToolUse hookで回し、packer buildをpermissionsのdenyで止める構成を、設定ファイルとスクリプトで示します。例題はAmazon EBSビルダーのHCL2テンプレートです。
Claude Codeに任せる範囲を2段に分ける
Packerのコマンドは、副作用の有無で2つに分かれます。
Packerコマンドの副作用で分ける
自動で回してよい側
packer fmt -checkはフォーマットの確認だけで、ファイルを書き換えません。packer validateはテンプレートの構文と設定を検査し、成功なら終了コード0、失敗なら非0を返します。
人が押す側
packer buildはテンプレート内のビルドを実行して成果物を生成します。ビルド同士は、指定がなければ並列に動きます。AMIやインスタンスなど、クラウド側に実体が残る操作です。
前者はhookに、後者はdenyに割り当てます。Claudeには「書く、検査結果を読む、直す」の反復だけを任せ、実際のイメージ作成は人が実行する形です。
検査対象のテンプレートを用意する
検査の対象になる最小のテンプレートです。required_pluginsでAmazonプラグインを指定し、amazon-ebsソースでAMIを作ります。ソースブロックの項目は、プラグイン側のドキュメントにある使用例に沿っています。
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon"
version = ">= 1.3.0"
}
}
}
source "amazon-ebs" "base" {
region = "us-east-1"
source_ami = "ami-fce3c696"
instance_type = "t2.micro"
ssh_username = "ubuntu"
ami_name = "base-{{timestamp}}"
}
build {
sources = ["source.amazon-ebs.base"]
}source_amiのami-fce3c696はプラグインのドキュメントの使用例にある値で、実在のリージョンやアカウントで使える保証はありません。実際にはsource_ami_filterで、所有者とAMI名のパターンを指定してベースイメージを選ぶ書き方もあります。この選択はClaudeに任せず、CLAUDE.mdに方針として書いておくほうが事故が減ります。
編集直後にfmt -checkとvalidateを回すhook
PostToolUseは、ツール呼び出しが成功した直後に動くイベントです。入力のJSONにはtool_input.file_pathが絶対パスで入ります。Edit|Writeをmatcherにすれば、Claudeがファイルを書いたり直したりするたびに起動します。
設定ファイル
.claude/settings.jsonに次を置きます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/packer-check.sh",
"timeout": 120
}
]
}
]
}
}timeoutは秒単位で、command hookの既定は600秒です。validateが外部サービスを待つ構成でなければ、短めに絞って構いません。
チェックスクリプト
.claude/hooks/packer-check.shの例です。対象は.pkr.hclと.pkrvars.hclだけにして、それ以外のファイルでは何もせず抜けます。
#!/usr/bin/env bash
set -u
f=$(jq -r '.tool_input.file_path // empty')
case "$f" in
*.pkr.hcl|*.pkrvars.hcl) ;;
*) exit 0 ;;
esac
dir=$(dirname "$f")
if ! out=$(packer fmt -check -diff "$f" 2>&1); then
{
echo "packer fmt -check が失敗しました: $f"
echo "$out"
echo "packer fmt で整形してから続けてください。"
} >&2
exit 2
fi
if ! out=$(packer validate "$dir" 2>&1); then
{
echo "packer validate が失敗しました: $dir"
echo "$out"
} >&2
exit 2
fi
exit 0押さえておく点は3つあります。
- 終了コード2を使う: PostToolUseではツールが既に動き終わっているため、exit 2でも編集は取り消されません。代わりにstderrがClaudeに渡ります。exit 0のときのstderrはClaudeに見えません
- fmtは-checkで止める:
-checkは整形済みかどうかを終了コードで返し、書き込みはしません。-diffを足すと差分がstderrに出るので、Claudeは何が崩れているかを読めます - validateはディレクトリで渡す:
sourceとbuildを別ファイルに分けたテンプレートでは、1ファイルだけを渡すより置き場所ごと渡すほうが全体を検査できます。fmtとinitのドキュメントがディレクトリ指定を例示しているのと同じ形です
jqが無い環境では、python3 -cなどで同じ取り出しを書き直します。
hookを組み込んだら、Claudeに任せる前に手元で動作を確かめます。スクリプトに実行権限を付け、標準入力にPostToolUseと同じ形のJSONを渡します。
chmod +x .claude/hooks/packer-check.sh
echo '{"tool_input":{"file_path":"'"$PWD"'/base.pkr.hcl"}}' \
| .claude/hooks/packer-check.sh; echo "exit=$?"整形が崩れたテンプレートでexit=2、整っていて検証も通るテンプレートでexit=0になれば、配線は正しく動いています。
validateの補足オプション
packer validateの主なオプションは次のとおりです。
| オプション | 効果 |
|---|---|
-syntax-only | 効果構文だけを調べ、設定は検証しない |
-var / -var-file | 効果変数を渡す。複数回指定できる |
-evaluate-datasources | 効果データソースも評価する(HCL2のみ) |
-only / -except | 効果名前で対象のビルドを絞る・外す |
-no-warn-undeclared-var | 効果未宣言変数の警告を消す |
変数に既定値が無いテンプレートでは、validateに値を渡す必要があります。hookのコマンドをpacker validate -var-file=vars.pkrvars.hcl "$dir"のように変えるか、テンプレート側に既定値を持たせます。
-evaluate-datasourcesは既定のhookには入れないでください。データソースが外部サービスから値を取る場合、サービスが操作ごとに課金する形なら、検証だけで費用が発生しうるとドキュメントが警告しています。
packer buildをdenyで止める
.claude/settings.jsonのpermissionsに、buildを止める規則を足します。
{
"permissions": {
"deny": [
"Bash(packer build *)",
"Bash(packer init -force *)"
],
"allow": [
"Bash(packer fmt *)",
"Bash(packer validate *)"
]
}
}規則の書き方には2つの性質があります。末尾の*の前に空白がある形は、引数なしの素のコマンドにも一致します。Bash(packer build *)はpacker build単体もpacker build -only=base .も止めます。もう1つは優先順位で、denyは広く書くと、より狭いallowがあっても勝ちます。allowでdenyの例外は作れません。
packer initはプラグインをダウンロードする操作です。既定のインストール先はUnixなら$HOME/.config/packer/pluginsで、PACKER_PLUGIN_PATHで変えられます。すべてのinitを止める必要はありませんが、-forceはプラグインを再インストールするため、上の例ではdenyに入れています。
落とし穴
hookはBashで書かれたファイルを見ない
PostToolUseのmatcherをEdit|Writeにしていると、Claudeがsed -iやリダイレクトでテンプレートを書き換えた場合には、hookが動きません。検査を抜けるのはこの経路です。.pkr.hclを直接書き換えるBashコマンドが気になるなら、Bash向けのPreToolUseでsed -i *.pkr.hclを止めるか、CLAUDE.mdに「HCLはEditかWriteで編集する」と書いておきます。
buildを人が押すときに知っておくこと
denyで止めたあと、人がpacker buildを実行する場面でも、失敗時の挙動は押さえておくと役立ちます。-on-errorの既定はcleanupで、一時ファイルと仮想マシンを片付けます。abortは後始末をせずに終了するため、次のビルドで-forceが要ることがあります。デバッグでabortを選ぶと、EC2インスタンスが残ります。
-forceは、前回の成果物がビルドの邪魔をするときに、ビルダーが前回の成果物を消して実行させるオプションです。具体的な挙動はビルダー次第なので、AMIを消す可能性を考えて、Claudeにこのフラグを付けさせない運用が無難です。
テンプレートを書かせるときのCLAUDE.md
hookとdenyは「やってはいけない」を機械で止める層です。「どう書くか」はCLAUDE.mdに寄せます。
## Packer
- テンプレートはHCL2(*.pkr.hcl)で書く。JSONテンプレートは新規に作らない
- 編集後はhookが packer fmt -check と packer validate を実行する。失敗したら出力を読んで直す
- packer build は実行しない。確認したいときは validate の結果を報告する
- source_ami は固定IDでなく source_ami_filter で選ぶ
- 認証情報をテンプレートに書かない。環境変数かプロファイルを使うJSONテンプレートを避ける理由の1つは、validateの-evaluate-datasourcesが、JSONテンプレートにデータソースが存在しないため無視される点です。HCL2とJSONの機能差が、検査の効き方に表れます。既存のJSONはhcl2_upgradeコマンドでHCLへ変換できます。
packerが入っていない環境での挙動
スクリプトはpackerの有無を確かめていません。未導入のマシンではpacker fmtがコマンド未検出で失敗し、exit 2でClaudeに「失敗」が戻り続けます。チームで共有するリポジトリなら、先頭にcommand -v packer >/dev/null || exit 0を足して、未導入なら素通りさせる選択肢があります。その代わり、検査されたのか素通りしたのかが見えなくなるので、READMEに導入手順を書く運用と組み合わせます。
プラグインも同じです。required_pluginsで宣言したプラグインはpacker initで入れます。ドキュメントは、新規・既存を問わずテンプレートで作業を始めるときの最初の手順としてinitを勧めており、何度実行しても既に入っているプラグインは消えません。ClaudeがHCLを書く前に、人がinitを1回実行しておく手順にしておくと、validateの失敗がプラグイン未導入由来か、テンプレートの誤りかを切り分けやすくなります。
packer initが対象にするのはpkr.hclで終わるテンプレートで、変数定義ファイルには.pkrvars.hclか.auto.pkrvars.hclの拡張子が推奨されています。この拡張子にしておくと、ディレクトリでinitを実行したときに変数定義ファイルは処理対象から外れます。hookのcaseが.pkr.hclと.pkrvars.hclの両方を拾うのは、この命名を前提にしています。
失敗の読み方
hookが失敗を返したとき、Claudeに渡る文字列は2種類です。
| 失敗 | 出力の特徴 | 次の手 |
|---|---|---|
| fmt -check | 出力の特徴整形が崩れたファイル名と差分が出る | 次の手packer fmt <ファイル>を実行させる |
| validate | 出力の特徴構文エラーか、必須項目の不足が出る | 次の手該当のブロックを直させる |
fmtの失敗は機械的に直せるので、Claudeにpacker fmtを実行させれば済みます。validateの失敗は内容を読む必要があります。たとえばドキュメントの例にある「Either a path or inline script must be specified」は、スクリプトのパスかインラインのスクリプトが指定されていないという指摘で、直す対象がエラー文から分かります。
同じhook設計は、他の静的解析ツールにも使い回せます。シェルならShellCheckのhook、Goならgolangci-lintのhookが同じ型です。hookが起動できなかったときの扱いを決めたいなら、onFailureの設定も合わせて確認してください。生成物を触らせないdenyの考え方はGraphQL Codegenの記事にもあります。
まとめ
Packerの記事で押さえるべき線は、fmtとvalidateを自動、buildを人の手に分けることです。fmtとvalidateはhookで編集のたびに回り、失敗はexit 2でClaudeに戻ります。buildはdenyで止まり、AMIという実体が生まれる操作は人の確認を挟みます。残る穴はBash経由の書き換えと、変数や認証のあるテンプレートでのvalidateの落ち方で、前者はCLAUDE.mdとPreToolUse、後者は-var-fileで埋められます。