jqコマンドによるJSONデータ高度処理とDevOps自動化

Tech

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

jqコマンドによるJSONデータ高度処理とDevOps自動化

DevOps環境におけるデータ処理は、自動化と堅牢性が求められます。本記事では、jq コマンドを核として、curl によるAPI連携、systemd による定期実行を組み合わせ、JSONデータの高度な処理とDevOpsの自動化を実現する手法について解説します。安全なBashスクリプトの書き方、権限分離、トラブルシューティングまでを網羅し、実運用に耐えうるシステム構築を目指します。

1. 要件と前提

本記事で解説するシステムは、以下の環境とツールを前提とします。

  • OS: Linux (systemdが利用可能なディストリビューション、例: CentOS, Ubuntu, Debian)

  • シェル: Bash 4.x 以降

  • jq: JSON処理ツール (バージョン1.6以上を推奨)

  • curl: データ転送ツール (バージョン7.x以上、特に --json オプションを利用する場合は 7.82.0 以降。curl 7.82.0 は2022年03月30日にリリースされました [1])

  • systemd: システムおよびサービスマネージャー

  • 実行ユーザー: 非特権ユーザーで実行することを基本とし、必要に応じて sudo を利用。

2. 実装

2.1. 全体の処理フロー

以下のフローは、systemd タイマーによって定期的に起動されるサービスが、Bashスクリプトを実行し、curl で外部APIからJSONデータを取得、そのデータを jq で処理する一連の流れを示しています。

graph TD
    A["systemd Timer"] --> |定期実行| B["systemd Service Unit"];
    B --> |起動| C{"Bash Script"};
    C --> |APIリクエスト| D("外部API");
    D --> |JSON応答| C;
    C --> |jqで高度処理| E["整形・変換済みJSONデータ"];
    E --> |ログ/DB/ファイルへ出力| F["データストア"];

2.2. 安全で冪等なBashスクリプトフレームワーク

まず、DevOps環境で安全かつ冪等な処理を実現するためのBashスクリプトの基本構造を定義します。

#!/bin/bash


# スクリプト名: process_data.sh

# set -euo pipefail:


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


#   -u: 未定義の変数を参照した場合、スクリプトを終了


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

set -euo pipefail

# 処理対象のデータ取得・処理を行う関数

main() {

    # 一時ディレクトリの作成


    # mktemp -d: 一意な一時ディレクトリを作成。これにより、複数実行時の競合を防止し冪等性を高める。

    local tmpdir
    tmpdir=$(mktemp -d -t data_process_XXXXXX)

    # trap: スクリプト終了時に一時ディレクトリを確実に削除


    # EXITシグナルは、スクリプトが正常終了またはエラー終了した場合のいずれでも発生

    trap 'rm -rf "$tmpdir"' EXIT

    # 変数の定義 (読み取り専用を推奨)

    readonly API_ENDPOINT="https://api.example.com/data"
    readonly API_KEY="your_api_key_here" # 本番では環境変数やシークレット管理ツールを使用
    readonly OUTPUT_DIR="/var/log/my_app"
    readonly LOG_FILE="$OUTPUT_DIR/process_data-$(date +%Y%m%d).log"

    mkdir -p "$OUTPUT_DIR" # 出力ディレクトリが存在しない場合は作成

    echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト開始" | tee -a "$LOG_FILE"

    # curlを用いた堅牢なAPI呼び出し

    local json_response
    json_response=$(fetch_data) # fetch_data関数でデータを取得

    # jqを用いた高度なJSON処理

    local processed_data
    processed_data=$(process_json "$json_response") # process_json関数でデータを処理

    # 処理結果の保存

    echo "$processed_data" | tee -a "$LOG_FILE"
    echo "$processed_data" > "$tmpdir/output_$(date +%Y%m%d%H%M%S).json"

    echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト終了" | tee -a "$LOG_FILE"
}

# curlで外部APIからデータを取得する関数


# エラーハンドリング、リトライ、TLS設定を含む

fetch_data() {
    local max_retries=5
    local retry_delay_sec=5
    local connect_timeout_sec=10
    local max_time_sec=30
    local http_code
    local response_body
    local api_url="$API_ENDPOINT/metrics" # 例: メトリクス取得API

    for ((i=1; i<=max_retries; i++)); do
        echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し試行 $i/$max_retries" | tee -a "$LOG_FILE" >&2

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


        # --location: リダイレクトを自動でフォロー


        # --cacert: 証明書の検証 (本番環境では適切に設定)


        # --retry, --retry-delay, --retry-max-time: ネットワーク一時障害への対応


        # --connect-timeout, --max-time: タイムアウト設定


        # -H "Authorization: Bearer $API_KEY": 認証ヘッダーの例

        response_body=$(
            curl -sS \
                 --fail-with-body \
                 --location \
                 --cacert /etc/ssl/certs/ca-certificates.crt \
                 --retry "$max_retries" \
                 --retry-delay "$retry_delay_sec" \
                 --retry-max-time "$((max_retries * retry_delay_sec * 2))" \
                 --connect-timeout "$connect_timeout_sec" \
                 --max-time "$max_time_sec" \
                 -H "Authorization: Bearer $API_KEY" \
                 -H "Content-Type: application/json" \
                 "$api_url" 2>&1
        )
        http_code=$? # curlの終了コードを取得

        if [[ $http_code -eq 0 ]]; then
            echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し成功" | tee -a "$LOG_FILE" >&2
            echo "$response_body"
            return 0
        else
            echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: API呼び出し失敗 (exit code: $http_code)" | tee -a "$LOG_FILE" >&2
            echo "$response_body" | tee -a "$LOG_FILE" >&2
            if [[ $i -lt $max_retries ]]; then
                echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: $retry_delay_sec秒後にリトライ..." | tee -a "$LOG_FILE" >&2
                sleep "$retry_delay_sec"
            fi
        fi
    done
    echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: 最大リトライ回数を超過。API呼び出しに失敗しました。" | tee -a "$LOG_FILE" >&2
    exit 1 # スクリプトをエラー終了させる
}

# jqでJSONデータを処理する関数


# 複数の処理ステップをパイプで繋ぐ例

process_json() {
    local json_input="$1"

    # jqの具体的な処理例:


    # 1. 配列内の各オブジェクトから 'id' と 'status' を抽出し、'timestamp' を追加


    # 2. 'status' が "active" の要素のみをフィルタリング


    # 3. 'value' フィールドの合計値を計算し、新しいJSONオブジェクトとして出力


    # 4. 'metadata' を削除し、'processed_at' を追加

    echo "$json_input" | jq -c '
        [
            .data.items[] | {
                id: .id,
                status: .status,
                value: .value,
                timestamp: (.timestamp | fromdateiso8601) # ISO 8601文字列をUNIXタイムスタンプに変換
            } | select(.status == "active") # "active"なアイテムのみを抽出
        ]
        | {
            total_active_items: length,
            sum_of_values: (map(.value) | add), # activeアイテムのvalueの合計
            processed_at: (now | todateiso8601),
            items: . # フィルタリング・変換後のアイテムリスト
        }

        # この例では、APIレスポンス全体ではなく、必要なデータ構造のみを抽出・変換しています。


        # 必要に応じて、別の jq 処理を追加することも可能です。

    '

    # 計算量: N個の要素を持つ配列処理の場合、O(N)


    # メモリ条件: 入力JSONのサイズと、変換後のJSONサイズに依存。大規模なJSONでは注意が必要。

}

# スクリプト実行

main "$@"

解説:

  • set -euo pipefail: スクリプトの堅牢性を高めるための基本的な設定です。

  • mktemp -dtrap: 一時ファイルを安全に管理し、スクリプト終了時にクリーンアップを保証します。冪等性のためにも重要です。

  • readonly: 重要な変数を誤って変更しないように保護します。

  • curl オプション:

    • --fail-with-body: エラー時にボディを出力し、デバッグを容易にします。

    • --retry --retry-delay --retry-max-time: 一時的なネットワーク障害やAPIの過負荷に対応するためのリトライ戦略を設定します。

    • --connect-timeout --max-time: 接続および全体のタイムアウトを設定し、ハングアップを防止します。

    • --cacert: TLS証明書の検証パスを指定し、セキュアな通信を確保します。

    • -H "Content-Type: application/json": JSONデータを送信する場合に必須です。curl 7.82.0 以降では --json オプションも利用できますが、ここでは広く互換性のある -H を使用しています。

  • jq 処理:

    • [ ... ] | map(...) | select(...): 配列内のオブジェクトを変換し、特定の条件でフィルタリングする高度なパターンです。

    • fromdateiso8601, now | todateiso8601: 日付時刻の変換例です。

    • map(.value) | add: 配列内の特定フィールドの合計値を計算します。

    • length, add: jq の組み込み関数で、配列の要素数や数値の合計を算出します。

2.3. systemd unitとtimerの設定

このBashスクリプトを定期的に実行するため、systemd のサービスユニットとタイマーユニットを設定します。

2.3.1. サービスユニット (my-app-processor.service)

/etc/systemd/system/my-app-processor.service を作成します。

[Unit]
Description=My Application Data Processor
Documentation=https://example.com/docs/my-app-processor
After=network.target

[Service]

# Root権限の扱いと権限分離:


# ExecStartはroot権限で実行されますが、User/Groupディレクティブで非特権ユーザーに切り替えます。


# これにより、スクリプト自体は必要最小限の権限で実行され、セキュリティリスクを低減します。

User=myuser # 実行ユーザー (root以外の既存ユーザーを指定)
Group=myuser # 実行グループ
WorkingDirectory=/opt/my_app # スクリプトの作業ディレクトリ
ExecStart=/bin/bash /opt/my_app/process_data.sh # 実行するスクリプトのフルパス

# StandardOutput/StandardError: スクリプトの出力をsystemd journalに送る

StandardOutput=journal
StandardError=journal

# Restart: サービスが異常終了した場合の再起動ポリシー

Restart=on-failure

# RestartSec: 再起動を試みるまでの待機時間

RestartSec=5s

[Install]
WantedBy=multi-user.target # 通常はtimerユニットから呼び出されるため直接enableはしない

注意点:

  • UserGroup には、スクリプトの実行に必要な権限のみを持つ非特権ユーザーを指定します。このユーザーは、OUTPUT_DIR (/var/log/my_app) への書き込み権限を持つ必要があります。

  • ExecStart のパスはスクリプトの実際の配置場所に合わせてください。

2.3.2. タイマーユニット (my-app-processor.timer)

/etc/systemd/system/my-app-processor.timer を作成します。

[Unit]
Description=Run My Application Data Processor every 10 minutes
Requires=my-app-processor.service # このタイマーはサービスに依存
Documentation=https://example.com/docs/my-app-processor

[Timer]

# OnCalendar: 定期実行スケジュール (例: 10分ごと)


# 具体的な実行例: OnCalendar=*-*-* *:00/10:00 (毎時00, 10, 20, 30, 40, 50分に実行)

OnCalendar=*:0/10:00

# AccuracySec: 実行時間の精度 (例: 1分以内に起動)

AccuracySec=1min

# Persistent: サービス停止中に起動タイミングを逃した場合、次回起動時に即時実行するか


# (例: サーバー再起動後に、停止中にスキップされた実行があればすぐ実行)

Persistent=true

[Install]
WantedBy=timers.target

解説:

  • OnCalendar: systemd タイマーの核となる部分で、柔軟なスケジュール設定が可能です。ここでは10分ごとに実行するよう設定しています。

  • Requires=my-app-processor.service: このタイマーは対応するサービスユニットに依存します。

3. 検証

3.1. スクリプト単体での検証

スクリプトを直接実行し、意図した通りに動作するか確認します。

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

chmod +x /opt/my_app/process_data.sh

# スクリプトを直接実行

/opt/my_app/process_data.sh

# ログファイルと出力ファイルの確認

ls -l /var/log/my_app/
cat /var/log/my_app/process_data-$(date +%Y%m%d).log

3.2. systemdサービスの起動とログ確認

systemd ユニットをリロードし、サービスを直接起動して動作を確認します。

# systemdに新しいユニットファイルを認識させる

sudo systemctl daemon-reload

# サービスを手動で開始

sudo systemctl start my-app-processor.service

# サービスのステータス確認

sudo systemctl status my-app-processor.service

# サービスログの確認 (journalctl)


# -u: ユニット指定, -f: フォローモード, --since "N minutes ago": 過去N分間のログ

sudo journalctl -u my-app-processor.service -f

ログにスクリプトの出力(INFO: スクリプト開始など)が表示され、エラーがないことを確認します。

4. 運用

4.1. systemdサービスの有効化と無効化

タイマーを有効化することで、定期実行が開始されます。

# タイマーを有効化 (OS起動時に自動でタイマーが起動するようになる)

sudo systemctl enable my-app-processor.timer

# タイマーを起動 (即座にタイマーが開始される)

sudo systemctl start my-app-processor.timer

# タイマーのステータス確認

sudo systemctl status my-app-processor.timer

# 定期実行を停止する場合

sudo systemctl stop my-app-processor.timer
sudo systemctl disable my-app-processor.timer

タイマーを enable した後、システムを再起動してもタイマーが自動的に起動し、スケジュール通りにサービスが実行されることを確認することをお勧めします。

4.2. ログ監視

journalctl を利用して、定期的にサービスログを監視します。

# 直近のログを表示

sudo journalctl -u my-app-processor.service --since "1 day ago"

# エラーのみをフィルタリング

sudo journalctl -u my-app-processor.service -p err

# リアルタイムでログを監視

sudo journalctl -u my-app-processor.service -f

4.3. 設定変更時の手順

スクリプトやsystemdユニットファイルを変更した場合は、以下の手順で適用します。

  1. スクリプト変更: process_data.sh を修正。

  2. サービス/タイマーユニット変更: /etc/systemd/system/*.service*.timer を修正。

  3. systemdデーモンのリロード: sudo systemctl daemon-reload

  4. タイマーの再起動: sudo systemctl restart my-app-processor.timer (サービスはタイマーから起動されるため、タイマーを再起動すれば良い)

5. トラブルシュート

5.1. スクリプトのエラーハンドリング

  • set -euo pipefail: これにより、多くのエラーが即座に捕捉され、スクリプトが異常終了します。終了ステータスやログを確認して原因を特定します。

  • curl エラー: fetch_data 関数内で curl の終了コード ($?) を確認し、エラーメッセージをログに出力しています。

    • ネットワークの問題: curl: (6) Could not resolve host, (7) Failed to connect

    • API認証エラー: HTTP 401/403 (レスポンスボディを確認)

    • APIサーバーエラー: HTTP 5xx (レスポンスボディを確認)

  • jq パースエラー: jq は不正なJSON入力に対してエラーを出力し、スクリプトが中断されます。json_input の内容が正しいJSON形式であるか確認してください。

5.2. systemdユニットのステータスとログ

  • サービスが起動しない:

    • sudo systemctl status my-app-processor.serviceActive: failed などになっていないか確認。

    • sudo journalctl -u my-app-processor.service -f でエラーログを確認。ExecStart パス間違い、スクリプトの実行権限不足、依存サービスの未起動などが考えられます。

  • タイマーが動作しない:

    • sudo systemctl status my-app-processor.timerActive: active かつ Next: ... が期待通りか確認。

    • sudo systemctl list-timers --all で全てのタイマーとその状態を確認。

    • タイマーユニットの OnCalendar 設定が正しいか再確認。

5.3. 権限の問題

  • スクリプトが書き込みを試みるディレクトリ (/var/log/my_app/, /tmp) に対して、systemd サービスで指定された User が書き込み権限を持っているか確認します。

    • 例: sudo -u myuser test -w /var/log/my_app/ && echo "Writable" || echo "Not Writable"
  • APIキーや秘密情報へのアクセス権限も確認が必要です。

6. まとめ

jq を用いたJSONデータの高度処理を核とし、curl による堅牢なAPI連携、そして systemd による確実な定期実行を組み合わせたDevOps自動化のフレームワークを提示しました。set -euo pipefailtrap を活用した安全なBashスクリプト、一時ディレクトリの適切な利用、そして systemdUser ディレクティブによる権限分離は、運用負荷を軽減し、システムの安定性とセキュリティを向上させるために不可欠です。

DevOpsエンジニアは、これらのツールと原則を組み合わせることで、複雑なデータ処理パイプラインを構築し、日々の運用作業を効率的に自動化することが可能です。本記事で紹介した内容は、様々なAPI連携やデータ変換の基盤として応用できるでしょう。


[1] curl. (2022-03-30). curl 7.82.0. Retrieved from https://curl.se/changes.html

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

コメント

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