jqコマンドによるJSONデータ処理術

Tech

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

jqコマンドによるJSONデータ処理術

要件と前提

DevOpsの現場では、REST APIからのデータ取得、設定ファイルの管理、ログ解析など、JSON形式のデータを扱う機会が頻繁にあります。これらのタスクを効率的かつ自動的に処理するためには、jqコマンドが非常に強力なツールとなります。本記事では、jqコマンドを核として、curlによる安全なデータ取得、堅牢なBashスクリプトの作成、そしてsystemdによるサービス自動化までをDevOpsエンジニアの視点から解説します。

前提ツール

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

  • jq (バージョン1.6以上、最新の1.7.1は2024年7月17日にリリースされ、from_entriesの機能が強化されています [1, 2])

  • curl

  • bash

  • systemd (Linux環境)

セキュリティと堅牢性への配慮

処理スクリプトは以下の原則に基づき設計します。

  • Idempotent (冪等性): 何度実行しても同じ結果が得られるように設計します。

  • 安全なBashスクリプト: set -euo pipefailの活用、trapによるリソースクリーンアップ、mktempによる一時ファイルの安全な管理 [7]。

  • 最小権限の原則: サービスは必要最小限の権限で実行し、原則としてroot権限は使用しません。root権限が必要な場合は、細心の注意と権限分離を検討します。

実装

全体処理フロー

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

graph TD
    A["開始: JSON処理スクリプト実行"] --> B{"一時ディレクトリの安全な作成とクリーンアップ設定"};
    B --> C["curlによる外部APIからJSONデータ取得"];
    C --|HTTPエラー発生時| F["エラーログ出力と異常終了"];
    C --|データ取得成功時| D["jqによるJSONデータの抽出・加工"];
    D --> E["加工済みデータの利用または保存"];
    E --> G["処理完了: 一時ディレクトリ削除"];
    F --> G;

データの取得:curlを用いた安全なAPIアクセス

外部APIからJSONデータを取得する際、ネットワークエラーやサーバ側の問題に備える必要があります。以下のcurlコマンド例は、再試行、タイムアウト、TLS検証を考慮しています [3]。

#!/bin/bash


# set -euo pipefail はスクリプトの堅牢性を高めるための標準的な設定です。


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


# -u: 未定義の変数を使用した場合、エラーとします。


# -o pipefail: パイプライン中のコマンドが一つでも失敗した場合、パイプライン全体を失敗とします。

set -euo pipefail

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


# mktemp -d はセキュアな一時ディレクトリを作成します。


# TRAP_EXIT シグナルハンドラが終了時にこのディレクトリを削除します。

TMP_DIR=$(mktemp -d -t json_processor_XXXXXX)

# trap コマンドはスクリプト終了時に特定のコマンドを実行します。


# EXIT シグナルはスクリプトの正常終了・異常終了に関わらず常に実行されます。


# ERR シグナルはコマンドが失敗した時に実行されます。

trap 'rm -rf "${TMP_DIR}"' EXIT
trap 'echo "エラーが発生しました。一時ディレクトリ ${TMP_DIR} を削除して終了します。" >&2' ERR

API_ENDPOINT="https://api.example.com/data"
OUTPUT_FILE="${TMP_DIR}/response.json"
CA_CERT_PATH="/etc/ssl/certs/ca-certificates.crt" # 必要に応じて指定

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - APIからデータを取得中: ${API_ENDPOINT}"

# curlコマンドでAPIからJSONデータを安全に取得


# --fail: HTTPエラー(4xx, 5xx)が発生した場合、curlを失敗させます。


# --silent: 進捗表示を抑制します。


# --show-error: --silentと併用し、エラーメッセージを表示します。


# --retry 5: 失敗時に最大5回再試行します。


# --retry-connrefused: 接続拒否でも再試行します。


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


# --connect-timeout 10: 接続確立のタイムアウトを10秒とします。


# --max-time 30: 転送を含めた最大実行時間を30秒とします。


# --location: リダイレクトを自動的に追跡します。


# --compressed: Content-Encodingヘッダに従い、圧縮されたレスポンスを自動的に解凍します。


# --cacert: 特定のCA証明書パスを指定し、TLS検証を強化します。


# -o: 出力ファイルを指定します。

curl --fail --silent --show-error \
     --retry 5 --retry-connrefused --retry-max-time 60 \
     --connect-timeout 10 --max-time 30 \
     --location --compressed \
     --cacert "${CA_CERT_PATH}" \
     -o "${OUTPUT_FILE}" "${API_ENDPOINT}"

if [ ! -s "${OUTPUT_FILE}" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S JST') - エラー: APIからのレスポンスが空または不正です。" >&2
    exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - データ取得完了: ${OUTPUT_FILE}"

# ここからjqによるJSON処理


# ...

JSONデータの処理:jqの活用

取得したJSONファイルから必要な情報を抽出・加工します。例えば、特定のキーの値を取得したり、配列をフィルタリングしたり、新しいJSON構造を生成したりできます。

# ... (前述のcurlスクリプトの続き)

# 例1: JSONファイルから特定のフィールドを抽出


# 入力JSONの例: {"items": [{"id": 1, "name": "Item A", "status": "active"}, {"id": 2, "name": "Item B", "status": "inactive"}]}

echo "--- 全アイテムの名前とIDを抽出 ---"
jq -r '.items[] | "\(.id): \(.name)"' "${OUTPUT_FILE}"

# 例2: 特定の条件でフィルタリングし、新しいオブジェクトを生成


# 'select(.status == "active")': statusが"active"の要素のみを選択


# '{id, name}': 選択した要素からidとnameのみを含む新しいオブジェクトを生成

echo "--- アクティブなアイテムのみを抽出 ---"
jq '[.items[] | select(.status == "active") | {id, name}]' "${OUTPUT_FILE}"

# 例3: 複数キーを持つオブジェクトの配列から、特定の情報を集計


# '.data[]': "data"配列の各要素を処理


# '| select(.value > 100)' : valueが100より大きい要素のみを選択


# '| .key' : 選択した要素から"key"の値を取得


# '| unique' : 重複する値を削除

echo "--- valueが100を超えるキーの一覧 ---"
jq -r '[.data[] | select(.value > 100) | .key] | unique[]' "${OUTPUT_FILE}" <<EOF
{"data": [{"key": "A", "value": 120}, {"key": "B", "value": 90}, {"key": "A", "value": 150}]}
EOF

# 処理結果をファイルに保存する例

PROCESSED_DATA_FILE="${TMP_DIR}/processed_data.json"
jq '[.items[] | select(.status == "active")]' "${OUTPUT_FILE}" > "${PROCESSED_DATA_FILE}"
echo "$(date '+%Y-%m-%d %H:%M:%S JST') - 処理済みデータ保存: ${PROCESSED_DATA_FILE}"

# ... 処理済みのデータを利用する後続処理 ...

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - JSON処理完了。"
exit 0

systemdによるサービス化

定期的にJSONデータを取得・処理するためには、systemdのユニットとタイマー機能が非常に便利です [4, 5]。ここでは、上記のBashスクリプトを毎日午前3時に実行するサービスとして設定する例を示します。

1. サービススクリプトの配置

上記のBashスクリプトを /usr/local/bin/json-data-processor.sh として保存します。

# /usr/local/bin/json-data-processor.sh

#!/bin/bash

set -euo pipefail

TMP_DIR=$(mktemp -d -t json_processor_XXXXXX)
trap 'rm -rf "${TMP_DIR}"' EXIT
trap 'echo "$(date "+%Y-%m-%d %H:%M:%S JST") - ERROR: Script failed unexpectedly." >&2' ERR

API_ENDPOINT="https://api.example.com/data"
OUTPUT_FILE="${TMP_DIR}/response.json"
CA_CERT_PATH="/etc/ssl/certs/ca-certificates.crt"

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - Info: JSONデータ処理スクリプト開始."

curl --fail --silent --show-error \
     --retry 5 --retry-connrefused --retry-max-time 60 \
     --connect-timeout 10 --max-time 30 \
     --location --compressed \
     --cacert "${CA_CERT_PATH}" \
     -o "${OUTPUT_FILE}" "${API_ENDPOINT}"

if [ ! -s "${OUTPUT_FILE}" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S JST') - Error: APIレスポンスが空または不正です。" >&2
    exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - Info: データ取得完了。"

# jq処理の例 (ここでは簡略化し、idとnameを抽出して標準出力)

jq -r '[.items[] | {id, name}]' "${OUTPUT_FILE}"

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - Info: JSONデータ処理スクリプト終了."
exit 0

実行権限を付与します。 sudo chmod +x /usr/local/bin/json-data-processor.sh

2. systemd Unitファイルの作成

/etc/systemd/system/json-data-processor.service を作成します。

# /etc/systemd/system/json-data-processor.service

[Unit]
Description=Process JSON data from API
Documentation=https://example.com/docs/json-processor
After=network.target

[Service]

# User/Group: このサービスを実行するユーザーとグループを指定します。


# root以外の専用ユーザーを作成し、最小権限で実行することがセキュリティのベストプラクティスです。

User=datauser
Group=datauser

# WorkingDirectory: スクリプトの実行ディレクトリを指定します。

WorkingDirectory=/opt/json-processor

# ExecStart: サービスが起動したときに実行するコマンドを指定します。

ExecStart=/usr/local/bin/json-data-processor.sh

# Type=oneshot: 処理が完了すると終了するタイプのサービス。

Type=oneshot

# RemainAfterExit=yes: サービスが終了しても、systemdがサービスの状態を"active"として保持します。


#                     timerからの実行では通常不要ですが、手動実行時のステータス確認に役立ちます。

RemainAfterExit=no

# PrivateTmp=yes: サービス専用の一時名前空間を作成し、他のプロセスから一時ファイルが見えないようにします。

PrivateTmp=yes

# ProtectSystem=full: /usr, /boot, /etc をリードオンリーでマウントし、システムファイルの変更を防ぎます。

ProtectSystem=full

# NoNewPrivileges=yes: 特権エスカレーションを禁止します。

NoNewPrivileges=yes

# StandardOutput/StandardError: 標準出力と標準エラー出力をジャーナルログに送信します。

StandardOutput=journal
StandardError=journal

[Install]

# WantedBy: このサービスが有効化されたときに、どのターゲットに依存するかを指定します。


#           タイマーから起動されるため、ここでは指定しません。


#           手動でsystemctl enableしたい場合は multi-user.target を指定します。

注意: User=datauserGroup=datauser は事前に作成しておく必要があります (sudo useradd -r -s /bin/false datauser)。また、WorkingDirectory=/opt/json-processor ディレクトリも作成し、datauser が書き込み権限を持つように設定してください。

3. systemd Timerファイルの作成

/etc/systemd/system/json-data-processor.timer を作成します。

# /etc/systemd/system/json-data-processor.timer

[Unit]
Description=Run JSON data processor daily at 3 AM

# Documentation=... (必要に応じて追加)

[Timer]

# OnCalendar: スクリプトを実行するスケジュールを指定します。


#             "Daily" は毎日、"*-*-* 03:00:00" は毎日午前3時を意味します。

OnCalendar=Daily

# Persistent=true: タイマーの有効化中にサービスが実行されなかった場合、


#                  次回起動時に遅延実行を試みます。

Persistent=true

# Unit: タイマーがトリガーされたときに起動するサービスユニットを指定します。

Unit=json-data-processor.service

[Install]

# WantedBy=timers.target: タイマーシステムが起動したときにこのタイマーが有効化されるようにします。

WantedBy=timers.target

4. systemdサービスの有効化と起動

作成したユニットファイルをsystemdに認識させ、タイマーを有効化します。

# systemdの構成を再ロード

sudo systemctl daemon-reload

# タイマーを有効化し、次回起動時から自動的に実行されるように設定

sudo systemctl enable json-data-processor.timer

# タイマーを今すぐ起動(次回スケジュールまで待たずに初回実行をトリガー)

sudo systemctl start json-data-processor.timer

# サービスを手動で実行することも可能


# sudo systemctl start json-data-processor.service

検証

サービスが正しく設定され、意図した通りに動作することを確認します。

  1. タイマーの状態確認:

    systemctl list-timers | grep json-data-processor
    
    # 出力例:
    
    
    # NEXT                         LEFT          LAST                         PASSED      UNIT                         ACTIVATES
    
    
    # Mon 2024-07-30 03:00:00 JST  10h left      Sun 2024-07-29 03:00:00 JST  14h ago     json-data-processor.timer    json-data-processor.service
    

    NEXTLASTの時刻が正しいか確認します。

  2. サービスの状態確認:

    systemctl status json-data-processor.service
    
    # アクティブな場合、"Active: inactive (dead)" または "Active: active (exited)" と表示されます。
    
    
    # 重要なのは、`ExecStart`が成功したか、エラーで終了したかです。
    
  3. ジャーナルログの確認: サービス実行時の出力はjournalctlで確認できます。

    journalctl -u json-data-processor.service --since "today"
    
    # スクリプトの標準出力/エラー出力がここに表示されます。
    
    
    # エラーメッセージがないか、成功メッセージが出力されているかを確認します。
    
  4. 一時ファイルのクリーンアップ確認: スクリプトが終了した後、一時ディレクトリがrm -rf "${TMP_DIR}"によって正しく削除されていることを確認します。手動実行してみて、/tmpなどを確認すると良いでしょう。

運用

本サービスを安定して運用するためには、以下の点に留意します。

  • ログの監視: journalctlのログを定期的に確認するか、Fluentd/Lokiなどのログ収集システムと連携させ、異常が発生した際にアラートが上がるように設定します。

  • エラー通知: スクリプト内でエラー発生時にSlackやメールなどの通知システムに連携する機能を組み込むことで、問題に迅速に対応できます。ExecStopPostディレクティブをserviceユニットに追加し、異常終了時に通知スクリプトを実行することも可能です。

    # json-data-processor.service 内
    
    [Service]
    
    # ...
    
    OnFailure=notify-on-failure@%n.service # 失敗時に特定のサービスを実行する
    

    または、スクリプト内で trap ERR にて通知処理を組み込むこともできます。

  • バージョン管理: jqスクリプトやBashスクリプト、systemdユニットファイルはGitなどのバージョン管理システムで管理し、変更履歴を追跡できるようにします。

  • 権限管理の継続的な見直し: サービス実行ユーザーの権限は、ビジネス要件の変化に合わせて定期的に見直し、常に最小権限を維持するようにします。

トラブルシュート

問題が発生した場合の一般的なトラブルシュート手順です。

  1. systemctl statusjournalctlの確認:

    • sudo systemctl status json-data-processor.service でサービスの状態を確認し、エラーメッセージがないか確認します。

    • journalctl -u json-data-processor.service -f でリアルタイムのログを確認し、エラーメッセージやタイムアウト情報を特定します。

  2. スクリプトの手動実行: サービススクリプト (/usr/local/bin/json-data-processor.sh) を直接手動で実行し、エラーが発生しないか、期待通りの出力が得られるかを確認します。set -x をスクリプトに追加して詳細な実行トレースを有効にすると、デバッグに役立ちます。

  3. jqコマンドのエラー: jqは構文エラーの場合、具体的なエラーメッセージを出力します。例えば、「parse error: Expected another JSON value at line X, column Y」のようなメッセージは、入力JSONが不正であるか、jqフィルタの構文が誤っていることを示します。

  4. curlコマンドのエラー:

    • ネットワーク接続の問題か (Connection refused, Could not resolve host)。

    • APIエンドポイントがダウンしているか。

    • 認証情報やAPIキーが誤っているか。

    • TLS証明書の問題か (SSL certificate problem: unable to get local issuer certificate)。--insecureオプションはデバッグ目的でのみ使用し、本番環境では--cacertなどで証明書チェーンを正しく設定します。

  5. 権限の問題:

    • サービス実行ユーザー (datauser) が、スクリプトや必要なファイル、ディレクトリへの読み書き権限を持っているか確認します。

    • WorkingDirectoryTMP_DIRのパーミッションが正しく設定されているか確認します。

まとめ

jqコマンドを核としたJSONデータ処理の自動化について、DevOpsエンジニアが実践すべき堅牢な手法を解説しました。curlによる安全なAPI連携、set -euo pipefailtrapを活用したBashスクリプトの堅牢化、そしてsystemdによるサービス化とタイマーでの自動実行まで、一連のプロセスを網羅しました。

これらの技術を組み合わせることで、システムの運用におけるデータ収集、加工、レポート作成などのタスクを効率的に自動化し、DevOpsの実践において大きな価値をもたらすことができます。セキュリティ、冪等性、最小権限の原則を常に意識し、安定したシステム運用を目指しましょう。


参照情報:

[1] jq 1.7.1 リリースノート. (2024年7月17日). GitHub. https://github.com/jqlang/jq/releases/tag/jq-1.7.1 [2] jq Manual. (2024年7月17日). jq Project. https://jqlang.github.io/jq/manual/ [3] curl Manual. (取得日: 2024年7月29日). curl Project. https://curl.se/docs/manpage.html [4] systemd.service(5) man page. (取得日: 2024年7月29日). freedesktop.org. https://www.freedesktop.org/software/systemd/man/systemd.service.html [5] systemd.timer(5) man page. (取得日: 2024年7月29日). freedesktop.org. https://www.freedesktop.org/software/systemd/man/systemd.timer.html [6] mktemp man page (GNU Coreutils). (取得日: 2024年7月29日). GNU Project. (具体的なURLはディストリビューションに依存するため割愛) [7] Advanced Bash-Scripting Guide. (取得日: 2024年7月29日). (具体的なURLは複数存在するため割愛)

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

コメント

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