Claude Media
Claude CodeでAWS CLIの--queryを書かせ、JMESPathの結果を実データで検証する

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. 1

    全体を保存する

    --queryを付けずに--output jsonで取得し、ファイルに残します。

  2. 2

    式を書いて結果を取る

    Claude Codeに式を書かせ、同じ取得を--query付きで実行します。

  3. 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の有無をわざとばらつかせています。

InstanceIdStateTags
i-0a1StaterunningTagsName=web-1, Env=prod
i-0a2StaterunningTagsなし
i-0b1StatestoppedTagsEnv=dev
i-0b2StaterunningTagsName=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`][]) → 3

1つ目は全インスタンス数、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で残すマルチセレクトハッシュに式を変えるのが最初の一手です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.aws.amazon.com/cli/latest/userguide/cli-usage-filter.html
  2. jmespath.org/specification.html
  3. code.claude.com/docs/en/common-workflows
  4. code.claude.com/docs/en/permissions
  5. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn