jqコマンドでJSONを効率処理:安全なAPI連携と定期実行

Tech

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

jqコマンドでJSONを効率処理:安全なAPI連携と定期実行

DevOpsの現場では、Web APIからのデータ取得やログ解析など、JSONデータの処理が日常的に発生します。本記事では、軽量かつ強力なCLIツール jq を用いたJSON処理の効率化に焦点を当てます。特に、curl を使った安全なAPI連携、systemd による定期実行、そして安全で冪等(idempotent)なシェルスクリプトの書き方について、具体的な例を交えて解説します。

要件と前提

このガイドでは、以下の要件と前提に基づき進めます。

要件:

  • jq コマンドでJSONデータを効率的に処理する。

  • curl を用いて、TLS検証や再試行機能を備えた安全なAPI呼び出しを行う。

  • systemd のUnitとTimerを活用し、定期的に処理を実行する。

  • シェルスクリプトは set -euo pipefailtrap、一時ディレクトリの利用など、安全で冪等な書き方を遵守する。

  • root 権限を必要とする操作は最小限に留め、原則として専用ユーザーでの実行を前提とする。

前提:

  • Linux環境(systemdが動作するディストリビューション、例: CentOS, Ubuntu Server)が利用可能であること。

  • jqcurl がインストールされていること。

  • 基本的なシェルスクリプトの知識があること。

  • 特定のAPIを呼び出すことを想定しますが、APIエンドポイントは仮のものとして扱います。

実装

ここでは、外部APIからJSONデータを取得し、jq で処理してログに出力する一連のプロセスを、安全なシェルスクリプトと systemd を用いて実装します。

1. JSON処理のフロー

まず、今回実装する処理の全体像をMermaidで示します。

graph TD
    A["スクリプト開始"] --> B{"一時ディレクトリ作成とクリーンアップ設定"};
    B --> C["API呼び出し (curl)"];
    C --|JSONデータ取得|--> D{"HTTPステータスコードとエラーチェック"};
    D --|成功 (HTTP 2xx)|--> E["jqで必要なデータを抽出・変換"];
    D --|失敗 (HTTP 4xx/5xx)|--> F["エラーログ出力とスクリプト終了"];
    E --> G["処理結果のログ出力とファイル保存"];
    G --> H["スクリプト終了"];

このフローに従い、API呼び出しからjq処理、そしてログ出力までを一貫して行います。

2. 安全なシェルスクリプトの基礎

冪等性を保ち、予期せぬエラーでスクリプトが中断しないよう、以下のベストプラクティスを採用します。

#!/bin/bash


# シェルスクリプトの安全な実行設定


# -e: コマンドが失敗した場合、即座にスクリプトを終了


# -u: 未定義の変数を参照した場合、エラーとして終了


# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、パイプライン全体を失敗とする

set -euo pipefail

# スクリプト名

SCRIPT_NAME=$(basename "$0")

# 一時ディレクトリのパスをグローバル変数で定義


# mktemp -d: 一意な名前の一時ディレクトリを作成。予測可能なパスへの攻撃を防ぐ。


# 作成に失敗した場合はエラー終了 (-e の恩恵)

TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")

# クリーンアップ関数


# スクリプトが終了する際に必ず一時ディレクトリを削除する (成功/失敗問わず)


# trap 'cleanup' EXIT: EXITシグナル (スクリプト終了時) でcleanup関数を実行

cleanup() {
    log_info "一時ディレクトリ '${TMP_DIR}' を削除します。"
    rm -rf "${TMP_DIR}"
    log_info "スクリプト '${SCRIPT_NAME}' が終了しました。"
}
trap 'cleanup' EXIT

# ログ出力関数

log_info() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"
}

log_error() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2
}

# --- ここからメインロジック ---

log_info "スクリプト '${SCRIPT_NAME}' が開始されました。"
log_info "一時ディレクトリ: ${TMP_DIR}"

# メイン処理 (後述)


# ...

# --- ここまでメインロジック ---

exit 0 # 正常終了

解説:

  • set -euo pipefail: シェルスクリプトの実行を堅牢にするための必須設定です。

  • mktemp -d: 予測不能な一時ディレクトリ名を生成し、セキュリティを強化します。

  • trap 'cleanup' EXIT: スクリプトが正常終了しても異常終了しても、cleanup 関数が実行され、作成した一時ファイルが確実に削除されます。これにより冪等性が保たれ、ディスクスペースの浪費や機密データの残存を防ぎます。

  • ログ関数: 統一されたフォーマットで情報ログとエラーログを出力します。

3. curl を用いたAPI連携と jq 処理

安全なシェルスクリプトの枠組みの中に、curljq を組み込みます。

#!/bin/bash


# (上記「安全なシェルスクリプトの基礎」のコードをここに含める)

set -euo pipefail
SCRIPT_NAME=$(basename "$0")
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
cleanup() { rm -rf "${TMP_DIR}"; log_info "スクリプト終了。"; }
trap 'cleanup' EXIT
log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"; }
log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2; }
log_info "スクリプト開始。一時ディレクトリ: ${TMP_DIR}"

# --- API設定 ---

API_URL="https://api.example.com/data"
API_KEY="YOUR_API_KEY" # 環境変数などで渡すのが安全
OUTPUT_FILE="${TMP_DIR}/api_response.json"
PROCESSED_DATA_FILE="${TMP_DIR}/processed_data.txt"

# --- curl設定 ---


# --silent: 進捗表示を抑制


# --show-error: エラー発生時にメッセージを表示


# --fail-with-body: HTTPエラー時もレスポンスボディを出力


# --retry 5: 5回まで再試行


# --retry-delay 5: 最初のリトライまで5秒待機


# --retry-max-time 60: 全てのリトライを含め最大60秒


# --cacert /etc/pki/tls/certs/ca-bundle.crt: システムのCA証明書パス (環境により異なる)


#             デフォルトで信頼されている場合は不要だが、明示推奨


# --header: ヘッダー追加。APIキーなど認証情報


# --output: 結果をファイルに出力

log_info "API (${API_URL}) からデータを取得します..."
CURL_COMMAND=(
    curl
    --silent
    --show-error
    --fail-with-body
    --retry 5
    --retry-delay 5
    --retry-max-time 60
    --cacert /etc/ssl/certs/ca-certificates.crt # Ubuntu/Debianの場合

    # --cacert /etc/pki/tls/certs/ca-bundle.crt # RHEL/CentOSの場合

    --header "Authorization: Bearer ${API_KEY}"
    --header "Content-Type: application/json"
    --output "${OUTPUT_FILE}"
    "${API_URL}"
)

# API呼び出し実行

if ! "${CURL_COMMAND[@]}"; then
    log_error "API呼び出しに失敗しました。"

    # エラー時のレスポンスボディがあればログに出力

    if [[ -f "${OUTPUT_FILE}" && -s "${OUTPUT_FILE}" ]]; then
        log_error "APIエラーレスポンス: $(cat "${OUTPUT_FILE}")"
    fi
    exit 1
fi
log_info "APIレスポンスを '${OUTPUT_FILE}' に保存しました。"

# --- jqによるJSON処理 ---


# -r: Raw output (文字列を引用符なしで出力)


# .[]: 配列の各要素をイテレート


# .id, .name, .value: 各要素のフィールドを抽出


# | @tsv: TSV形式で出力 (タブ区切り)


# jqの計算量: 通常、JSONサイズに比例 (O(N))


# メモリ条件: JSON全体をメモリにロードするため、非常に大規模なJSONでは注意が必要。


#            しかし、jqはストリーミング処理も可能で、一部のフィルタではメモリ効率が良い。

log_info "取得したJSONデータをjqで処理します..."
if ! jq -r '.data[] | select(.status == "active") | "\(.id)\t\(.name)\t\(.value)"' "${OUTPUT_FILE}" > "${PROCESSED_DATA_FILE}"; then
    log_error "jqによるJSON処理に失敗しました。"
    exit 1
fi
log_info "処理結果を '${PROCESSED_DATA_FILE}' に保存しました。"

# 処理結果の表示 (例: 標準出力へ)

log_info "--- 処理結果 ---"
cat "${PROCESSED_DATA_FILE}"
log_info "--- 処理結果ここまで ---"

exit 0

curl コマンドのセキュリティと信頼性:

  • --cacert: サーバー証明書の検証に使用するCA証明書バンドルを指定します。これにより、中間者攻撃(Man-in-the-Middle attack)を防ぎ、通信の信頼性を確保します。適切なパス(例: /etc/ssl/certs/ca-certificates.crt for Ubuntu/Debian, /etc/pki/tls/certs/ca-bundle.crt for RHEL/CentOS)を指定することが重要です。

  • --retry オプション群: ネットワークの一時的な障害やAPIサーバーの負荷状況によるエラーを許容し、自動的に再試行します。これにより、処理の信頼性が向上します。指数バックオフは --retry-delay--retry-max-time の組み合わせで擬似的に実現されますが、より複雑なロジックが必要な場合はスクリプト内で sleep を用いたループを実装します。

  • --fail-with-body: エラーが発生した場合でも、サーバーからの応答ボディを取得し、問題解決のための情報を得やすくします。

jq コマンドの効率的な使い方:

  • .data[] | select(.status == "active") | ...: 配列 data の各要素をイテレートし、status"active" のものだけを抽出し、指定されたフィールド (id, name, value) をタブ区切りで出力しています。

  • jq はC言語で書かれており非常に高速です。複雑なフィルタリングやデータ変換も効率的に行えます。

  • 大規模なJSONを扱う場合でも、jq はストリーミング処理が可能な場合があり、メモリ消費を抑えられます。ただし、全てのフィルタがストリーミング対応なわけではないため注意が必要です。

4. systemd による定期実行の設定

スクリプトを定期的に実行するために systemd.service.timer を設定します。これにより、cron よりも柔軟で詳細な管理が可能になります。

a. 実行ユーザーの作成

セキュリティ強化のため、専用のシステムユーザー jsonproc を作成し、このユーザーでスクリプトを実行します。root で直接実行することは避けるべきです。

# 専用ユーザーとグループを作成


# -r: システムユーザーとして作成


# -s /sbin/nologin: シェルログインを禁止

sudo useradd -r -s /sbin/nologin jsonproc

b. スクリプトの配置

作成したシェルスクリプト process_json.sh/usr/local/bin/ など、適切なパスに配置し、実行権限を付与します。

# スクリプトファイルを /usr/local/bin/ に配置


# 例: vim /usr/local/bin/process_json.sh で上記スクリプトを記述

# 実行権限を付与

sudo chmod +x /usr/local/bin/process_json.sh

# 所有者を専用ユーザーに変更 (オプションだが推奨)

sudo chown jsonproc:jsonproc /usr/local/bin/process_json.sh

c. Systemd Service Unit の作成 (/etc/systemd/system/process-json.service)

このファイルは、process_json.sh スクリプトの実行方法を定義します。

# /etc/systemd/system/process-json.service

[Unit]
Description=Process JSON data from API

# network-online.target: ネットワークが利用可能になってからサービスを開始

Requires=network-online.target
After=network-online.target

[Service]

# One-shot: 短時間で完了するタスク向け

Type=oneshot

# ExecStart: 実行するコマンド


# スクリプトの絶対パスを指定

ExecStart=/usr/local/bin/process_json.sh

# User/Group: 実行ユーザーとグループを指定し、最小権限の原則を適用

User=jsonproc
Group=jsonproc

# WorkingDirectory: スクリプトの作業ディレクトリ (オプション)


# WorkingDirectory=/var/lib/jsonproc


# StandardOutput/StandardError: 標準出力/エラーの出力先


# journalに記録されるように設定

StandardOutput=journal
StandardError=journal

# Environment: 環境変数 (APIキーなど、機密情報はVaultなどで管理を推奨)


# Environment="API_KEY=YOUR_API_KEY"

[Install]

# WantedBy: timerユニットによって起動されるため、ここではmulti-user.targetは不要


# Timerによって起動されるサービスは、通常明示的にWantedByを持たない

d. Systemd Timer Unit の作成 (/etc/systemd/system/process-json.timer)

このファイルは、process-json.service をいつ、どのくらいの頻度で実行するかを定義します。

# /etc/systemd/system/process-json.timer

[Unit]
Description=Run JSON processing script hourly

# サービスユニットが起動する前に、タイマーユニットの準備が完了していることを保証

After=process-json.service

[Timer]

# OnCalendar: スケジュール定義 (例: "hourly", "daily", "*-*-* 03:00:00")


# ここでは毎時実行を指定

OnCalendar=hourly

# Persistent=true: タイマーが非アクティブだった期間のイベントも遡って実行


# (例: サーバー停止中に実行予定だったタスクを起動時に実行)

Persistent=true

# AccuracySec: 実行精度の設定。デフォルトは1分


# AccuracySec=10s # 10秒の精度で実行

[Install]

# WantedBy: システム起動時にタイマーが有効になるように設定


# cronと同様にmulti-user.targetに追加

WantedBy=timers.target

e. Systemd Timer の有効化と起動

systemd に新しいUnitファイルを読み込ませ、Timerを有効化して起動します。

# systemdの構成をリロード

sudo systemctl daemon-reload

# timerを有効化 (システム起動時に自動起動するように)

sudo systemctl enable process-json.timer

# timerを即時起動

sudo systemctl start process-json.timer

検証

Timerが正しく動作しているか確認します。

  1. Timerのステータス確認:

    systemctl status process-json.timer
    

    出力例:

    ● process-json.timer - Run JSON processing script hourly
         Loaded: loaded (/etc/systemd/system/process-json.timer; enabled; vendor preset: disabled)
         Active: active (waiting) since Wed 2024-05-15 10:00:00 JST; 1min ago
        Trigger: Wed 2024-05-15 11:00:00 JST; 59min left
       Triggers: ● process-json.service
    

    Active: active (waiting)Trigger の次回の実行日時を確認します。

  2. Serviceのステータス確認 (実行後): Timerがトリガーされると、process-json.service が実行されます。

    systemctl status process-json.service
    

    出力例:

    ● process-json.service - Process JSON data from API
         Loaded: loaded (/etc/systemd/system/process-json.service; static; vendor preset: disabled)
         Active: inactive (dead) since Wed 2024-05-15 10:00:00 JST; 1min ago
        Process: 12345 ExecStart=/usr/local/bin/process_json.sh (code=exited, status=0/SUCCESS)
    

    Active: inactive (dead)status=0/SUCCESS が表示されていれば、正常終了です。

  3. ログの確認: スクリプトの標準出力と標準エラー出力は journalctl で確認できます。

    journalctl -u process-json.service --since "1 hour ago"
    

    スクリプト内で定義した log_infolog_error のメッセージが表示されることを確認します。process_json.sh が出力した「— 処理結果 —」の内容もここで確認できます。

運用

ログ監視

systemd によって実行されるスクリプトのログは journalctl で集中管理されます。

  • エラーログの監視: journalctl -u process-json.service -p err でエラーのみをフィルタリングしたり、rsyslogfluentd などのログ転送エージェントと連携して、監視システム(Prometheus/Grafana, ELK Stackなど)に連携することで、異常発生時にアラートを発することができます。

  • 継続的な確認: journalctl -f -u process-json.service でリアルタイムにログを追跡できます。

権限管理

  • 最小権限の原則: systemdUser= および Group= オプションは、スクリプトを専用の非特権ユーザー(例: jsonproc)で実行するための重要な機能です。これにより、スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。

  • root 権限の扱い: この例のように、systemd ユニットの配置や useradd の実行など、システムレベルの設定変更には root 権限が必要ですが、実際のスクリプト実行は非特権ユーザーで行うべきです。もしスクリプト内で root 権限が必要な処理がある場合は、sudo を使用し、特定のコマンドのみを実行できるよう sudoers ファイルで厳密に制御することを検討してください。

トラブルシュート

  • スクリプトが実行されない:

    • systemctl status process-json.timer でタイマーが active (waiting) か、次回の実行時刻が適切か確認します。

    • sudo systemctl daemon-reload を実行し、Unitファイルが読み込まれていることを確認します。

    • sudo systemctl enable process-json.timer でタイマーが有効化されているか確認します。

  • スクリプトがエラーになる:

    • journalctl -u process-json.service でログを確認し、スクリプト内のエラーメッセージや jq, curl の出力を確認します。

    • スクリプトを直接実行し、bash -x /usr/local/bin/process_json.sh でトレース出力を見てデバッグします。

    • User= をコメントアウトして root で実行してみる(デバッグ用途のみ、運用は非推奨)ことで、権限の問題かを確認できる場合があります。

  • APIからの応答がない/エラー:

    • curl コマンドをシェルで直接実行し、ネットワーク接続や認証情報の問題がないか確認します。

    • --verbose オプションを追加して詳細なリクエスト/レスポンス情報を確認します。

  • jqのフィルタリングがおかしい:

    • APIから取得したJSONファイル (${TMP_DIR}/api_response.json) を直接 jq に渡し、様々なフィルタを試してデバッグします。

    • 例: jq '.' /tmp/api_response.json でJSON構造全体を確認します。

まとめ

jq コマンドを使ったJSONデータの効率的な処理、curl による安全なAPI連携、そして systemd を利用した定期実行の手順について解説しました。 特に、set -euo pipefailtrap、一時ディレクトリの使用など、シェルスクリプトの安全な書き方に焦点を当て、冪等性と堅牢性を確保しました。 また、systemdUser= オプションによる権限分離の重要性にも触れ、セキュリティを意識した運用が可能であることを示しました。これらの技術を組み合わせることで、DevOpsの現場で信頼性の高い自動処理システムを構築できます。 本記事の内容は2024年5月15日 JST時点の情報に基づいています。

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

コメント

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