jqコマンドによるJSONデータ変換とフィルタリング:DevOpsにおける実践例

Tech

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

jqコマンドによるJSONデータ変換とフィルタリング:DevOpsにおける実践例

DevOpsの現場では、APIからのデータ取得、ログ解析、設定ファイルの管理など、JSONデータを扱う機会が頻繁にあります。本記事では、強力なコマンドラインJSONプロセッサであるjqを用いて、JSONデータの変換とフィルタリングを行う実践的な方法を解説します。さらに、curlコマンドでの安全なデータ取得、systemdを用いた処理の定期実行、そして安全なシェルスクリプトの原則までをDevOpsエンジニアの視点から深掘りします。

要件と前提

本記事で解説する手順を実践するために、以下のコマンドと原則が必要です。

jqコマンドのインストール

jqは多くのLinuxディストリビューションでパッケージマネージャを通じて利用可能です。 例えば、Debian/Ubuntu系ではapt、RHEL/CentOS系ではyumまたはdnfでインストールできます。

# Debian/Ubuntuの場合

sudo apt update
sudo apt install -y jq

# RHEL/CentOS/Fedoraの場合

sudo yum install -y jq

# または

sudo dnf install -y jq

jqのバージョンは、執筆時点(2024年7月29日)で最新のjq-1.7.1 [1](2024年3月31日リリース)を推奨します。

curlコマンドの利用

外部APIからのデータ取得にはcurlコマンドを使用します。最新のTLSプロトコルや再試行機能を利用するため、最新版のcurlがインストールされていることを確認してください。

安全なシェルスクリプトの原則

堅牢な自動化スクリプトを作成するため、以下のシェルスクリプト原則を厳守します。

  • set -euo pipefail: 未定義変数の参照 (-u)、ゼロ以外の終了コードでの終了 (-e)、パイプライン中のエラー伝播 (-o pipefail) を強制します。

  • trap: スクリプト終了時に一時ファイルを確実にクリーンアップします。

  • mktemp -d: 一時ファイルを安全な方法で作成します。

root権限の扱いと権限分離

systemdの設定ファイルの配置やサービスの有効化にはroot権限が必要です。しかし、実際にサービスを実行する際には、セキュリティの観点から最小権限の原則に従い、専用の非特権ユーザーアカウントを使用することを強く推奨します。これにより、万が一サービスが侵害された場合でも、システム全体への影響を最小限に抑えることができます。

実装

JSONデータ取得と基本フィルタリング

ここでは、curlを用いて外部APIからJSONデータを取得し、jqで基本的なフィルタリングと抽出を行う例を示します。信頼性とセキュリティを考慮したcurlのオプションを含みます。

#!/bin/bash

set -euo pipefail

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

TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT

# APIエンドポイント (例: ダミーのJSONデータを返すAPI)

API_URL="https://jsonplaceholder.typicode.com/posts"
OUTPUT_FILE="${TMP_DIR}/posts.json"
FILTERED_FILE="${TMP_DIR}/filtered_posts.json"

echo "INFO: Fetching data from ${API_URL}..."

# curlでJSONデータを取得


# -s: サイレントモード


# -S: エラー時のみ表示


# -L: リダイレクトを追跡


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


# --retry-delay 5: 再試行間隔を5秒に設定


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


# --tlsv1.2: TLSv1.2の使用を強制 (必要に応じて --tlsv1.3 も検討)


# --fail-with-body: HTTPエラー時にレスポンスボディを表示し、終了コードを非ゼロにする

if ! curl -sSL --retry 5 --retry-delay 5 --retry-max-time 60 --tlsv1.2 --fail-with-body "${API_URL}" -o "${OUTPUT_FILE}"; then
    echo "ERROR: Failed to fetch data from API." >&2
    exit 1
fi

echo "INFO: Original JSON data saved to ${OUTPUT_FILE}"

# jqでデータをフィルタリング (idが5より大きく、かつuserIdが1の投稿を抽出)


# .[]: 配列の要素を個別に処理


# select(.id > 5 and .userId == 1): 条件に合う要素のみを選択


# .title: 選択された要素からtitleフィールドのみを抽出

echo "INFO: Filtering JSON data..."
jq '.[] | select(.id > 5 and .userId == 1) | .title' "${OUTPUT_FILE}" > "${FILTERED_FILE}"

echo "INFO: Filtered titles saved to ${FILTERED_FILE}. Content:"
cat "${FILTERED_FILE}"

# 例: 最初の10件の投稿のIDとタイトルを抽出

echo -e "\nINFO: Extracting ID and Title for first 10 posts:"
jq '.[0:10] | .[] | {id: .id, title: .title}' "${OUTPUT_FILE}"

# このスクリプトは指定されたAPIからデータを取得し、特定の条件でフィルタリングした投稿のタイトルを出力します。


# 入力: APIからのJSONデータ。


# 出力: フィルタリングされた投稿タイトルリスト。


# 前提: curl, jqがインストールされていること。


# 計算量: jqの処理はJSONデータのサイズとフィルタリングの複雑さに比例。線形時間O(N)。


# メモリ条件: 処理するJSONデータのサイズに依存。大規模データの場合はストリーミング処理(jq -n --stream)を検討。

上記のcurlコマンドは、ネットワークの一時的な問題に対応するための再試行 (--retry) や、特定のTLSバージョン (--tlsv1.2) の強制、HTTPエラー時にレスポンスボディを表示する (--fail-with-body) など、堅牢なデータ取得のためのオプションを含んでいます[2]。

複雑な変換と集計

jqはより複雑なデータ変換や集計も得意です。ここでは、map関数やオブジェクトの再構築、変数を用いた例を示します。

#!/bin/bash

set -euo pipefail

INPUT_JSON='[
  {"name": "Alice", "score": 85, "active": true},
  {"name": "Bob", "score": 92, "active": true},
  {"name": "Charlie", "score": 78, "active": false}
]'

echo "INFO: Original JSON:"
echo "${INPUT_JSON}" | jq '.'

# アクティブなユーザーのみを抽出し、スコアを10点加算した新しいオブジェクトに変換


# `map(...)`: 配列の各要素に関数を適用し、新しい配列を生成


# `select(.active)`: activeがtrueの要素のみを選択


# `{name: .name, score: (.score + 10)}`: 新しいオブジェクトを構築し、スコアを更新

echo -e "\nINFO: Transformed and aggregated JSON:"
echo "${INPUT_JSON}" | jq 'map(select(.active) | {name: .name, score: (.score + 10)})'

# すべてのユーザーの平均スコアを計算

echo -e "\nINFO: Average score of all users:"
echo "${INPUT_JSON}" | jq 'map(.score) | add / length'

# jq --arg で外部から変数を渡す例

echo -e "\nINFO: Filtering with external argument (threshold=80):"
THRESHOLD=80
echo "${INPUT_JSON}" | jq --argjson threshold "$THRESHOLD" '.[] | select(.score >= $threshold)'

# このスクリプトは、インラインJSONデータに対してjqを用いた複雑な変換と集計の例を示します。


# 入力: インラインJSON文字列。


# 出力: 変換または集計されたJSONデータ。


# 前提: jqがインストールされていること。


# 計算量: JSONデータのサイズとフィルタリングの複雑さに比例。


# メモリ条件: 処理するJSONデータのサイズに依存。

定期実行のためのsystemd unit/timer

先のcurljqを組み合わせたスクリプトを、systemdのユニットとタイマー機能を用いて定期実行する構成を考えます。これにより、OSの起動と同時にサービスが開始され、指定した間隔で処理が自動実行されるようになります[3]。

1. スクリプトの準備

まず、実行するスクリプトを作成します。例として、先ほどの基本フィルタリングのスクリプトをdata_processor.shとして保存します。

#!/bin/bash

set -euo pipefail

# スクリプトのパスは適宜調整してください

SCRIPT_DIR="/opt/data_processor"
LOG_DIR="/var/log/data_processor"
mkdir -p "$LOG_DIR" # ログディレクトリを事前に作成
cd "$SCRIPT_DIR" || exit 1

# スクリプト実行ユーザーの環境変数を読み込む場合


# if [ -f /etc/data_processor/env ]; then


#   . /etc/data_processor/env


# fi

TMP_DIR=$(mktemp -d --tmpdir=/var/tmp/data_processor) # /var/tmp を使用し、パーミッションに注意
trap 'rm -rf "$TMP_DIR"' EXIT

API_URL="https://jsonplaceholder.typicode.com/posts"
OUTPUT_FILE="${TMP_DIR}/posts.json"
FILTERED_FILE="${TMP_DIR}/filtered_posts.json"
LOG_FILE="${LOG_DIR}/process_$(date +%Y%m%d).log" # 日付ごとにログファイルを分ける

exec > >(tee -a "$LOG_FILE") 2>&1 # 標準出力と標準エラー出力をログファイルとターミナルに出力

echo "$(date +%Y-%m-%dT%H:%M:%S) INFO: Starting data processing."

if ! curl -sSL --retry 5 --retry-delay 5 --retry-max-time 60 --tlsv1.2 --fail-with-body "${API_URL}" -o "${OUTPUT_FILE}"; then
    echo "$(date +%Y-%m-%dT%H:%M:%S) ERROR: Failed to fetch data from API."
    exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%S) INFO: Original JSON data saved to ${OUTPUT_FILE}"

jq '.[] | select(.id > 5 and .userId == 1) | .title' "${OUTPUT_FILE}" > "${FILTERED_FILE}"
echo "$(date +%Y-%m-%dT%H:%M:%S) INFO: Filtered titles saved to ${FILTERED_FILE}. Content:"
cat "${FILTERED_FILE}"
echo "$(date +%Y-%m-%dT%H:%M:%S) INFO: Data processing finished."

# このスクリプトは、APIからJSONデータを取得し、jqでフィルタリングした結果をログファイルに記録します。


# 入力: 外部APIからのJSONデータ。


# 出力: フィルタリングされた結果がログファイルに記録される。


# 前提: curl, jqがインストールされていること。スクリプトが/opt/data_processorに配置され、実行権限が付与されていること。


# 計算量: JSONデータのサイズとフィルタリングの複雑さに比例。


# メモリ条件: 処理するJSONデータのサイズに依存。

data_processor.sh/opt/data_processor/data_processor.shとして保存し、実行権限を付与します。

sudo mkdir -p /opt/data_processor
sudo mv data_processor.sh /opt/data_processor/
sudo chmod +x /opt/data_processor/data_processor.sh
sudo mkdir -p /var/log/data_processor # ログディレクトリも作成

2. systemd Unitファイルの作成

/etc/systemd/system/data-processor.serviceとして以下の内容で保存します。

[Unit]
Description=Data Processor Service
Documentation=https://example.com/data-processor
Requires=network-online.target
After=network-online.target

[Service]

# 実行ユーザーを専用の非特権ユーザーに設定 (存在しない場合は作成が必要)

User=datauser
Group=datauser

# スクリプトの実行ディレクトリ

WorkingDirectory=/opt/data_processor

# 実行するコマンド

ExecStart=/opt/data_processor/data_processor.sh

# サービスが予期せず終了した場合に自動的に再起動

Restart=on-failure
RestartSec=5s

# 標準出力と標準エラー出力をjournaldに送る

StandardOutput=journal
StandardError=journal

# 環境変数をファイルから読み込む場合 (例: APIキー)


# EnvironmentFile=/etc/data_processor/env


# 一時ディレクトリの管理 (systemd-tmpfiles または RuntimeDirectory を利用するのがより安全)


# PrivateTmp=true # サービス専用の一時ディレクトリを/tmp以下に作成

[Install]
WantedBy=multi-user.target

root権限の注意点: User=datauserGroup=datauserで、サービスがdatauserという非特権ユーザーで実行されるように設定しています。datauserが存在しない場合は、sudo useradd -r -s /bin/false datauserなどで事前に作成してください。/opt/data_processorディレクトリの所有権もdatauserに設定することを検討してください。

3. systemd Timerファイルの作成

/etc/systemd/system/data-processor.timerとして以下の内容で保存します。

[Unit]
Description=Run data processor service daily

# data-processor.service ユニットと関連付ける

Requires=data-processor.service

[Timer]

# 毎日午前3時に実行

OnCalendar=*-*-* 03:00:00

# タイマーが欠落した場合でも、システムの起動時にすぐに実行

Persistent=true

# タイマーがトリガーされるたびに、最大30秒のランダムな遅延を追加

RandomDelaySec=30

[Install]
WantedBy=timers.target

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

タイマーを有効化し、サービスを起動します。

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

これにより、data-processor.serviceは毎日午前3時に自動実行されるようになります。

検証

実装したスクリプトとsystemdサービスが意図通りに動作するかを確認します。

手動実行による検証

まず、data_processor.shスクリプトを単体で実行し、エラーがないこと、期待する出力が得られることを確認します。

/opt/data_processor/data_processor.sh

実行後、/var/log/data_processor/ディレクトリにログファイルが作成され、その内容が正しいかを確認します。

systemdサービスのテスト

systemdサービスを手動で起動し、動作を確認します。

sudo systemctl start data-processor.service
sudo systemctl status data-processor.service

statusコマンドでActive: active (exited)またはActive: active (running)と表示されれば正常に起動しています。サービスが実行したログはjournalctlで確認できます。

journalctl -u data-processor.service --since "1 hour ago"

これにより、スクリプトの標準出力および標準エラー出力がjournaldに記録されていることを確認できます。

運用

システム運用における考慮事項です。

ログ監視

systemdで実行されるサービスは、そのログがjournaldに集約されます。定期的なログ確認や、異常発生時のアラート設定が重要です。

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

journalctl -f -u data-processor.service

# 特定の日付以降のログを確認

journalctl -u data-processor.service --since "2024-07-29 00:00:00 JST"

ログローテーションはjournaldが自動的に行いますが、必要に応じて/etc/systemd/journald.confで設定を調整できます。

権限管理とセキュリティ

  • 最小権限の原則: systemdユニットファイルでUser=Group=ディレクティブを適切に設定し、サービスが不要な権限を持たないようにします。

  • APIキーの管理: APIキーなどの機密情報はスクリプトに直接記述せず、EnvironmentFileを通じて安全に読み込むか、Vaultのようなシークレット管理ツールを使用することを推奨します。EnvironmentFileを利用する場合、ファイルはrootのみが読み書きできる権限 (chmod 600) に設定すべきです。

冪等性

定期実行されるスクリプトは、複数回実行されてもシステムの状態が不整合にならないよう、冪等性 (idempotence) を確保することが重要です。例えば、ファイルへの書き込みは既存の内容を上書きするか、日付つきファイル名で新しいファイルを作成するように設計します。

トラブルシュート

問題が発生した場合の対処法です。

jqのエラーメッセージ解析

jqは構文エラーや型エラーに対して比較的明確なエラーメッセージを出力します。

# 誤ったjqクエリの例

echo '{"key": "value"}' | jq '.key['

# 出力: jq: error: Expected value but found ']' (syntax error) at <top-level>, line 1:


# {"key": "value"}


#                 ^

エラーメッセージに示される行番号と位置を基に、クエリを見直してください。

curlのエラーハンドリング

curlが失敗した場合、以下の点を確認します。

  • HTTPステータスコード: --verboseオプションを追加して、詳細なリクエスト/レスポンス情報を確認します。--fail-with-bodyを使用している場合、HTTPエラーのレスポンスボディも確認できます。

  • ネットワーク接続: 対象APIへの接続性 (pingtelnet) を確認します。

  • TLS/SSL証明書: 証明書のエラーが発生していないか確認します。--insecureオプションはデバッグ目的でのみ使用し、本番環境では避けてください。

systemdのデバッグ

systemd関連の問題は、以下のコマンドで診断します。

  • sudo systemctl status data-processor.service: サービスの現在の状態と最新のログを確認します。

  • sudo journalctl -xe -u data-processor.service: サービスの詳細なログとエラーメッセージを確認します。-xで追加の説明、-eで最終エントリから表示します。

  • sudo systemd-analyze verify /etc/systemd/system/data-processor.service: ユニットファイルの構文エラーをチェックします。

Mermaidによる処理フロー

curljqsystemdを組み合わせた一連のデータ処理フローを以下に示します。

graph TD
    A["外部API"] -- HTTPS GETリクエスト |curl| --> B("JSONデータ取得");
    B --> C{"jqコマンドによるフィルタリング/変換"};
    C -- 処理済みJSON --> D["ファイル出力/次処理"];
    D -- 定期実行をトリガー |systemd.timer| --> E("systemd.service実行");
    E -- ログ出力 --> F["journalctlによる監視"];

まとめ

jqコマンドを用いたJSONデータの効率的な変換とフィルタリング、curlによる安全なAPI連携、そしてsystemdによる処理の定期実行について、DevOpsの実践的な観点から解説しました。set -euo pipefailなどの安全なシェルスクリプトの原則と、最小権限の原則に基づいたsystemdの権限分離により、堅牢で信頼性の高い自動化システムを構築できます。これらのテクニックを習得することで、データ処理の自動化と運用効率の向上に大きく貢献できるでしょう。


[1] stedolan. jq GitHub Releases. (2024年3月31日). https://github.com/stedolan/jq/releases/tag/jq-1.7.1 [2] curl. curl man page. (2024年7月29日最終アクセス). https://curl.se/docs/manpage.html [3] freedesktop.org. systemd.service man page. (2024年7月29日最終アクセス). https://www.freedesktop.org/software/systemd/man/systemd.service.html [4] freedesktop.org. systemd.timer man page. (2024年7月29日最終アクセス). https://www.freedesktop.org/software/systemd/man/systemd.timer.html

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

コメント

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