jqを用いたJSONデータフィルタリングとDevOpsパイプラインへの統合

Tech

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

jqを用いたJSONデータフィルタリングとDevOpsパイプラインへの統合

要件と前提

DevOps環境におけるJSONデータ処理は、API連携やログ解析、設定管理において不可欠です。本ガイドでは、軽量ながら強力なJSONプロセッサであるjqを用いて、安全かつ効率的にデータをフィルタリング、変換し、DevOpsパイプラインに統合する手法を解説します。特に、以下の要件を満たすことを目指します。

  • セキュアなスクリプト: set -euo pipefailtrap、一時ディレクトリの安全な使用など、堅牢なシェルスクリプトの記述。

  • 外部データ取得: curlを用いた安全なAPIアクセス(TLS検証、再試行、バックオフ戦略)。

  • JSON処理: jqによる高度なフィルタリング、変換、オブジェクト操作。

  • 自動化とスケジューリング: systemd unitsystemd timerを用いた定期実行の実現。

  • 権限分離: root権限の適切な扱いと、最小権限の原則に則った運用。

前提として、以下のツールがシステムにインストールされていることとします。

  • jq (バージョン 1.6以上を推奨)

  • curl

  • bash (バージョン 4.0以上を推奨)

  • systemd が利用可能なLinuxディストリビューション

実装

ここでは、特定のAPIからJSONデータを取得し、jqでフィルタリング・変換した後、ファイルに保存する一連の処理を実装します。この処理はsystemdによって定期実行されることを想定します。

1. セキュアなシェルスクリプトのフレームワーク

idempotentな処理を実現し、スクリプトの途中で予期せぬエラーが発生した場合でもクリーンアップを行うための基本的なフレームワークを定義します。

#!/bin/bash


# スクリプト名: process_api_data.sh

# 厳格なエラーハンドリング設定:


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


# -u: 未定義の変数を使用しようとした場合、エラーを出力してスクリプトを終了する


# -o pipefail: パイプライン内でコマンドが失敗した場合、パイプライン全体の終了ステータスをその失敗したコマンドのステータスにする

set -euo pipefail

# 一時ディレクトリの作成とクリーンアップ関数


# スクリプト実行ユーザーのホームディレクトリに一時ファイルを保存し、権限分離を意識する。

cleanup() {
    echo "Cleaning up temporary directory: ${TMP_DIR}"

    # TMP_DIRが存在し、かつディレクトリであることを確認してから削除

    if [[ -d "${TMP_DIR}" ]]; then
        rm -rf "${TMP_DIR}"
    fi
}

# Ctrl+C (INT)、スクリプト終了 (EXIT)、ターミナル終了 (TERM) 時にcleanup関数を呼び出す

trap cleanup EXIT INT TERM

# スクリプト実行ユーザー名を取得

CURRENT_USER=$(whoami)

# 一時ディレクトリをユーザーのホームディレクトリ内に作成


# rootで実行される場合は /var/tmp など、適切なシステム一時ディレクトリを利用すべき

if [[ "${CURRENT_USER}" == "root" ]]; then
    TMP_DIR=$(mktemp -d "/var/tmp/${0##*/}.XXXXXXXXXX")
    OUTPUT_DIR="/var/lib/api_data" # rootで書き込み可能なシステム上の場所
else
    TMP_DIR=$(mktemp -d "${HOME}/.tmp/${0##*/}.XXXXXXXXXX")
    OUTPUT_DIR="${HOME}/api_data" # ユーザーのホームディレクトリ内の場所
fi

# 出力ディレクトリが存在しない場合は作成

mkdir -p "${OUTPUT_DIR}"

echo "Temporary directory: ${TMP_DIR}"
echo "Output directory: ${OUTPUT_DIR}"

# 実行ユーザーの権限確認と警告


# root権限での実行は可能な限り避けるべき。必要な場合にのみ使用し、権限を厳しく管理する。

if [[ "${EUID}" -eq 0 ]]; then
    echo "WARNING: This script is running as root. Ensure necessary precautions are taken for root operations." >&2
    echo "Consider running this script as a less privileged user if possible." >&2

    # rootでしか実行できない場合はここで処理を継続するが、そうでなければexitしても良い

fi

# ここから主要処理


# ...

2. curlによる安全なデータ取得

APIからのデータ取得にはcurlを使用します。TLS検証を徹底し、一時的なネットワーク問題に対応するため再試行と指数バックオフ戦略を導入します。

# ... (前述のスクリプトフレームワークの続き)

API_ENDPOINT="https://api.example.com/v1/data"

# 出力ファイル名にJSTの現在日時を含める

OUTPUT_FILE="${OUTPUT_DIR}/api_data_$(date +%Y%m%d%H%M%S).json"
CURL_LOG="${TMP_DIR}/curl.log"

# CA証明書のパス (多くのLinuxディストリビューションで標準)


# 環境に合わせて調整してください (例: Ubuntu/Debian: /etc/ssl/certs/ca-certificates.crt, RHEL/CentOS: /etc/pki/tls/certs/ca-bundle.crt)

CA_CERT_PATH="/etc/ssl/certs/ca-certificates.crt"

echo "Fetching data from ${API_ENDPOINT}..."

# curlコマンドのオプション:


# -sS: サイレントモード (-s) かつエラー表示 (-S)


# --retry 5: 最大5回再試行する


# --retry-delay 5: 最初の再試行まで5秒待機し、以降は指数バックオフ (5s, 10s, 20s...)


# --retry-max-time 60: 再試行を含めた最大実行時間60秒


# --cacert: CA証明書を指定してTLS検証を行う。サーバ証明書の検証に利用。


# --output: ダウンロードしたデータを指定した一時ファイルに出力


# --fail: HTTPエラー (4xx/5xx) があった場合、終了ステータスを非ゼロにする


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

if ! curl -sS \
    --retry 5 \
    --retry-delay 5 \
    --retry-max-time 60 \
    --cacert "${CA_CERT_PATH}" \
    --output "${TMP_DIR}/raw_data.json" \
    --fail \
    --show-error \
    "${API_ENDPOINT}" 2> "${CURL_LOG}"; then
    echo "ERROR: Failed to fetch data from API. See ${CURL_LOG} for details." >&2
    cat "${CURL_LOG}" >&2 # エラーログの内容を標準エラー出力にも表示
    exit 1
fi

echo "Raw data saved to ${TMP_DIR}/raw_data.json"

# ... (jqでの処理に続く)

3. jqでのJSONフィルタリングと変換

取得したJSONデータ(例:{"items":[{"id":1, "status":"active", "value":100},{"id":2, "status":"inactive", "value":200}, {"id":3, "status":"active", "value":300}]})をjqで加工します。 ここでは、「statusactiveのアイテムのみを抽出し、idvalueを新しいオブジェクトとして配列で出力」する例を示します。

# ... (curlでのデータ取得の続き)

FILTERED_DATA_FILE="${TMP_DIR}/filtered_data.json"

echo "Filtering and transforming data with jq..."

# jqコマンドのオプションとフィルタリングロジック:


# -c: 圧縮出力 (各JSONオブジェクトが1行に出力される)


# .items[]: トップレベルの`items`配列を展開し、個別の要素をストリームとして扱う


# select(.status == "active"): 各要素の中から`status`フィールドが"active"であるオブジェクトを選択


# {id, value}: 選択されたオブジェクトから`id`と`value`のフィールドを持つ新しいオブジェクトを作成


# [ ... ] は配列としてまとめるが、ここではパイプで個別のオブジェクトを流しているため、結果は行区切りのJSONオブジェクトのストリームとなる。


# 配列として出力する場合は `[ .items[] | select(.status == "active") | {id, value} ]` と記述する。

if ! jq -c '.items[] | select(.status == "active") | {id, value}' "${TMP_DIR}/raw_data.json" > "${FILTERED_DATA_FILE}"; then
    echo "ERROR: jq processing failed. Check JSON format or jq filter syntax." >&2
    exit 1
fi

echo "Filtered data saved to ${FILTERED_DATA_FILE}"

# 最終的な出力ファイルを一時ディレクトリから永続的な出力ディレクトリへ移動


# mvコマンドはアトミックな操作であり、ファイルの上書き中に読み取り側で不完全なファイルが見えるリスクを軽減する

mv "${FILTERED_DATA_FILE}" "${OUTPUT_FILE}"
echo "Final processed data moved to ${OUTPUT_FILE}"

# ... (スクリプトの終わり)

この一連のデータ処理フローをMermaidで視覚化します。

graph TD
    A["データソースAPI"] --> |HTTP GET| B{"curlでデータ取得"};
    B --> |JSONデータ| C{"jqでフィルタリング"};
    C --> |フィルタ済みJSON| D["処理結果"];
    D --> |ファイル保存 or 他のシステムへ| E["後続処理"];
    E --> F["定期実行 (systemd Timer)"];
    B --|TLS検証| G("信頼されたCA証明書");
    B --|エラーハンドリング| H("再試行ロジック");

4. systemd unit/timerの定義

このシェルスクリプトを定期的に実行するため、systemdserviceユニットとtimerユニットを定義します。

a. サービスユニット (/etc/systemd/system/api-data-processor.service)

スクリプトは非特権ユーザーで実行することが強く推奨されます。ここでは例としてappuserというユーザーを想定します。ExecStartにはスクリプトのフルパスを指定します。

[Unit]
Description=API Data Processor Service
After=network-online.target # ネットワークが利用可能になってから起動を試みる

[Service]
Type=oneshot # 一度実行して終了するサービスタイプ
User=appuser # スクリプトを実行する非特権ユーザー
Group=appuser # スクリプトを実行する非特権グループ
WorkingDirectory=/opt/api-processor # スクリプトの作業ディレクトリ (例: スクリプト本体が配置される場所)
ExecStart=/opt/api-processor/process_api_data.sh # スクリプトのフルパス
StandardOutput=journal # 標準出力をsystemdジャーナルに記録
StandardError=journal # 標準エラー出力をsystemdジャーナルに記録
Restart=on-failure # サービスが失敗した場合でも再起動しない (oneshotのため)

# 環境変数が必要な場合は Environment=KEY=VALUE の形式で指定。機密情報は環境変数ではなく、Secrets Managerなどを利用すべき。

[Install]
WantedBy=multi-user.target

注意: UserGroupで指定するユーザーは事前に作成しておく必要があります。また、ExecStartで指定するスクリプトとWorkingDirectoryのパスは、appuserが読み書きできる適切なパーミッションを設定してください。

b. タイマーユニット (/etc/systemd/system/api-data-processor.timer)

毎日午前3時00分00秒にサービスユニットを起動する例です。

[Unit]
Description=Run API Data Processor daily at 3 AM
RefuseManualStart=no # systemctl start コマンドでの手動起動を許可
RefuseManualStop=no  # systemctl stop コマンドでの手動停止を許可

[Timer]
OnCalendar=*-*-* 03:00:00 # 毎日午前3時00分00秒に起動
Persistent=true # タイマーが非アクティブな間に発生したイベントも、次にアクティブになったときに実行する
Unit=api-data-processor.service # このタイマーが起動するサービスユニットの名前

[Install]
WantedBy=timers.target

検証

1. シェルスクリプトのテスト

スクリプトを直接実行して動作を確認します。

# スクリプトを適切なディレクトリに配置 (例: /opt/api-processor/)


# スクリプトに実行権限を付与

sudo chmod +x /opt/api-processor/process_api_data.sh

# 非特権ユーザー (例: appuser) でテスト実行


# appuserユーザーが事前に作成され、/opt/api-processor/ ディレクトリへの読み取り・実行権限を持っていること

sudo -u appuser /opt/api-processor/process_api_data.sh

# スクリプト実行後、出力ファイルの確認


# appuserのホームディレクトリ下のapi_dataディレクトリにファイルが生成される想定

ls -l /home/appuser/api_data/
cat /home/appuser/api_data/api_data_*.json

2. systemdユニットの有効化と起動、ログ確認

サービスユニットとタイマーユニットを有効化し、正しく動作するか確認します。

# systemd設定をリロードして新しいユニットファイルを認識させる

sudo systemctl daemon-reload

# タイマーユニットを有効化し、起動

sudo systemctl enable api-data-processor.timer
sudo systemctl start api-data-processor.timer

# タイマーの状態を確認: 次の実行時刻などが表示される

sudo systemctl list-timers | grep api-data-processor

# サービスユニットの手動起動(テスト目的。タイマーが動作する前に実行確認)

sudo systemctl start api-data-processor.service

# ログの確認: サービスユニットの標準出力/エラー出力がジャーナルに記録されている


# --sinceオプションには、JSTの具体日付と時刻を指定

sudo journalctl -u api-data-processor.service --since "{{jst_today}} 00:00:00" -f

運用

  • 監視: systemdのステータス(systemctl status api-data-processor.service)やスクリプトの出力ログを監視システム(Prometheus, Grafanaなど)と連携させ、異常を検知した際にアラートを発報するように設定します。スクリプト内のエラーハンドリングにより、ゼロ以外の終了コードでスクリプトが終了した場合、systemdはそれを失敗として記録します。

  • ログ管理: journalctlで確認できるログは、rsyslogfluentdなどを通じて集中ログ管理システム(ELK Stack, Lokiなど)に転送し、長期保管と分析を可能にします。これにより、問題発生時の原因究明が容易になります。

  • 設定管理: スクリプト、systemdユニットファイル、設定ファイルなどはGitなどのバージョン管理システムで管理し、変更履歴を追跡可能にします。Ansible, Chef, Puppetなどの構成管理ツールを用いて、これらのファイルをデプロイすることを推奨します。

  • 権限分離の徹底: root権限は最小限に抑え、可能な限り非特権ユーザーでスクリプトを実行します。APIキーやデータベース接続情報などの機密情報は、環境変数、VaultAWS Secrets Managerなどのセキュアな仕組みで管理し、スクリプト内にハードコードしないようにします。

トラブルシュート

  • jqのエラー: jqコマンドが失敗した場合、通常はJSONパスの記述ミスや入力JSONの形式不正が原因です。jq -r . < input.jsonで生JSONの内容を確認し、jqの構文チェッカー(例: echo '{"a":1}' | jq '.' または jq '.' < input.json)で問題箇所を特定します。特に、入力JSONが完全に読み込まれていない場合や、予期しない空の入力があった場合もエラーになり得ます。

  • curlのエラー: curlのエラーメッセージはスクリプトで指定したCURL_LOGファイルで確認できます。HTTPステータスコード(4xx, 5xx)やネットワーク接続の問題、DNS解決エラー、TLS証明書の検証エラーなどを特定します。--verboseオプションをcurlコマンドに追加すると、より詳細な通信ログを取得でき、問題の切り分けに役立ちます。

  • systemdのエラー: systemctl status api-data-processor.serviceでサービスの現在の状態を確認します。詳細なログはjournalctl -u api-data-processor.serviceで確認でき、スクリプトのエラー出力やシステムメッセージが含まれます。ExecStartパスの誤り、スクリプトファイルへの権限不足、指定ユーザーの存在、依存関係の問題(例: network-online.targetが到達できない)などが一般的な原因です。

まとめ

、DevOpsの文脈でjqを用いたJSONデータフィルタリングの具体的な実装方法を解説しました。セキュアなシェルスクリプトの記述から始まり、curlによる安全なAPIデータ取得、jqによる高度なデータ加工、そしてsystemd unit/timerを用いた自動化まで、一連のパイプラインを構築する手順を示しました。

特に、set -euo pipefailtrapによる堅牢なスクリプト、curlのTLS検証と再試行、そしてsystemdにおける非特権ユーザーでの実行といったDevOpsにおけるベストプラクティスを強調しました。これにより、システム管理者やDevOpsエンジニアは、信頼性と保守性の高いデータ処理ワークフローを構築し、日々の運用を効率化できるでしょう。

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

コメント

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