jqコマンドによるJSONデータ変換とsystemdによる自動化

Tech

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

jqコマンドによるJSONデータ変換とsystemdによる自動化

DevOps環境におけるデータ連携では、APIからのJSON取得と整形が頻繁に発生します。本記事では、軽量で強力なJSONプロセッサである jq コマンドを活用し、curl による安全なAPIアクセス、そして systemd による定期的な自動実行を組み合わせる実践的なDevOpsプラクティスを紹介します。冪等性、セキュリティ、エラーハンドリングを考慮した堅牢なスクリプトとシステム設計について解説します。

要件と前提

要件

  • 外部APIからJSONデータを取得し、特定のフィールドを抽出・整形する。

  • 整形したJSONデータをファイルに出力する。

  • 処理は定期的に自動実行されること。

  • スクリプトは冪等であり、中断されても安全に再実行できること。

  • セキュリティを考慮し、最小権限の原則を遵守すること。

  • エラー発生時には適切なログが出力されること。

前提

  • OS: CentOS Stream 9 / Ubuntu Server 22.04 LTS などのLinuxディストリビューション

  • インストール済みのコマンド: jq (バージョン 1.7以降を推奨 [1]), curl (バージョン 7.x以降を推奨), bash (バージョン 4.x以降を推奨), systemd

  • システム時刻がJST(日本標準時)に設定されていること。

実装

処理フローの概要

本記事で構築するJSONデータ変換と自動実行のプロセスは、以下のフローチャートで示されます。

graph TD
    A["開始: systemdタイマー"] --> B{"systemd Unitが起動"}
    B -- 起動 --> C("シェルスクリプト実行")
    C --> D("一時ディレクトリ作成 | & クリーンアップ設定")
    D --> E("curlで外部APIからJSON取得 | TLS検証, リトライ設定")
    E -- 成功 --> F("jqでJSONデータ変換 | フィルタリング, 整形")
    E -- 失敗 --> H("エラー処理 & ログ出力 | ステータスコード, エラーメッセージ")
    F -- 成功 --> G("変換済みデータをファイル出力 | 冪等なファイル名")
    F -- 失敗 --> H
    G --> I("終了: 一時ディレクトリ削除")
    H --> I

安全なBashスクリプトの作成

JSONデータ変換を行うスクリプトは、一時ファイルの管理、エラーハンドリング、そして外部APIへの安全な接続が重要です。以下に、これらの要素を考慮したBashスクリプトの例を示します。

スクリプトの基本構造とセキュリティ対策

冪等性を確保し、エラー時にクリーンアップを行うために、set -euo pipefailtrap コマンド、そして mktemp -d による一時ディレクトリの利用は必須です [5]。

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

  • set -u: 未定義の変数を使用しようとするとエラーになります。

  • set -o pipefail: パイプライン中の任意のコマンドが失敗した場合、パイプライン全体が失敗したとみなされます。

  • trap '...' EXIT: スクリプトの終了時(正常終了、エラー終了問わず)に指定したコマンドを実行します。これにより一時ディレクトリの削除などを確実に実行できます。

#!/bin/bash


# スクリプト名: process_json_data.sh

# --- 1. スクリプトのセキュリティと堅牢性の設定 ---

set -euo pipefail

# スクリプト実行日時 (JST) - 出力ファイル名に使用

TIMESTAMP=$(date +%Y%m%d%H%M%S)
LOG_FILE="/var/log/json_processor/process_json_data.log" # ログファイルパス
API_URL="https://api.example.com/data" # 取得元APIのURL (仮)
OUTPUT_DIR="/var/lib/json_processor/processed" # 変換済みJSON出力ディレクトリ

# ログディレクトリと出力ディレクトリの作成 (冪等に)

mkdir -p "$(dirname "${LOG_FILE}")"
mkdir -p "${OUTPUT_DIR}"

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


# mktemp -d で安全な一時ディレクトリを作成


# `trap` を用いて、スクリプト終了時に一時ディレクトリを確実に削除する

TMP_DIR=$(mktemp -d -t json_proc_XXXXXX)
if [[ ! -d "${TMP_DIR}" ]]; then
    echo "$(date +'%Y-%m-%d %H:%M:%S JST') [ERROR] Failed to create temporary directory. Exiting." | tee -a "${LOG_FILE}"
    exit 1
fi

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

trap 'rm -rf "${TMP_DIR}"' EXIT

echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] Script started. Temporary directory: ${TMP_DIR}" | tee -a "${LOG_FILE}"

# --- 3. JSONデータの取得 (curlとTLS/再試行/バックオフ) ---


# curl を用いて外部APIからJSONデータを取得


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


# --fail-with-body: HTTPエラーが発生した場合、レスポンスボディを表示して終了ステータス1を返す


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


# --retry-connrefused: 接続拒否でも再試行


# --retry-max-time 30: 再試行を含めて最大30秒


# --connect-timeout 5: 接続確立のタイムアウトを5秒に設定


# --max-time 10: ファイル転送全体のタイムアウトを10秒に設定


# --cacert: サーバ証明書の検証に使用するCA証明書ファイルを指定 (セキュリティ強化のため推奨)


# --tlsv1.2: TLSv1.2のみを使用 (最新のセキュリティ標準に準拠)

API_RESPONSE_FILE="${TMP_DIR}/api_response.json"
echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] Fetching JSON data from ${API_URL}..." | tee -a "${LOG_FILE}"
if ! curl -sS \
          --fail-with-body \
          --retry 5 \
          --retry-connrefused \
          --retry-max-time 30 \
          --connect-timeout 5 \
          --max-time 10 \
          --cacert /etc/pki/tls/certs/ca-bundle.crt \
          --tlsv1.2 \
          "${API_URL}" -o "${API_RESPONSE_FILE}"; then
    echo "$(date +'%Y-%m-%d %H:%M:%S JST') [ERROR] Failed to fetch data from API. See curl output above." | tee -a "${LOG_FILE}"
    exit 1
fi
echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] JSON data fetched successfully." | tee -a "${LOG_FILE}"

# --- 4. JSONデータの変換 (jqの活用例) ---


# 取得したJSONデータをjqで処理


# 例: id, name, status, timestampを抽出し、新しいJSONオブジェクトとして整形


#    timestampはISO 8601形式に変換し、JSTであることを明示


# jq 1.7以降では`--argfile`が利用可能です [1]。

PROCESSED_DATA_FILE="${OUTPUT_DIR}/processed_data_${TIMESTAMP}.json"
echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] Processing JSON data with jq..." | tee -a "${LOG_FILE}"

if ! jq -r '[.[] | {
    id: .id,
    name: .name,
    status: .status,
    updated_at: (.timestamp | strptime("%Y-%m-%dT%H:%M:%SZ") | localtime | strftime("%Y-%m-%dT%H:%M:%S%z"))
}]' "${API_RESPONSE_FILE}" > "${PROCESSED_DATA_FILE}"; then
    echo "$(date +'%Y-%m-%d %H:%M:%S JST') [ERROR] Failed to process JSON data with jq." | tee -a "${LOG_FILE}"
    exit 1
fi
echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] JSON data processed and saved to ${PROCESSED_DATA_FILE}" | tee -a "${LOG_FILE}"

echo "$(date +'%Y-%m-%d %H:%M:%S JST') [INFO] Script finished successfully." | tee -a "${LOG_FILE}"
exit 0
  • 入出力: APIからJSONを入力し、jq で整形後、新しいJSONファイルを ${OUTPUT_DIR} に出力。ログは ${LOG_FILE} に出力。

  • 前提: jq, curl, bash がインストール済み。API_URLはJSONを返すことを想定。

  • 計算量: jq の処理はJSONデータのサイズに比例。O(N)

  • メモリ条件: 一時ファイルとしてAPIレスポンス全体をメモリにロードする可能性がある。巨大なJSONの場合はストリーミング処理を検討。

root権限の扱いと権限分離

上記のスクリプトは、一般ユーザーで実行することを想定しています。特に systemd サービスとして実行する場合、root 権限ではなく、専用のシステムユーザー(例: json_processor)を作成し、そのユーザーでサービスを実行することが強く推奨されます。これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます [6]。

  • systemdUser= および Group= オプションで実行ユーザーとグループを指定します。

  • ${OUTPUT_DIR}${LOG_FILE} は、そのユーザーが書き込み権限を持つように設定します。

systemd Unit/Timerによる定期実行

systemd を使用して、上記のBashスクリプトを定期的に自動実行します。systemd.service で実行する処理を定義し、systemd.timer でそのサービスを起動するスケジュールを定義します [3][4]。

Service Unitファイルの作成

/etc/systemd/system/json-processor.service として保存します。

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

[Unit]
Description=JSON Data Processor Service
Documentation=https://example.com/docs/json-processor
After=network-online.target # ネットワークが利用可能になった後に起動
Wants=network-online.target

[Service]
Type=oneshot # 実行後すぐに終了するタイプのサービス
User=json_processor # スクリプトを実行するユーザーを指定 (rootではない)
Group=json_processor # スクリプトを実行するグループを指定
WorkingDirectory=/opt/json_processor # スクリプトが実行される作業ディレクトリ
ExecStart=/opt/json_processor/process_json_data.sh # 実行するスクリプトのフルパス
StandardOutput=append:/var/log/json_processor/service.log # 標準出力をログファイルに追記
StandardError=append:/var/log/json_processor/service.log # 標準エラー出力をログファイルに追記

# ExitCode=0以外で失敗とみなす


# Restart=on-failure # サービスが失敗した場合に再起動 (oneshotの場合は通常不要)


# TimeoutStartSec=60 # サービス起動のタイムアウト

[Install]
WantedBy=timers.target # このサービスはタイマーによって起動されることを示唆
  • User/Group: json_processor ユーザーとグループは事前に作成し、/opt/json_processor ディレクトリとログ・出力ディレクトリ (/var/log/json_processor, /var/lib/json_processor/processed) に適切な権限を付与してください。

    • 例: sudo useradd -r -s /sbin/nologin json_processor

    • 例: sudo chown -R json_processor:json_processor /opt/json_processor /var/log/json_processor /var/lib/json_processor

Timer Unitファイルの作成

/etc/systemd/system/json-processor.timer として保存します。

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

[Unit]
Description=Run JSON Data Processor Service daily at 03:00 JST
Documentation=https://example.com/docs/json-processor

# After=network-online.target # タイマー自体はネットワークに依存しないが、Serviceが依存する場合がある

[Timer]
OnCalendar=*-*-* 03:00:00 # 毎日午前3時00分00秒 (JST) に起動
AccuracySec=1min # スケジュール時刻から最大1分程度のずれを許容
Persistent=true # タイマーが非アクティブな間に発生したイベントは、次回起動時にすぐに実行される
Unit=json-processor.service # このタイマーが起動するサービスユニット

[Install]
WantedBy=timers.target # タイマーが自動起動されるようにする

systemdの設定と有効化

  1. systemd設定のリロード: 新しいUnitファイルを認識させるために systemd デーモンをリロードします。

    sudo systemctl daemon-reload
    
  2. タイマーの有効化と起動: タイマーを有効化し、すぐに起動します。これにより、次回指定時刻から定期実行が開始されます。

    sudo systemctl enable json-processor.timer
    sudo systemctl start json-processor.timer
    
  3. タイマーの動作確認: タイマーの現在の状態を確認します。

    sudo systemctl status json-processor.timer
    
    # Active: active (waiting) や Next: Mon 2024-07-30 03:00:00 JST が表示されればOK
    

    現在 2024年7月29日 JST なので、次の実行は 2024年7月30日 03:00:00 JST と表示されるはずです。

検証

手動実行による検証

スクリプト単体でエラーなく動作するかを確認します。

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

sudo mkdir -p /opt/json_processor /var/log/json_processor /var/lib/json_processor/processed
sudo cp process_json_data.sh /opt/json_processor/
sudo chmod +x /opt/json_processor/process_json_data.sh

# 専用ユーザーで実行 (権限確認のため)

sudo -u json_processor /opt/json_processor/process_json_data.sh

実行後、/var/lib/json_processor/processed ディレクトリに整形されたJSONファイルが出力されているか、/var/log/json_processor/process_json_data.log にエラーがないかを確認します。

systemdタイマーの動作確認

タイマーが適切に設定され、次回の実行時刻が表示されていることを確認します。

sudo systemctl list-timers --all

NEXT 列に意図した時刻が表示されていること、LEFT 列に残りの時間が表示されていることを確認します。

ログの確認

スクリプトと systemd サービスの両方のログを確認します。

  • スクリプト固有のログ:

    cat /var/log/json_processor/process_json_data.log
    
  • systemdサービスログ:

    journalctl -u json-processor.service
    journalctl -u json-processor.timer
    

    これらのログを確認することで、スクリプトの実行状況、API通信の成否、jq処理の成否、および systemd による起動が期待通りに行われているかを詳細に把握できます。

運用

権限管理とセキュリティ

  • 最小権限の原則: json_processor ユーザーは、この処理に必要なファイルやディレクトリへの最小限の権限のみを持つようにします。/opt/json_processor/var/log/json_processor/var/lib/json_processor/processedjson_processor ユーザーが所有し、他のユーザーからの読み書きを制限します。

  • 認証情報: API認証が必要な場合、ハードコードは避け、環境変数、または systemdEnvironmentFile オプションでセキュアに管理します。

  • 証明書: curl--cacert オプションで、信頼できるCA証明書バンドルを使用し、TLS通信の安全性を確保します。

パフォーマンスとリソース監視

  • リソース消費: jqcurl の実行が長時間に及ぶ場合、CPUやメモリの使用量を監視し、必要に応じてスクリプトや処理ロジックの最適化を検討します。

  • ログローテーション: journalctl が管理する systemd ログは自動的にローテーションされますが、スクリプトが出力する /var/log/json_processor/process_json_data.log については、logrotate などのツールを設定してログファイルの肥大化を防ぎます。

設定の変更とデプロイ

  • 設定管理: スクリプト内のAPI URLや出力パスなどの設定値は、スクリプトの外部ファイル(例: /etc/json_processor/config.env)に切り出し、source コマンドで読み込むことで、スクリプト本体の変更なしに設定変更を可能にします。

  • デプロイ: Gitなどのバージョン管理システムでスクリプトと systemd Unitファイルを管理し、CI/CDパイプラインを通じてセキュアにデプロイする仕組みを構築します。

トラブルシュート

エラーメッセージの読み解き方

  • スクリプトのエラー: set -euo pipefail により、エラー発生時にスクリプトは即座に終了し、どのコマンドでエラーが発生したかがログに出力されます。例: curl: (6) Could not resolve host: api.example.com

  • jqのエラー: jq の構文エラーは詳細なメッセージを出力します。例: jq: error: syntax error, unexpected '}'

  • systemdのエラー: journalctl -u json-processor.servicejournalctl -u json-processor.timer で、サービスやタイマーの起動失敗、実行ユーザーの権限問題、スクリプトの終了コードなどの情報を確認できます。

ログからの原因特定

ログはトラブルシューティングの最も重要な情報源です。

  • スクリプトのログ (/var/log/json_processor/process_json_data.log) で、[INFO] メッセージがどこまで進み、[ERROR] がどこで発生したかを確認します。

  • systemd サービスログで、ExecStart コマンドが正常に実行されたか、終了コードは何かを確認します。

一般的な問題と解決策

  • APIアクセス失敗:

    • 原因: ネットワーク接続の問題、APIサーバーのダウン、ファイアウォールによるブロック、DNS解決失敗、不正なAPIキー、TLS証明書の問題。

    • 解決策: ping, curl を手動実行して接続を確認。--verbose オプションで curl の詳細な出力を確認。/etc/pki/tls/certs/ca-bundle.crt が存在し、最新かを確認。

  • jq処理失敗:

    • 原因: 入力JSONの形式が不正、jq フィルタの構文エラー、予期しないJSONスキーマの変更。

    • 解決策: jq コマンドを簡単なJSON入力で手動で試す。入力JSONファイルを直接確認。APIのドキュメントを確認し、JSONスキーマの変更がないかチェック。

  • 権限エラー:

    • 原因: json_processor ユーザーがログファイルや出力ディレクトリに書き込み権限がない。スクリプトファイル自体に実行権限がない。

    • 解決策: ls -l でファイルやディレクトリの権限を確認し、chown, chmod で修正する。

まとめ

jq コマンドを用いたJSONデータ変換スクリプトを curlsystemd で自動化し、DevOps環境での堅牢なデータ処理パイプラインを構築する手法を解説しました。set -euo pipefailtrap を活用した安全なBashスクリプト、TLS検証と再試行を含む curl の利用、そして User=Group= オプションによる権限分離を考慮した systemd Unit/Timerの設計が、システムの安定稼働とセキュリティに不可欠です。

2024年7月29日 (JST) 現在、これらのベストプラクティスは広く利用されており、今後のシステム運用においてもその重要性は変わりません。定期的なログ確認と適切なトラブルシューティングにより、本記事で紹介したシステムは長期にわたって安定した運用が期待できます。


参考文献: [1] jq Developers. “jq Manual”. jqlang.github.io. (参照日: 2024年7月29日). URL: https://jqlang.github.io/jq/manual/ [2] Daniel Stenberg and curl contributors. “curl man page”. curl.se. (参照日: 2024年7月29日). URL: https://curl.se/docs/manpage.html [3] systemd Project. “systemd.unit man page”. freedesktop.org. (参照日: 2024年7月29日). URL: https://www.freedesktop.org/software/systemd/man/systemd.unit.html [4] systemd Project. “systemd.timer man page”. freedesktop.org. (参照日: 2024年7月29日). URL: https://www.freedesktop.org/software/systemd/man/systemd.timer.html [5] Chet Ramey, Brian Fox. “GNU Bash Reference Manual”. gnu.org. (参照日: 2024年7月29日). URL: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html [6] Red Hat, Inc. “Red Hat Enterprise Linux Security Hardening Guide”. access.redhat.com. (参照日: 2024年7月29日). URL: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/security_hardening/index

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

コメント

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