<h1 class="wp-block-heading">jqとcurlで実現する堅牢なJSONデータ処理:DevOpsプラクティス</h1>
<p>DevOpsの世界では、外部APIからのデータ取得、変換、そしてシステムへの統合が日常的に行われます。本稿では、<code>jq</code>と<code>curl</code>を組み合わせ、JSONデータを安全かつ冪等に抽出し、整形する一連のプロセスを解説します。さらに、<code>systemd</code>を用いた定期実行と、DevOpsエンジニアとして重要な権限分離やトラブルシューティングのプラクティスも示します。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<h3 class="wp-block-heading">要件</h3>
<ul class="wp-block-list">
<li>GitHub APIからJSONデータを取得し、特定の条件でフィルタリング・整形する。</li>
<li>処理は<code>bash</code>スクリプトとして実装し、冪等性、安全性、エラーハンドリングを考慮する。</li>
<li><code>curl</code>でTLS検証、再試行、バックオフを実装する。</li>
<li><code>jq</code>でJSONデータの抽出、フィルタリング、変換、整形を行う。</li>
<li><code>systemd unit/timer</code>を用いて、この処理を定期実行する。</li>
<li>処理全体を<code>mermaid</code>のフロー図で表現する。</li>
<li><code>root</code>権限の扱いと権限分離に関する注意点を明記する。</li>
</ul>
<h3 class="wp-block-heading">前提</h3>
<ul class="wp-block-list">
<li>Linux環境(Ubuntu/CentOSなど)が利用可能であること。</li>
<li><code>curl</code>、<code>jq</code>、<code>systemd</code>がインストールされていること。
<ul>
<li><code>sudo apt update && sudo apt install -y curl jq</code> または <code>sudo yum install -y curl jq</code></li>
</ul></li>
</ul>
<h2 class="wp-block-heading">実装</h2>
<p>外部APIからGo言語のリポジトリ情報を取得し、スター数が5000以上のものを抽出し、CSV形式で整形するスクリプトを作成します。</p>
<h3 class="wp-block-heading">スクリプトの作成 (<code>fetch_github_repos.sh</code>)</h3>
<pre data-enlighter-language="generic">#!/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 ---"
</pre>
<h3 class="wp-block-heading">フロー図</h3>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
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;
</pre></div>
<h2 class="wp-block-heading">検証</h2>
<p>スクリプトを直接実行して、期待通りに動作するか確認します。</p>
<pre data-enlighter-language="generic"># スクリプトに実行権限を付与
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
# ...
</pre>
<p>エラーケースの確認:
– API URLを間違えてみる(例: <code>API_URL="https://api.github.com/invalid_url"</code>)。<code>curl</code>がエラーで終了し、スクリプトも停止することを確認します。
– <code>jq</code>のパスを間違えてみる(例: <code>select(.non_existent_field > 5000)</code>)。<code>jq</code>がエラーで終了し、スクリプトも停止することを確認します。</p>
<h2 class="wp-block-heading">運用</h2>
<p><code>systemd</code>の<code>Unit</code>と<code>Timer</code>を用いて、スクリプトを定期的に実行するように設定します。</p>
<h3 class="wp-block-heading">権限分離の注意点</h3>
<p><code>systemd</code>サービスは、<code>User=</code>と<code>Group=</code>ディレクティブを用いて、<code>root</code>権限ではなく専用の非特権ユーザーで実行することを強く推奨します。これにより、スクリプトが誤動作した場合の影響範囲を最小限に抑え、セキュリティを向上させます。ここでは例として<code>devopsuser</code>というユーザーを作成し、そのユーザーで実行します。</p>
<pre data-enlighter-language="generic"># 非特権ユーザーの作成
sudo useradd -r -s /bin/false devopsuser
# 出力ディレクトリの所有者をdevopsuserに変更
sudo mkdir -p /var/lib/github_data
sudo chown devopsuser:devopsuser /var/lib/github_data
</pre>
<h3 class="wp-block-heading">systemd Unitファイル (<code>/etc/systemd/system/github-data-fetch.service</code>)</h3>
<pre data-enlighter-language="generic">[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
</pre>
<h3 class="wp-block-heading">systemd Timerファイル (<code>/etc/systemd/system/github-data-fetch.timer</code>)</h3>
<pre data-enlighter-language="generic">[Unit]
Description=Run GitHub data fetcher daily
[Timer]
# 毎日午前3時に実行
OnCalendar=daily
# Persistent=true: タイマーが停止していた間に実行すべきタイミングがあった場合、起動後すぐに実行
Persistent=true
[Install]
# systemctl enable で有効化されるように
WantedBy=timers.target
</pre>
<h3 class="wp-block-heading">配置と起動</h3>
<ol class="wp-block-list">
<li>スクリプトを適切な場所に配置(例: <code>/usr/local/bin/</code>)。
<pre data-enlighter-language="generic">sudo mv fetch_github_repos.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/fetch_github_repos.sh
</pre></li>
<li><code>systemd</code>ユニットとタイマーファイルを配置。
<pre data-enlighter-language="generic"># 上記の内容をそれぞれファイルとして保存
sudo vi /etc/systemd/system/github-data-fetch.service
sudo vi /etc/systemd/system/github-data-fetch.timer
</pre></li>
<li><code>systemd</code>設定をリロード。
<pre data-enlighter-language="generic">sudo systemctl daemon-reload
</pre></li>
<li>タイマーを有効化して起動。
<pre data-enlighter-language="generic">sudo systemctl enable github-data-fetch.timer
sudo systemctl start github-data-fetch.timer
</pre></li>
<li>タイマーの状態を確認。
<pre data-enlighter-language="generic">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
</pre></li>
</ol>
<h3 class="wp-block-heading">ログ確認</h3>
<p>スクリプトの実行ログは<code>journalctl</code>で確認できます。</p>
<pre data-enlighter-language="generic"># サービスの実行ログを確認
journalctl -u github-data-fetch.service
# 直近の実行ログ (root権限で実行されていれば不要ですが、念のため)
journalctl -u github-data-fetch.service -f
</pre>
<h2 class="wp-block-heading">トラブルシュート</h2>
<ul class="wp-block-list">
<li><strong>スクリプトのエラー</strong>: <code>set -euo pipefail</code>により、エラー発生箇所で即座にスクリプトが停止します。<code>journalctl</code>でエラーメッセージを確認し、スクリプト内の処理を見直してください。<code>trap</code>コマンドにより一時ファイルが適切にクリーンアップされることも確認してください。</li>
<li><strong><code>curl</code>のエラー</strong>: ネットワーク接続問題、APIレート制限、APIキーの失効、不正なURLなどが考えられます。<code>journalctl</code>でエラーメッセージを確認するか、スクリプト内で<code>curl --verbose</code>を追加して詳細なリクエスト/レスポンス情報を確認します。再試行ロジックが有効かどうかも確認ポイントです。</li>
<li><strong><code>jq</code>のエラー</strong>: 入力JSONの形式が不正、<code>jq</code>のフィルタリングパスが間違っている、期待するデータが存在しないなどが原因です。<code>journalctl</code>で<code>jq</code>のエラーメッセージ(例: <code>parse error</code>)を確認し、<code>jq</code>の式を修正します。入力JSONが期待通りか、<code>jq -c .</code>などで出力して確認するのも有効です。</li>
<li><strong><code>systemd</code>のエラー</strong>: <code>systemctl status github-data-fetch.service</code>や<code>systemctl status github-data-fetch.timer</code>で状態を確認します。<code>journalctl -xeu github-data-fetch.service</code>で詳細なログとシステムメッセージを合わせて確認することで、権限問題(<code>User=</code>設定とファイルのパーミッションが合っていないなど)やパスの問題(<code>ExecStart</code>のパスが間違っている)を発見できます。</li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>本稿では、<code>jq</code>と<code>curl</code>を用いたJSONデータ処理のDevOpsプラクティスを解説しました。<code>set -euo pipefail</code>や<code>trap</code>による安全な<code>bash</code>スクリプトの書き方、<code>curl</code>の堅牢なデータ取得方法、<code>jq</code>による柔軟なデータ抽出・変換・整形、そして<code>systemd</code>による自動化と権限分離の重要性を示しました。</p>
<p>これらの技術とプラクティスを組み合わせることで、外部システムからのデータ取得・処理を自動化し、安定性と信頼性の高いDevOpsパイプラインを構築できます。特に、非特権ユーザーでの実行や機密情報の安全な管理といった権限分離は、システムのセキュリティを確保する上で不可欠です。本記事が、日々のDevOps業務におけるJSONデータ処理の一助となれば幸いです。</p>
jqとcurlで実現する堅牢なJSONデータ処理:DevOpsプラクティス
DevOpsの世界では、外部APIからのデータ取得、変換、そしてシステムへの統合が日常的に行われます。本稿では、jq
とcurl
を組み合わせ、JSONデータを安全かつ冪等に抽出し、整形する一連のプロセスを解説します。さらに、systemd
を用いた定期実行と、DevOpsエンジニアとして重要な権限分離やトラブルシューティングのプラクティスも示します。
要件と前提
要件
- GitHub APIからJSONデータを取得し、特定の条件でフィルタリング・整形する。
- 処理は
bash
スクリプトとして実装し、冪等性、安全性、エラーハンドリングを考慮する。
curl
でTLS検証、再試行、バックオフを実装する。
jq
でJSONデータの抽出、フィルタリング、変換、整形を行う。
systemd unit/timer
を用いて、この処理を定期実行する。
- 処理全体を
mermaid
のフロー図で表現する。
root
権限の扱いと権限分離に関する注意点を明記する。
前提
- Linux環境(Ubuntu/CentOSなど)が利用可能であること。
curl
、jq
、systemd
がインストールされていること。
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
がエラーで終了し、スクリプトも停止することを確認します。
運用
systemd
のUnit
とTimer
を用いて、スクリプトを定期的に実行するように設定します。
権限分離の注意点
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
配置と起動
- スクリプトを適切な場所に配置(例:
/usr/local/bin/
)。
sudo mv fetch_github_repos.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/fetch_github_repos.sh
systemd
ユニットとタイマーファイルを配置。
# 上記の内容をそれぞれファイルとして保存
sudo vi /etc/systemd/system/github-data-fetch.service
sudo vi /etc/systemd/system/github-data-fetch.timer
systemd
設定をリロード。
sudo systemctl daemon-reload
- タイマーを有効化して起動。
sudo systemctl enable github-data-fetch.timer
sudo systemctl start github-data-fetch.timer
- タイマーの状態を確認。
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
のフィルタリングパスが間違っている、期待するデータが存在しないなどが原因です。journalctl
でjq
のエラーメッセージ(例: parse error
)を確認し、jq
の式を修正します。入力JSONが期待通りか、jq -c .
などで出力して確認するのも有効です。
systemd
のエラー: systemctl status github-data-fetch.service
やsystemctl status github-data-fetch.timer
で状態を確認します。journalctl -xeu github-data-fetch.service
で詳細なログとシステムメッセージを合わせて確認することで、権限問題(User=
設定とファイルのパーミッションが合っていないなど)やパスの問題(ExecStart
のパスが間違っている)を発見できます。
まとめ
本稿では、jq
とcurl
を用いたJSONデータ処理のDevOpsプラクティスを解説しました。set -euo pipefail
やtrap
による安全なbash
スクリプトの書き方、curl
の堅牢なデータ取得方法、jq
による柔軟なデータ抽出・変換・整形、そしてsystemd
による自動化と権限分離の重要性を示しました。
これらの技術とプラクティスを組み合わせることで、外部システムからのデータ取得・処理を自動化し、安定性と信頼性の高いDevOpsパイプラインを構築できます。特に、非特権ユーザーでの実行や機密情報の安全な管理といった権限分離は、システムのセキュリティを確保する上で不可欠です。本記事が、日々のDevOps業務におけるJSONデータ処理の一助となれば幸いです。
コメント