jqとcurlで実現する堅牢なJSONデータ処理:DevOpsプラクティス

Mermaid

jqとcurlで実現する堅牢なJSONデータ処理:DevOpsプラクティス

DevOpsの世界では、外部APIからのデータ取得、変換、そしてシステムへの統合が日常的に行われます。本稿では、jqcurlを組み合わせ、JSONデータを安全かつ冪等に抽出し、整形する一連のプロセスを解説します。さらに、systemdを用いた定期実行と、DevOpsエンジニアとして重要な権限分離やトラブルシューティングのプラクティスも示します。

要件と前提

要件

  • GitHub APIからJSONデータを取得し、特定の条件でフィルタリング・整形する。
  • 処理はbashスクリプトとして実装し、冪等性、安全性、エラーハンドリングを考慮する。
  • curlでTLS検証、再試行、バックオフを実装する。
  • jqでJSONデータの抽出、フィルタリング、変換、整形を行う。
  • systemd unit/timerを用いて、この処理を定期実行する。
  • 処理全体をmermaidのフロー図で表現する。
  • root権限の扱いと権限分離に関する注意点を明記する。

前提

  • Linux環境(Ubuntu/CentOSなど)が利用可能であること。
  • curljqsystemdがインストールされていること。
    • sudo apt update && sudo apt install -y curl jq または sudo yum install -y curl jq

実装

外部APIからGo言語のリポジトリ情報を取得し、スター数が5000以上のものを抽出し、CSV形式で整形するスクリプトを作成します。

スクリプトの作成 (fetch_github_repos.sh)

#!/bin/bash
# set -euo pipefail:
#   -e: 終了コードが非ゼロのコマンドがあれば即座に終了
#   -u: 未定義の変数を使おうとするとエラー
#   -o pipefail: パイプライン中のコマンドが一つでも失敗すれば終了コードが非ゼロになる
set -euo pipefail

# 一時ディレクトリの作成と自動削除
# mktemp -d: 安全な一時ディレクトリを作成
TMP_DIR=$(mktemp -d -t github_data_XXXXXX)
# trap: スクリプト終了時に一時ディレクトリを削除
trap 'rm -rf "$TMP_DIR"' EXIT

# 出力ファイルの定義
OUTPUT_FILE="/var/lib/github_data/go_popular_repos.csv"
# 出力ディレクトリが存在しなければ作成 (冪等性)
mkdir -p "$(dirname "$OUTPUT_FILE")"

# GitHub APIのURL
API_URL="https://api.github.com/search/repositories?q=language:go&sort=stars&order=desc"

# 権限に関する注意点:
# APIトークンなど機密情報は直接スクリプトに書かず、環境変数やセキュアなストレージから取得する。
# 例: GITHUB_TOKEN="${GITHUB_TOKEN:-}"
# 環境変数を使用する場合、systemd unitファイルでEnvironment=設定する。
# 本例では公開APIのため不要。

echo "--- $(date '+%Y-%m-%d %H:%M:%S') ---"
echo "Fetching data from GitHub API..."

# curlでGitHub APIからJSONデータを取得
# --fail: HTTPステータスコードが200番台以外の場合、即座にエラー終了
# --silent: 進捗表示を抑制
# --show-error: エラー発生時にエラーメッセージを表示
# --retry 5: 失敗時に5回まで再試行
# --retry-delay 5: 5秒間隔で再試行
# --retry-max-time 60: 合計60秒まで再試行
# --location: リダイレクトを追跡
# --compressed: 圧縮をサポート
# --connect-timeout 10: 接続タイムアウト10秒
# --max-time 30: 最大30秒で処理を終了
if ! curl -sS --fail --location --compressed --connect-timeout 10 --max-time 30 \
         --retry 5 --retry-delay 5 --retry-max-time 60 \
         "$API_URL" > "$TMP_DIR/github_repos.json"; then
    echo "ERROR: Failed to fetch data from GitHub API." >&2
    exit 1
fi

echo "Processing JSON data with jq..."

# jqでJSONデータを抽出・変換・整形
# .items[]: 配列の各要素を展開
# select(.stargazers_count > 5000): スター数が5000以上のリポジトリをフィルタリング
# {name: .full_name, stars: .stargazers_count, url: .html_url}: 必要なフィールドを抽出・リネーム
# @csv: CSV形式に整形 (ヘッダ行は含まず)
if ! jq -r '.items[] | select(.stargazers_count > 5000) | {name: .full_name, stars: .stargazers_count, url: .html_url} | @csv' \
         "$TMP_DIR/github_repos.json" > "$TMP_DIR/processed_data.csv"; then
    echo "ERROR: Failed to process JSON data with jq." >&2
    exit 1
fi

# ヘッダ行を追加して最終的なCSVファイルを作成
echo "name,stars,url" > "$OUTPUT_FILE"
cat "$TMP_DIR/processed_data.csv" >> "$OUTPUT_FILE"

echo "Data successfully extracted and saved to $OUTPUT_FILE"
echo "--- End ---"

フロー図

graph TD
    A["systemd Timer starts"] --> B("systemd Service executes script");
    B --> C{"Create Temporary Directory"};
    C --> D{"Fetch JSON from GitHub API via curl"};
    D -- Success --> E{"Process JSON with jq"};
    D -- Failure --> F("Log Error & Exit");
    E -- Success --> G("Add CSV Header & Save to Output File");
    E -- Failure --> F;
    G --> H("Delete Temporary Directory");
    H --> I["Script Ends"];
    F --> I;

検証

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

# スクリプトに実行権限を付与
chmod +x fetch_github_repos.sh

# スクリプトを実行
./fetch_github_repos.sh

# 出力ファイルの内容を確認
cat /var/lib/github_data/go_popular_repos.csv

# 例: 出力は以下のようになるはず (内容は変動します)
# name,stars,url
# gin-gonic/gin,73000,https://github.com/gin-gonic/gin
# go-gorm/gorm,71000,https://github.com/go-gorm/gorm
# ...

エラーケースの確認: – API URLを間違えてみる(例: API_URL="https://api.github.com/invalid_url")。curlがエラーで終了し、スクリプトも停止することを確認します。 – jqのパスを間違えてみる(例: select(.non_existent_field > 5000))。jqがエラーで終了し、スクリプトも停止することを確認します。

運用

systemdUnitTimerを用いて、スクリプトを定期的に実行するように設定します。

権限分離の注意点

systemdサービスは、User=Group=ディレクティブを用いて、root権限ではなく専用の非特権ユーザーで実行することを強く推奨します。これにより、スクリプトが誤動作した場合の影響範囲を最小限に抑え、セキュリティを向上させます。ここでは例としてdevopsuserというユーザーを作成し、そのユーザーで実行します。

# 非特権ユーザーの作成
sudo useradd -r -s /bin/false devopsuser
# 出力ディレクトリの所有者をdevopsuserに変更
sudo mkdir -p /var/lib/github_data
sudo chown devopsuser:devopsuser /var/lib/github_data

systemd Unitファイル (/etc/systemd/system/github-data-fetch.service)

[Unit]
Description=Fetch and process GitHub repository data
After=network.target

[Service]
# スクリプトはoneshotタイプで一度実行したら終了
Type=oneshot
# スクリプトのフルパスを指定
ExecStart=/usr/local/bin/fetch_github_repos.sh
# 実行ユーザーとグループを指定 (重要: 権限分離)
User=devopsuser
Group=devopsuser
# 作業ディレクトリ
WorkingDirectory=/var/lib/github_data
# 標準出力と標準エラーをsyslogに送る
StandardOutput=journal
StandardError=journal

[Install]
# Timerユニットによって呼び出される
WantedBy=timers.target

systemd Timerファイル (/etc/systemd/system/github-data-fetch.timer)

[Unit]
Description=Run GitHub data fetcher daily

[Timer]
# 毎日午前3時に実行
OnCalendar=daily
# Persistent=true: タイマーが停止していた間に実行すべきタイミングがあった場合、起動後すぐに実行
Persistent=true

[Install]
# systemctl enable で有効化されるように
WantedBy=timers.target

配置と起動

  1. スクリプトを適切な場所に配置(例: /usr/local/bin/)。
    sudo mv fetch_github_repos.sh /usr/local/bin/
    sudo chmod +x /usr/local/bin/fetch_github_repos.sh
    
  2. systemdユニットとタイマーファイルを配置。
    # 上記の内容をそれぞれファイルとして保存
    sudo vi /etc/systemd/system/github-data-fetch.service
    sudo vi /etc/systemd/system/github-data-fetch.timer
    
  3. systemd設定をリロード。
    sudo systemctl daemon-reload
    
  4. タイマーを有効化して起動。
    sudo systemctl enable github-data-fetch.timer
    sudo systemctl start github-data-fetch.timer
    
  5. タイマーの状態を確認。
    systemctl list-timers | grep github-data-fetch
    # 例: Mon 2023-10-26 03:00:00 UTC 14h left  github-data-fetch.timer  github-data-fetch.service
    

ログ確認

スクリプトの実行ログはjournalctlで確認できます。

# サービスの実行ログを確認
journalctl -u github-data-fetch.service

# 直近の実行ログ (root権限で実行されていれば不要ですが、念のため)
journalctl -u github-data-fetch.service -f

トラブルシュート

  • スクリプトのエラー: set -euo pipefailにより、エラー発生箇所で即座にスクリプトが停止します。journalctlでエラーメッセージを確認し、スクリプト内の処理を見直してください。trapコマンドにより一時ファイルが適切にクリーンアップされることも確認してください。
  • curlのエラー: ネットワーク接続問題、APIレート制限、APIキーの失効、不正なURLなどが考えられます。journalctlでエラーメッセージを確認するか、スクリプト内でcurl --verboseを追加して詳細なリクエスト/レスポンス情報を確認します。再試行ロジックが有効かどうかも確認ポイントです。
  • jqのエラー: 入力JSONの形式が不正、jqのフィルタリングパスが間違っている、期待するデータが存在しないなどが原因です。journalctljqのエラーメッセージ(例: parse error)を確認し、jqの式を修正します。入力JSONが期待通りか、jq -c .などで出力して確認するのも有効です。
  • systemdのエラー: systemctl status github-data-fetch.servicesystemctl status github-data-fetch.timerで状態を確認します。journalctl -xeu github-data-fetch.serviceで詳細なログとシステムメッセージを合わせて確認することで、権限問題(User=設定とファイルのパーミッションが合っていないなど)やパスの問題(ExecStartのパスが間違っている)を発見できます。

まとめ

本稿では、jqcurlを用いたJSONデータ処理のDevOpsプラクティスを解説しました。set -euo pipefailtrapによる安全なbashスクリプトの書き方、curlの堅牢なデータ取得方法、jqによる柔軟なデータ抽出・変換・整形、そしてsystemdによる自動化と権限分離の重要性を示しました。

これらの技術とプラクティスを組み合わせることで、外部システムからのデータ取得・処理を自動化し、安定性と信頼性の高いDevOpsパイプラインを構築できます。特に、非特権ユーザーでの実行や機密情報の安全な管理といった権限分離は、システムのセキュリティを確保する上で不可欠です。本記事が、日々のDevOps業務におけるJSONデータ処理の一助となれば幸いです。

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

コメント

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