本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
jqを駆使したAPIレスポンスの高度な集計:group_byとreduceによるインベントリ解析の自動化
【導入と前提】
本スクリプトは、クラウドインフラのAPIやマイクロサービスから取得した大量のJSON配列を、特定のキー(環境、サービス名、ステータス等)ごとにグループ化し、合計値やカウントを算出する処理を自動化・堅牢化します。
実行環境: GNU/Linux (Ubuntu/RHEL/CentOS)
前提ツール:
jq(v1.6以上推奨),curl(v7.40以上)対象: 構造化されたJSONログや、クラウドプロバイダーのインベントリデータ
【処理フローと設計】
graph TD
A["API/JSON Raw Data"] --> B{"jq Process"}
B --> C["group_by: 特定キーで配列を多次元化"]
B --> D["reduce: 単一パスでの累積・集計"]
C --> E["map: 各グループの要約オブジェクト作成"]
D --> F["Final Report: カテゴリ別合計/集計値"]
E --> F
F --> G["stdout / Log File"]
group_byは直感的な記述が可能ですが、内部でソートを行うため計算量は $O(N \log N)$ となります。一方、reduceは1スキャン($O(N)$)で処理可能なため、大規模データのストリーム処理に適しています。
【実装:堅牢な自動化スクリプト】
以下は、ダミーのインベントリデータから「ステータスごとのカウント」と「コストの合計」を算出する実戦的なスクリプトです。
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Script: aggregate_inventory.sh
# Description: jqのgroup_byとreduceを用いた高度なJSON集計
# -----------------------------------------------------------------------------
set -euo pipefail
IFS=$'\n\t'
# 一時ファイルのクリーンアップ設定
TMP_FILE=$(mktemp)
trap 'rm -f "$TMP_FILE"' EXIT
# --- 1. データ取得 (ダミーAPIレスポンスの生成) ---
cat << 'EOF' > "$TMP_FILE"
[
{"id": "v-001", "status": "running", "cost": 150, "region": "ap-northeast-1"},
{"id": "v-002", "status": "stopped", "cost": 10, "region": "us-east-1"},
{"id": "v-003", "status": "running", "cost": 200, "region": "ap-northeast-1"},
{"id": "v-004", "status": "running", "cost": 150, "region": "us-east-1"},
{"id": "v-005", "status": "terminated", "cost": 0, "region": "ap-northeast-1"}
]
EOF
echo "=== [1] group_by によるリージョン別集計 ==="
# group_by(.key) は指定キーで配列を分割し、[ [obj, obj], [obj] ] の形にする
jq -r '
group_by(.region) | map({
region: .[0].region,
count: length,
total_cost: map(.cost) | add
})
' "$TMP_FILE"
echo -e "\n=== [2] reduce によるステータス別の高度な集計 ==="
# reduce はメモリ効率が良く、初期値 ( {} ) に対して逐次更新を行う
jq -r '
reduce .[] as $item (
{};
.[$item.status].count += 1 |
.[$item.status].total_cost += $item.cost
) | to_entries | map({
status: .key,
metrics: .value
})
' "$TMP_FILE"
# 補足: 実戦では curl を以下のように利用
# curl -sfL --retry 3 --retry-delay 5 "https://api.example.com/v1/resources" \
# -H "Authorization: Bearer ${API_TOKEN}" | jq ...
systemd/Timer 設定例(定期集計用)
集計処理をバックグラウンドで定期実行する場合のユニット定義例です。
# /etc/systemd/system/inventory-aggregator.service [Unit] Description=Daily Cloud Cost Aggregator [Service] Type=oneshot ExecStart=/usr/local/bin/aggregate_inventory.sh EnvironmentFile=/etc/default/cloud-api-creds User=reports-user # /etc/systemd/system/inventory-aggregator.timer [Timer] OnCalendar=daily Persistent=true [Install] WantedBy=timers.target
【検証と運用】
正常系の確認
スクリプトを実行し、以下のJSON構造が出力されることを確認します。
# 実行
./aggregate_inventory.sh
# 期待される出力(抜粋)
# [
# { "status": "running", "metrics": { "count": 3, "total_cost": 500 } },
# ...
# ]
ログ確認方法
systemd経由で実行している場合は、journalctl を使用して、パイプラインの途中でエラーが発生していないか確認します。
# ユニットの最新ログを確認 journalctl -u inventory-aggregator.service -n 50 --no-pager
【トラブルシューティングと落とし穴】
データ型の不一致:
reduce内で加算を行う際、costが文字列として渡されるとエラーになります。($item.cost | tonumber)を使用して型を明示的に変換してください。大規模データのメモリ消費:
group_byは全データをメモリに展開してソートします。数GB単位のJSONを扱う場合は、jq -c '.[]'で各行を分離し、reduceまたは外部のawk等と組み合わせるストリーム処理を検討してください。環境変数の扱い: APIトークンなどを
jqの引数に直接渡すとpsコマンドから見えてしまうため、必ず--argオプション経由で渡すようにします(例:jq --arg key "$VAL" '.[$key]')。
【まとめ】
運用の冪等性と堅牢性を維持するための3つのポイント:
計算効率の選択: 直感的な
group_byか、高速なreduceかをデータサイズに応じて使い分ける。エラーハンドリング:
set -euo pipefailを徹底し、パイプライン途中のjqの失敗を検知できるようにする。型保証:
tonumberやtostringを活用し、入力データがスキーマから外れている場合のクラッシュを未然に防ぐ。

