Claude CodeでAWS CLIの--queryを書かせ、JMESPathの結果を実データで検証する
AWS CLIの--queryに渡すJMESPathをClaude Codeに書かせるときの権限設定と、--output jsonとの突き合わせ手順。Tagsが無いリソースが黙って消える落とし穴も実測で示します。
AWS CLIの--queryはJMESPathという式でJSONを絞り込む仕組みです。式は短いのに癖が強く、Claude Codeに書かせると「それらしい式が通るのに件数が合わない」状態になりがちです。対策は式の巧拙ではなく、--output jsonで取った全体と--queryの結果を件数まで突き合わせる検証ループをClaude Codeに回させることです。
この記事では、読み取り専用の権限設定、CLAUDE.mdに置く規約、突き合わせの手順、そしてnullと空配列で結果が欠ける落とし穴を順に示します。
Claude Codeに任せる範囲を読み取りだけに絞る
--queryの検証には、実際にAWSへ問い合わせるコマンドを何度も実行します。実行のたびに承認を求められると回らないので、読み取り系だけを許可します。許可ルールは.claude/settings.jsonに書きます。
{
"permissions": {
"allow": [
"Bash(aws sts get-caller-identity)",
"Bash(aws ec2 describe-instances *)",
"Bash(aws ec2 describe-volumes *)"
]
}
}ルールは先頭から固定の語で一致を見ます。*はサブコマンドの後ろに置きます。Bash(aws * describe-instances)のように*を前に置くと起動時に警告が出る書き方です。
注意点が一つあります。Bash(aws *)のような広い拒否ルールは、より狭い許可ルールがあっても優先されます。拒否は許可より先に評価され、許可で例外を切り出すことができません。読み取りだけ通したいなら、広い拒否で囲うのではなく、通したいコマンドだけを許可に並べます。許可に無いコマンドは承認待ちになるので、更新系のaws ec2 terminate-instancesなどは自動では走りません。
権限ルールの確認と見直しは/permissionsコマンドの記事にまとめています。
CLAUDE.mdに検証の約束事を書く
権限を絞っても、Claudeが式を一発で書いて結果を信じてしまえば同じ失敗が起きます。リポジトリのCLAUDE.mdに次のような規約を置いておくと、毎回の指示を省けます。
## AWS CLI の --query を書くとき
- 先に `--output json` で全体を取り、work/ 配下に保存する
- 式を書いたら、全体 JSON の件数と `--query` 結果の件数を両方報告する
- 件数が合わないときは、欠けた要素の ID を jq で列挙して原因を述べる
- `--output text` は使わない。JSON で確認し、整形は最後に行う
- JMESPath のリテラルはバッククォートなので、シェルではシングルクォートで囲む最後の1行は実際によく崩れる箇所です。JMESPathの比較値は`running`のようにバッククォートで書きます。ダブルクォートで囲むとシェルがバッククォートを展開してしまいます。
--output textを避けるのにも理由があります。AWS CLIのユーザーガイドによると、--output textではページごとに出力が作られ、--queryはそのページごとに実行されます。そのため各ページの最初の一致要素が余分に出ることがあります。json・yaml・yaml-streamでは全体を1つの構造として処理してからクエリを1回だけ適用します。件数を数える検証ではこの差が効きます。
全体を先に取り、件数を突き合わせる
検証の流れは3段階です。
--queryを検証する3段階
- 1
全体を保存する
--queryを付けずに--output jsonで取得し、ファイルに残します。 - 2
式を書いて結果を取る
Claude Codeに式を書かせ、同じ取得を
--query付きで実行します。 - 3
件数と欠落を突き合わせる
全体JSONを
jqで数え、--queryの結果と一致するか確かめます。
指示の例は次のとおりです。
EC2のrunning状態のインスタンスIDを--queryで抜き出して。
先に--output jsonで全体をwork/instances.jsonに保存し、
jqで数えた件数と--queryの件数を両方報告して。例えば次のようなコマンドの並びになります。
aws ec2 describe-instances --output json > work/instances.json
jq '[.Reservations[].Instances[]] | length' work/instances.json
jq '[.Reservations[].Instances[]
| select(.State.Name == "running")] | length' work/instances.json
aws ec2 describe-instances \
--query 'Reservations[].Instances[?State.Name==`running`][].InstanceId'jq側の件数と--query側の件数が一致すれば、その式は少なくとも状態の絞り込みでは取りこぼしていません。一致しないときは、式ではなく「どの形のデータで落ちているか」を見る合図です。次の節で、落ちる典型例を実測で示します。
非対話で回したいならclaude -pが使えます。標準入力と標準出力はUnixの道具と同じように扱えるので、保存した全体JSONを渡して式の検証だけを頼む運用もできます。--allowedToolsの指定はclaude -pの記事で扱っています。なお、標準入力に流した内容はモデルに送られます。インスタンスのタグや名前に機密が含まれる環境では、渡す範囲を決めてから使ってください。
nullと空配列で結果が欠ける落とし穴
AWS CLI 2.15.29に同梱されているJMESPath 1.0.1のjp.pyで、手元の架空データに式を当てて挙動を確かめました。データはdescribe-instancesの形をまねた4インスタンスで、Tagsの有無をわざとばらつかせています。
| InstanceId | State | Tags |
|---|---|---|
| i-0a1 | Staterunning | TagsName=web-1, Env=prod |
| i-0a2 | Staterunning | Tagsなし |
| i-0b1 | Statestopped | TagsEnv=dev |
| i-0b2 | Staterunning | TagsName=batch-1, Env=prod |
i-0a2にはTagsキー自体がありません。この状態で式を変えて当てた結果が次のとおりです。
| 式 | 結果 |
|---|---|
Reservations[*].Instances[*].InstanceId | 結果[["i-0a1","i-0a2"],["i-0b1","i-0b2"]] |
Reservations[].Instances[].InstanceId | 結果["i-0a1","i-0a2","i-0b1","i-0b2"] |
Reservations[].Instances[?State.Name==`running`].InstanceId | 結果[["i-0a1","i-0a2"],["i-0b2"]] |
Reservations[].Instances[].Tags[?Key==`Name`].Value | 結果[["web-1"],[],["batch-1"]] |
Reservations[].Instances[].Tags[?Key==`Name`].Value[] | 結果["web-1","batch-1"] |
[*]と[]で配列の入れ子が変わる
1行目と2行目は同じデータで形が違います。Reservations[*]は予約ごとの配列を残すので二重の配列になり、Reservations[]は平らにします。Claudeが[*]で書いた式の結果をjqで数えると、4件ではなく2件に見えます。件数の突き合わせで最初に当たる差はこれです。
フィルタの結果は予約ごとに分かれる
[?State.Name==`running`]をインスタンス側に付けた式の結果は、2つの予約ごとに分かれた配列です。runningは3件なのに、外側の長さは2です。最後に[]を足すか、| []のようにパイプで平らにしてから数えます。
Tagsが無い要素は黙って消える
4行目が肝心です。タグ付きの3インスタンスのうちi-0a1とi-0b2は値が返りますが、i-0b1はNameタグが無いので空の[]になり、i-0a2はTagsキー自体が無いので結果から要素ごと消えています。出力は3要素で、インスタンス数4とも、Nameを持つ2とも合いません。
5行目のようにValue[]で平らにすると["web-1","batch-1"]となり、2件が見えるだけです。Nameが無いインスタンスが2つあることは、結果のどこにも現れません。「名前の一覧を出して」と頼まれた式がこの形で、件数を見なければ気づけない典型です。
欠落を見える形にする
Nameが無い要素も含めて並べたいときは、マルチセレクトハッシュを使います。
Reservations[].Instances[].{Id: InstanceId, Name: Tags[?Key==`Name`] | [0].Value}実測の結果は次のとおりで、タグが無い要素はnullとして残ります。
[{"Id":"i-0a1","Name":"web-1"},
{"Id":"i-0a2","Name":null},
{"Id":"i-0b1","Name":null},
{"Id":"i-0b2","Name":"batch-1"}]4インスタンスが4行出るので、件数が一致します。nullを含む行を数えれば、Nameが付いていないインスタンスの数も出ます。式を頼むときは「欠落は捨てずにnullで残す形にして」と条件に入れておきます。
「持たない」を選ぶ式はnot_nullで書く
欠落の逆、つまり「あるタグを持たないリソース」を選ぶ場面でも同じ罠があります。AWS CLIのユーザーガイドは、Value != `test`のような単純な式では除外できないと書いています。ボリュームには複数のタグが付くため、test以外のタグが1つでもあればそのボリュームが残るからです。
ユーザーガイドが示す除外の式はnot_null関数を使います。
aws ec2 describe-volumes \
--query 'Volumes[?!not_null(Tags[?Value == `test`].Value)] | []'内側のTags[?Value == `test`].Valueは、testタグを持つボリュームでは値の配列を返します。testタグを持たないボリュームでは、Tagsがあっても空配列、Tagsキー自体が無ければnullを返します。どちらもJMESPathでは偽として扱われるので、not_nullで包んで!で反転すると、testタグを持たないボリュームだけが残ります。末尾の| []は結果を平らにする指定です。
この手の式は、Claudeが自然に書く!=の形と見た目が近く、実行しても構文エラーにならず、それらしい配列が返ります。だからこそ、除外の式は「除外前の件数 − 除外後の件数 = testタグを持つ件数」をjqで別に数えて照合させます。
件数は式の中でも数えられる
突き合わせのたびにjqを呼ぶ代わりに、JMESPathのlength関数で--queryの中で件数を返す手もあります。AWS CLIのユーザーガイドにもlength(Volumes[?Iops > `1000`])の形の例があります。今回の架空データなら、次の2式の結果が一致するかを見ます。
length(Reservations[].Instances[]) → 4
length(Reservations[].Instances[?State.Name==`running`][]) → 31つ目は全インスタンス数、2つ目はrunning数です。ここでも[]で平らにしてからlengthに渡す点が要です。平らにしないと、予約の数である2が返ります。式の中で数えれば、jqを使えない環境でも同じ照合ができます。ただし、数えられるのは式が見ている範囲だけです。キーが無いために消えた要素は、lengthでも数えに入りません。全体JSONの件数と並べる手順は省けません。
取得量を減らすなら--filtersと組み合わせる
--queryはAWS CLI側で動くクライアント側フィルタです。サービスから返ってきたデータ全体に対して式を当てるので、大きなデータでは重くなることがあります。AWS CLIのユーザーガイドは、コマンドが--filtersのようなサーバー側フィルタを持つなら、先にそれで絞り、残りを--queryで整える構成を示しています。
aws ec2 describe-volumes \
--filters "Name=availability-zone,Values=us-west-2a" "Name=status,Values=attached" \
--query 'Volumes[?Size > `50`].{Id:VolumeId,Size:Size,Type:VolumeType}'検証の観点では、--filtersを足すと「全体JSON」の範囲も変わる点に注意します。件数を突き合わせるときは、全体JSONも同じ--filters付きで取得します。フィルタを付けない全体と比べると、式の誤りではなくフィルタの効果が差として出てしまいます。CLAUDE.mdの規約に「--filtersを使うときは、全体JSONの取得にも同じ--filtersを付ける」と1行足しておくと、取り違えを防げます。
同じ検証の型は、データベースや変換処理でも使えます。クエリの結果を別の経路で数え直す考え方は、ClaudeでスロークエリをEXPLAIN ANALYZEで絞る記事にも出てきます。AWSのサービス側をClaude Codeに設計させる流れはaws-serverlessプラグインの記事が扱っています。
式を直させるときの頼み方
件数が合わなかったときは、式をいきなり書き直させず、原因の特定を先にさせます。
全体JSONは4件、--queryの結果は3件だった。
欠けた要素のInstanceIdをjqで特定し、
欠けた理由(キーが無い/空配列/条件不一致)を分類してから式を直して。「理由を3つに分類する」という指定が効きます。キーが無い、空配列、条件に合わないでは、直し方が違うからです。キーが無いならnullを残す式に、空配列なら[0]でパイプしてnullに寄せる式に、条件不一致なら正しい除外として何も直さない、という分岐になります。
まとめ
--queryの式は、通ることと合っていることが別です。Claude Codeに式を書かせるなら、読み取り専用の許可ルールで安全に実行させ、全体JSONと--queryの件数を毎回並べて報告させる規約をCLAUDE.mdに置くのが近道です。落とし穴の中心は、Tagsのような任意のキーが無いとき、結果から要素が黙って消える点にあります。件数が合わないと分かったら、欠落をnullで残すマルチセレクトハッシュに式を変えるのが最初の一手です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。