jqを活用した複数JSON配列の共通キー結合とデータ構造再構築パイプライン

Tech
{"schema_version": "1.0", "article_type": "technical_guide", "target_audience": "SRE/DevOps Engineers", "tool_stack": ["jq", "bash", "curl"], "security_level": "High", "portability": "High"}

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

jqを活用した複数JSON配列の共通キー結合とデータ構造再構築パイプライン

【導入と前提】

異なるマイクロサービスやデータソースから取得した複数のJSON配列を、共通の識別子(キー)で高速かつ安全に結合し、分析や後続処理に適した新しい構造のJSONドキュメントを生成するオペレーションを堅牢化します。

前提条件と実行環境:

要素 要件 バージョン推奨
OS GNU/Linux互換環境(Bash 4.x以上) Ubuntu/RHEL/AlmaLinux
コマンド シェル環境標準ツール bash, coreutils
JSON処理 高度なフィルタリングと結合機能 jq (v1.6以上推奨)
通信 データ取得 curl

【処理フローと設計】

この処理では、ユーザー情報(Dataset A)と、ユーザーに紐づく権限情報(Dataset B)の2つの配列を、ユーザーIDを共通キーとして結合し、ネストした形式(Joined Output)に再構築します。

graph TD
    A["Dataset A (Users)"] -->|Input 1| C
    B["Dataset B (Permissions)"] -->|Input 2| C
    C("jq Processing") -->|Step 1: Create B Map| D{"Lookup Table (B)"}
    C -->|Step 2: Iterate A| E["Map A"]
    D -->|Step 3: Join| E
    E --> F["Joined Output"]
    F --> G("Validation/Next Step")

【実装:堅牢な自動化スクリプト】

本スクリプトでは、一時ファイルを安全に扱い、外部プロセス(curl)の失敗を確実に捕捉し、データの完全な結合処理を実行します。

結合スクリプト本体 (join_and_reconstruct.sh)

#!/usr/bin/env bash

# 堅牢なシェルスクリプトのための標準設定

set -euo pipefail  # 未定義変数、エラー、パイプライン失敗時に即座に終了
IFS=$'\n\t'        # スペース区切りの問題を回避

# --- 設定 ---

API_USER_ENDPOINT="https://api.example.com/users"
API_PERM_ENDPOINT="https://api.example.com/permissions"

# 認証トークンやAPIキー(環境変数から取得)

AUTH_TOKEN="${JSON_JOIN_API_KEY:?Error: JSON_JOIN_API_KEY environment variable not set}"

# 一時ファイルの設定

TMP_DIR=$(mktemp -d -t json-join-XXXXXX)
USER_JSON="${TMP_DIR}/users.json"
PERM_JSON="${TMP_DIR}/permissions.json"

# --- トラップ関数:エラー時および終了時のクリーンアップ ---

function cleanup {

    # 一時ディレクトリが存在し、かつ安全なパスであることを確認して削除

    if [[ -d "${TMP_DIR}" ]]; then
        rm -rf "${TMP_DIR}"
        echo "Cleaned up temporary directory: ${TMP_DIR}" >&2
    fi
}

# 終了時、中断時、エラー時にcleanupを実行

trap cleanup EXIT INT TERM

# --- データ取得関数 ---


# curlの堅牢化:リトライ、エラーチェック、進捗非表示

fetch_data() {
    local url=$1
    local output_file=$2
    local data_type=$3

    echo "Fetching ${data_type} data from ${url}..." >&2

    # -s: サイレント, -S: エラー時のみ表示, -L: リダイレクト追従, --fail: 4xx/5xxで非ゼロ終了


    # --retry 3: 最大3回リトライ

    if ! curl -sS -L --fail --retry 3 -H "Authorization: Bearer ${AUTH_TOKEN}" "${url}" -o "${output_file}"; then
        echo "ERROR: Failed to fetch ${data_type} data from ${url}" >&2
        exit 1
    fi

    # 取得したデータが有効なJSONであるかを jq で検証

    if ! jq empty < "${output_file}"; then
        echo "ERROR: Fetched file is not valid JSON: ${output_file}" >&2
        exit 1
    fi
    echo "${data_type} data fetched successfully." >&2
}

# --- メイン処理 ---

fetch_data "${API_USER_ENDPOINT}" "${USER_JSON}" "User"
fetch_data "${API_PERM_ENDPOINT}" "${PERM_JSON}" "Permission"

# jqによるJSON結合処理


# Goal: [


#   {


#     "id": 101,


#     "name": "Alice",


#     "permissions": ["read_s3", "write_ec2"]


#   },


#   ...


# ]

JQ_FILTER='

    # 1. Dataset B (Permissions)を読み込み、共通キー(user_id)をキーとしたルックアップマップ $perm_map を作成


    # 権限配列を user_id ごとにグループ化


    # reduce .[] as $item: 配列をイテレートし、初期オブジェクト {} に集約していく

    $perm_data | reduce .[] as $item ({}; 

        # $item.user_id をキーとして、既存の値に $item.permission を追加

        .[tostring($item.user_id)] += [$item.permission]
    ) as $perm_map
    |

    # 2. Dataset A (Users)を読み込み、マップを参照して結合

    .[] | 

    # .id を文字列に変換して $perm_map を参照(結合キーの型を合わせる)


    # // []: 該当キーがない場合、空の配列をデフォルト値として使用

    . + {
        "permissions": ($perm_map[tostring(.id)] // [])
    }
'

echo "Starting JSON join and reconstruction..." >&2

# jqの引数に一時ファイルを読み込ませ、結合を実行


# --slurpfile: 変数名 $perm_data にファイル全体を配列として読み込む


# USER_JSONを標準入力で受け取り、結果を標準出力へ出力

jq -c \
    --slurpfile perm_data "${PERM_JSON}" \
    "${JQ_FILTER}" \
    "${USER_JSON}"

echo "Processing complete. Joined JSON outputted to stdout." >&2

サンプルデータ構造(テスト用)

ファイル 内容
users.json [{"id": 101, "name": "Alice"}, {"id": 102, "name": "Bob"}]
permissions.json [{"user_id": 101, "permission": "read_s3"}, {"user_id": 101, "permission": "write_ec2"}, {"user_id": 103, "permission": "admin"}]

【検証と運用】

正常系の確認コマンド

スクリプトを実行し、結果が標準出力に出力されます。

# 環境変数を設定(テスト目的のためダミー値を設定)

export JSON_JOIN_API_KEY="dummy_token_12345"

# スクリプトを実行し、最初の数行を確認

./join_and_reconstruct.sh | head -n 3 | jq .

期待される出力(結合され、権限がネストされていること):

[
  {
    "id": 101,
    "name": "Alice",
    "permissions": ["read_s3", "write_ec2"]
  },
  {
    "id": 102,
    "name": "Bob",
    "permissions": []
  }
]

※Bobには対応する権限データがないため、permissions は空配列となります。

エラー時のログ確認方法

スクリプト内で echo "ERROR: ..." を使用しているため、エラー情報は標準エラー出力 (>&2) に出力されます。

# curlが失敗する状況をシミュレート


# 環境変数が未設定の場合

unset JSON_JOIN_API_KEY
./join_and_reconstruct.sh

# 標準エラー出力に以下のメッセージが出力される


# Error: JSON_JOIN_API_KEY environment variable not set


# Cleaned up temporary directory: /tmp/json-join-XXXXXX

【トラブルシューティングと落とし穴】

1. 権限問題と環境変数

  • 権限:スクリプトがアクセスするAPIエンドポイントに対する認証(AUTH_TOKEN)が適切であることを確認してください。機密情報である認証トークンは、コード内にハードコードせず、必ず環境変数やシークレットマネージャー(AWS Secrets Manager, Vaultなど)から取得してください。

  • 環境変数の漏洩防止set -a などで環境変数全体を誤って子プロセスに渡すことのないよう注意し、本スクリプトでは必要な変数のみを明示的に使用しています。

2. データ型の不一致

  • 結合キーの型:JSONでは数値(Number)と文字列(String)は厳密に区別されます。jq 内部で結合キーを参照する際 (tostring(.id))、参照元と参照先のデータ型を必ず一致させる必要があります。APIの出力によっては、IDが数値(101)であったり文字列("101")であったりするため、結合の失敗を防ぐために tostring() を積極的に使用します。

3. 一時ファイルのクリーンアップ

  • 本実装では、mktemp -d で安全な一時ディレクトリを作成し、trap cleanup EXIT INT TERM でスクリプトが正常終了、中断、エラー終了のいずれであっても確実に rm -rf が実行されるように設計しています。これにより、機密データがディスク上に残り続けるリスクを最小化します。

【まとめ】

この自動化されたデータ結合パイプラインは、以下の3つのポイントにより、運用の堅牢性と冪等性を維持します。

  1. アトミックなクリーンアップの保証(Trap)trap コマンドにより、予期せぬ中断やエラー発生時でも一時ファイルとディレクトリが確実に削除され、システム状態の副作用を残しません。

  2. 外部依存性の堅牢化(Curl Fail/Retry)curl --fail --retry オプションにより、ネットワークの一時的な不安定さやAPIエラーに対して耐性を持ち、再実行の際もデータ取得が冪等に試行されます。

  3. データ型に依存しない結合処理(jq tostring)jq フィルタ内で結合キーを明示的に文字列に変換することで、異なるAPIが返すデータ型の揺らぎを吸収し、安定した結合ロジックを提供します。 “`

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました