<p><!--META
{
"title": "jqコマンドによるJSONデータ高度処理とDevOps自動化",
"primary_category": "DevOps",
"secondary_categories": ["Linux", "Bash Scripting"],
"tags": ["jq", "curl", "systemd", "JSON", "Automation", "Bash", "DevOps"],
"summary": "DevOpsエンジニア向けに、jq、curl、systemdを組み合わせたJSONデータ高度処理の自動化手法を解説。安全なBashスクリプトと権限分離も詳述。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"jq, curl, systemdを連携させ、DevOpsでJSONデータ処理を自動化する高度な方法を解説!安全なBashスクリプト、システム権限分離、堅牢なAPI連携まで網羅。#jq
#curl #systemd #DevOps","hashtags":["#jq","#curl","#systemd","#DevOps"]},
"link_hints": ["https://jqlang.github.io/jq/manual/", "https://curl.se/docs/manpage.html", "https://www.freedesktop.org/software/systemd/man/systemd.service.html", "https://curl.se/changes.html"]
}
-->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">jqコマンドによるJSONデータ高度処理とDevOps自動化</h1>
<p>DevOps環境におけるデータ処理は、自動化と堅牢性が求められます。本記事では、<code>jq</code> コマンドを核として、<code>curl</code> によるAPI連携、<code>systemd</code> による定期実行を組み合わせ、JSONデータの高度な処理とDevOpsの自動化を実現する手法について解説します。安全なBashスクリプトの書き方、権限分離、トラブルシューティングまでを網羅し、実運用に耐えうるシステム構築を目指します。</p>
<h2 class="wp-block-heading">1. 要件と前提</h2>
<p>本記事で解説するシステムは、以下の環境とツールを前提とします。</p>
<ul class="wp-block-list">
<li><p><strong>OS</strong>: Linux (systemdが利用可能なディストリビューション、例: CentOS, Ubuntu, Debian)</p></li>
<li><p><strong>シェル</strong>: Bash 4.x 以降</p></li>
<li><p><strong><code>jq</code></strong>: JSON処理ツール (バージョン1.6以上を推奨)</p></li>
<li><p><strong><code>curl</code></strong>: データ転送ツール (バージョン7.x以上、特に <code>--json</code> オプションを利用する場合は 7.82.0 以降。<code>curl 7.82.0</code> は2022年03月30日にリリースされました [1])</p></li>
<li><p><strong><code>systemd</code></strong>: システムおよびサービスマネージャー</p></li>
<li><p><strong>実行ユーザー</strong>: 非特権ユーザーで実行することを基本とし、必要に応じて <code>sudo</code> を利用。</p></li>
</ul>
<h2 class="wp-block-heading">2. 実装</h2>
<h3 class="wp-block-heading">2.1. 全体の処理フロー</h3>
<p>以下のフローは、<code>systemd</code> タイマーによって定期的に起動されるサービスが、Bashスクリプトを実行し、<code>curl</code> で外部APIからJSONデータを取得、そのデータを <code>jq</code> で処理する一連の流れを示しています。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["systemd Timer"] --> |定期実行| B["systemd Service Unit"];
B --> |起動| C{"Bash Script"};
C --> |APIリクエスト| D("外部API");
D --> |JSON応答| C;
C --> |jqで高度処理| E["整形・変換済みJSONデータ"];
E --> |ログ/DB/ファイルへ出力| F["データストア"];
</pre></div>
<h3 class="wp-block-heading">2.2. 安全で冪等なBashスクリプトフレームワーク</h3>
<p>まず、DevOps環境で安全かつ冪等な処理を実現するためのBashスクリプトの基本構造を定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# スクリプト名: process_data.sh
# set -euo pipefail:
# -e: コマンドが失敗した場合、スクリプトを即座に終了
# -u: 未定義の変数を参照した場合、スクリプトを終了
# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、その失敗をパイプライン全体の終了ステータスとする
set -euo pipefail
# 処理対象のデータ取得・処理を行う関数
main() {
# 一時ディレクトリの作成
# mktemp -d: 一意な一時ディレクトリを作成。これにより、複数実行時の競合を防止し冪等性を高める。
local tmpdir
tmpdir=$(mktemp -d -t data_process_XXXXXX)
# trap: スクリプト終了時に一時ディレクトリを確実に削除
# EXITシグナルは、スクリプトが正常終了またはエラー終了した場合のいずれでも発生
trap 'rm -rf "$tmpdir"' EXIT
# 変数の定義 (読み取り専用を推奨)
readonly API_ENDPOINT="https://api.example.com/data"
readonly API_KEY="your_api_key_here" # 本番では環境変数やシークレット管理ツールを使用
readonly OUTPUT_DIR="/var/log/my_app"
readonly LOG_FILE="$OUTPUT_DIR/process_data-$(date +%Y%m%d).log"
mkdir -p "$OUTPUT_DIR" # 出力ディレクトリが存在しない場合は作成
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト開始" | tee -a "$LOG_FILE"
# curlを用いた堅牢なAPI呼び出し
local json_response
json_response=$(fetch_data) # fetch_data関数でデータを取得
# jqを用いた高度なJSON処理
local processed_data
processed_data=$(process_json "$json_response") # process_json関数でデータを処理
# 処理結果の保存
echo "$processed_data" | tee -a "$LOG_FILE"
echo "$processed_data" > "$tmpdir/output_$(date +%Y%m%d%H%M%S).json"
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト終了" | tee -a "$LOG_FILE"
}
# curlで外部APIからデータを取得する関数
# エラーハンドリング、リトライ、TLS設定を含む
fetch_data() {
local max_retries=5
local retry_delay_sec=5
local connect_timeout_sec=10
local max_time_sec=30
local http_code
local response_body
local api_url="$API_ENDPOINT/metrics" # 例: メトリクス取得API
for ((i=1; i<=max_retries; i++)); do
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し試行 $i/$max_retries" | tee -a "$LOG_FILE" >&2
# --fail-with-body: HTTPエラー時でもレスポンスボディを表示
# --location: リダイレクトを自動でフォロー
# --cacert: 証明書の検証 (本番環境では適切に設定)
# --retry, --retry-delay, --retry-max-time: ネットワーク一時障害への対応
# --connect-timeout, --max-time: タイムアウト設定
# -H "Authorization: Bearer $API_KEY": 認証ヘッダーの例
response_body=$(
curl -sS \
--fail-with-body \
--location \
--cacert /etc/ssl/certs/ca-certificates.crt \
--retry "$max_retries" \
--retry-delay "$retry_delay_sec" \
--retry-max-time "$((max_retries * retry_delay_sec * 2))" \
--connect-timeout "$connect_timeout_sec" \
--max-time "$max_time_sec" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
"$api_url" 2>&1
)
http_code=$? # curlの終了コードを取得
if [[ $http_code -eq 0 ]]; then
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し成功" | tee -a "$LOG_FILE" >&2
echo "$response_body"
return 0
else
echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: API呼び出し失敗 (exit code: $http_code)" | tee -a "$LOG_FILE" >&2
echo "$response_body" | tee -a "$LOG_FILE" >&2
if [[ $i -lt $max_retries ]]; then
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: $retry_delay_sec秒後にリトライ..." | tee -a "$LOG_FILE" >&2
sleep "$retry_delay_sec"
fi
fi
done
echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: 最大リトライ回数を超過。API呼び出しに失敗しました。" | tee -a "$LOG_FILE" >&2
exit 1 # スクリプトをエラー終了させる
}
# jqでJSONデータを処理する関数
# 複数の処理ステップをパイプで繋ぐ例
process_json() {
local json_input="$1"
# jqの具体的な処理例:
# 1. 配列内の各オブジェクトから 'id' と 'status' を抽出し、'timestamp' を追加
# 2. 'status' が "active" の要素のみをフィルタリング
# 3. 'value' フィールドの合計値を計算し、新しいJSONオブジェクトとして出力
# 4. 'metadata' を削除し、'processed_at' を追加
echo "$json_input" | jq -c '
[
.data.items[] | {
id: .id,
status: .status,
value: .value,
timestamp: (.timestamp | fromdateiso8601) # ISO 8601文字列をUNIXタイムスタンプに変換
} | select(.status == "active") # "active"なアイテムのみを抽出
]
| {
total_active_items: length,
sum_of_values: (map(.value) | add), # activeアイテムのvalueの合計
processed_at: (now | todateiso8601),
items: . # フィルタリング・変換後のアイテムリスト
}
# この例では、APIレスポンス全体ではなく、必要なデータ構造のみを抽出・変換しています。
# 必要に応じて、別の jq 処理を追加することも可能です。
'
# 計算量: N個の要素を持つ配列処理の場合、O(N)
# メモリ条件: 入力JSONのサイズと、変換後のJSONサイズに依存。大規模なJSONでは注意が必要。
}
# スクリプト実行
main "$@"
</pre>
</div>
<p><strong>解説:</strong></p>
<ul class="wp-block-list">
<li><p><strong><code>set -euo pipefail</code></strong>: スクリプトの堅牢性を高めるための基本的な設定です。</p></li>
<li><p><strong><code>mktemp -d</code> と <code>trap</code></strong>: 一時ファイルを安全に管理し、スクリプト終了時にクリーンアップを保証します。冪等性のためにも重要です。</p></li>
<li><p><strong><code>readonly</code></strong>: 重要な変数を誤って変更しないように保護します。</p></li>
<li><p><strong><code>curl</code> オプション</strong>:</p>
<ul>
<li><p><code>--fail-with-body</code>: エラー時にボディを出力し、デバッグを容易にします。</p></li>
<li><p><code>--retry</code> <code>--retry-delay</code> <code>--retry-max-time</code>: 一時的なネットワーク障害やAPIの過負荷に対応するためのリトライ戦略を設定します。</p></li>
<li><p><code>--connect-timeout</code> <code>--max-time</code>: 接続および全体のタイムアウトを設定し、ハングアップを防止します。</p></li>
<li><p><code>--cacert</code>: TLS証明書の検証パスを指定し、セキュアな通信を確保します。</p></li>
<li><p><code>-H "Content-Type: application/json"</code>: JSONデータを送信する場合に必須です。<code>curl 7.82.0</code> 以降では <code>--json</code> オプションも利用できますが、ここでは広く互換性のある <code>-H</code> を使用しています。</p></li>
</ul></li>
<li><p><strong><code>jq</code> 処理</strong>:</p>
<ul>
<li><p><code>[ ... ] | map(...) | select(...)</code>: 配列内のオブジェクトを変換し、特定の条件でフィルタリングする高度なパターンです。</p></li>
<li><p><code>fromdateiso8601</code>, <code>now | todateiso8601</code>: 日付時刻の変換例です。</p></li>
<li><p><code>map(.value) | add</code>: 配列内の特定フィールドの合計値を計算します。</p></li>
<li><p><code>length</code>, <code>add</code>: <code>jq</code> の組み込み関数で、配列の要素数や数値の合計を算出します。</p></li>
</ul></li>
</ul>
<h3 class="wp-block-heading">2.3. systemd unitとtimerの設定</h3>
<p>このBashスクリプトを定期的に実行するため、<code>systemd</code> のサービスユニットとタイマーユニットを設定します。</p>
<h4 class="wp-block-heading">2.3.1. サービスユニット (<code>my-app-processor.service</code>)</h4>
<p><code>/etc/systemd/system/my-app-processor.service</code> を作成します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=My Application Data Processor
Documentation=https://example.com/docs/my-app-processor
After=network.target
[Service]
# Root権限の扱いと権限分離:
# ExecStartはroot権限で実行されますが、User/Groupディレクティブで非特権ユーザーに切り替えます。
# これにより、スクリプト自体は必要最小限の権限で実行され、セキュリティリスクを低減します。
User=myuser # 実行ユーザー (root以外の既存ユーザーを指定)
Group=myuser # 実行グループ
WorkingDirectory=/opt/my_app # スクリプトの作業ディレクトリ
ExecStart=/bin/bash /opt/my_app/process_data.sh # 実行するスクリプトのフルパス
# StandardOutput/StandardError: スクリプトの出力をsystemd journalに送る
StandardOutput=journal
StandardError=journal
# Restart: サービスが異常終了した場合の再起動ポリシー
Restart=on-failure
# RestartSec: 再起動を試みるまでの待機時間
RestartSec=5s
[Install]
WantedBy=multi-user.target # 通常はtimerユニットから呼び出されるため直接enableはしない
</pre>
</div>
<p><strong>注意点:</strong></p>
<ul class="wp-block-list">
<li><p><code>User</code> と <code>Group</code> には、スクリプトの実行に必要な権限のみを持つ<strong>非特権ユーザー</strong>を指定します。このユーザーは、<code>OUTPUT_DIR</code> (<code>/var/log/my_app</code>) への書き込み権限を持つ必要があります。</p></li>
<li><p><code>ExecStart</code> のパスはスクリプトの実際の配置場所に合わせてください。</p></li>
</ul>
<h4 class="wp-block-heading">2.3.2. タイマーユニット (<code>my-app-processor.timer</code>)</h4>
<p><code>/etc/systemd/system/my-app-processor.timer</code> を作成します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run My Application Data Processor every 10 minutes
Requires=my-app-processor.service # このタイマーはサービスに依存
Documentation=https://example.com/docs/my-app-processor
[Timer]
# OnCalendar: 定期実行スケジュール (例: 10分ごと)
# 具体的な実行例: OnCalendar=*-*-* *:00/10:00 (毎時00, 10, 20, 30, 40, 50分に実行)
OnCalendar=*:0/10:00
# AccuracySec: 実行時間の精度 (例: 1分以内に起動)
AccuracySec=1min
# Persistent: サービス停止中に起動タイミングを逃した場合、次回起動時に即時実行するか
# (例: サーバー再起動後に、停止中にスキップされた実行があればすぐ実行)
Persistent=true
[Install]
WantedBy=timers.target
</pre>
</div>
<p><strong>解説:</strong></p>
<ul class="wp-block-list">
<li><p><code>OnCalendar</code>: <code>systemd</code> タイマーの核となる部分で、柔軟なスケジュール設定が可能です。ここでは10分ごとに実行するよう設定しています。</p></li>
<li><p><code>Requires=my-app-processor.service</code>: このタイマーは対応するサービスユニットに依存します。</p></li>
</ul>
<h2 class="wp-block-heading">3. 検証</h2>
<h3 class="wp-block-heading">3.1. スクリプト単体での検証</h3>
<p>スクリプトを直接実行し、意図した通りに動作するか確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># スクリプトに実行権限を付与
chmod +x /opt/my_app/process_data.sh
# スクリプトを直接実行
/opt/my_app/process_data.sh
# ログファイルと出力ファイルの確認
ls -l /var/log/my_app/
cat /var/log/my_app/process_data-$(date +%Y%m%d).log
</pre>
</div>
<h3 class="wp-block-heading">3.2. systemdサービスの起動とログ確認</h3>
<p><code>systemd</code> ユニットをリロードし、サービスを直接起動して動作を確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># systemdに新しいユニットファイルを認識させる
sudo systemctl daemon-reload
# サービスを手動で開始
sudo systemctl start my-app-processor.service
# サービスのステータス確認
sudo systemctl status my-app-processor.service
# サービスログの確認 (journalctl)
# -u: ユニット指定, -f: フォローモード, --since "N minutes ago": 過去N分間のログ
sudo journalctl -u my-app-processor.service -f
</pre>
</div>
<p>ログにスクリプトの出力(<code>INFO: スクリプト開始</code>など)が表示され、エラーがないことを確認します。</p>
<h2 class="wp-block-heading">4. 運用</h2>
<h3 class="wp-block-heading">4.1. systemdサービスの有効化と無効化</h3>
<p>タイマーを有効化することで、定期実行が開始されます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># タイマーを有効化 (OS起動時に自動でタイマーが起動するようになる)
sudo systemctl enable my-app-processor.timer
# タイマーを起動 (即座にタイマーが開始される)
sudo systemctl start my-app-processor.timer
# タイマーのステータス確認
sudo systemctl status my-app-processor.timer
# 定期実行を停止する場合
sudo systemctl stop my-app-processor.timer
sudo systemctl disable my-app-processor.timer
</pre>
</div>
<p>タイマーを <code>enable</code> した後、システムを再起動してもタイマーが自動的に起動し、スケジュール通りにサービスが実行されることを確認することをお勧めします。</p>
<h3 class="wp-block-heading">4.2. ログ監視</h3>
<p><code>journalctl</code> を利用して、定期的にサービスログを監視します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 直近のログを表示
sudo journalctl -u my-app-processor.service --since "1 day ago"
# エラーのみをフィルタリング
sudo journalctl -u my-app-processor.service -p err
# リアルタイムでログを監視
sudo journalctl -u my-app-processor.service -f
</pre>
</div>
<h3 class="wp-block-heading">4.3. 設定変更時の手順</h3>
<p>スクリプトや<code>systemd</code>ユニットファイルを変更した場合は、以下の手順で適用します。</p>
<ol class="wp-block-list">
<li><p><strong>スクリプト変更</strong>: <code>process_data.sh</code> を修正。</p></li>
<li><p><strong>サービス/タイマーユニット変更</strong>: <code>/etc/systemd/system/*.service</code> や <code>*.timer</code> を修正。</p></li>
<li><p><strong><code>systemd</code>デーモンのリロード</strong>: <code>sudo systemctl daemon-reload</code></p></li>
<li><p><strong>タイマーの再起動</strong>: <code>sudo systemctl restart my-app-processor.timer</code> (サービスはタイマーから起動されるため、タイマーを再起動すれば良い)</p></li>
</ol>
<h2 class="wp-block-heading">5. トラブルシュート</h2>
<h3 class="wp-block-heading">5.1. スクリプトのエラーハンドリング</h3>
<ul class="wp-block-list">
<li><p><strong><code>set -euo pipefail</code></strong>: これにより、多くのエラーが即座に捕捉され、スクリプトが異常終了します。終了ステータスやログを確認して原因を特定します。</p></li>
<li><p><strong><code>curl</code> エラー</strong>: <code>fetch_data</code> 関数内で <code>curl</code> の終了コード (<code>$?</code>) を確認し、エラーメッセージをログに出力しています。</p>
<ul>
<li><p>ネットワークの問題: <code>curl: (6) Could not resolve host</code>, <code>(7) Failed to connect</code></p></li>
<li><p>API認証エラー: HTTP 401/403 (レスポンスボディを確認)</p></li>
<li><p>APIサーバーエラー: HTTP 5xx (レスポンスボディを確認)</p></li>
</ul></li>
<li><p><strong><code>jq</code> パースエラー</strong>: <code>jq</code> は不正なJSON入力に対してエラーを出力し、スクリプトが中断されます。<code>json_input</code> の内容が正しいJSON形式であるか確認してください。</p></li>
</ul>
<h3 class="wp-block-heading">5.2. systemdユニットのステータスとログ</h3>
<ul class="wp-block-list">
<li><p><strong>サービスが起動しない</strong>:</p>
<ul>
<li><p><code>sudo systemctl status my-app-processor.service</code> で <code>Active: failed</code> などになっていないか確認。</p></li>
<li><p><code>sudo journalctl -u my-app-processor.service -f</code> でエラーログを確認。<code>ExecStart</code> パス間違い、スクリプトの実行権限不足、依存サービスの未起動などが考えられます。</p></li>
</ul></li>
<li><p><strong>タイマーが動作しない</strong>:</p>
<ul>
<li><p><code>sudo systemctl status my-app-processor.timer</code> で <code>Active: active</code> かつ <code>Next: ...</code> が期待通りか確認。</p></li>
<li><p><code>sudo systemctl list-timers --all</code> で全てのタイマーとその状態を確認。</p></li>
<li><p>タイマーユニットの <code>OnCalendar</code> 設定が正しいか再確認。</p></li>
</ul></li>
</ul>
<h3 class="wp-block-heading">5.3. 権限の問題</h3>
<ul class="wp-block-list">
<li><p>スクリプトが書き込みを試みるディレクトリ (<code>/var/log/my_app/</code>, <code>/tmp</code>) に対して、<code>systemd</code> サービスで指定された <code>User</code> が書き込み権限を持っているか確認します。</p>
<ul>
<li>例: <code>sudo -u myuser test -w /var/log/my_app/ && echo "Writable" || echo "Not Writable"</code></li>
</ul></li>
<li><p>APIキーや秘密情報へのアクセス権限も確認が必要です。</p></li>
</ul>
<h2 class="wp-block-heading">6. まとめ</h2>
<p>、<code>jq</code> を用いたJSONデータの高度処理を核とし、<code>curl</code> による堅牢なAPI連携、そして <code>systemd</code> による確実な定期実行を組み合わせたDevOps自動化のフレームワークを提示しました。<code>set -euo pipefail</code> や <code>trap</code> を活用した安全なBashスクリプト、一時ディレクトリの適切な利用、そして <code>systemd</code> の <code>User</code> ディレクティブによる権限分離は、運用負荷を軽減し、システムの安定性とセキュリティを向上させるために不可欠です。</p>
<p>DevOpsエンジニアは、これらのツールと原則を組み合わせることで、複雑なデータ処理パイプラインを構築し、日々の運用作業を効率的に自動化することが可能です。本記事で紹介した内容は、様々なAPI連携やデータ変換の基盤として応用できるでしょう。</p>
<hr/>
<p>[1] curl. (2022-03-30). <em>curl 7.82.0</em>. Retrieved from https://curl.se/changes.html</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
jqコマンドによるJSONデータ高度処理とDevOps自動化
DevOps環境におけるデータ処理は、自動化と堅牢性が求められます。本記事では、jq コマンドを核として、curl によるAPI連携、systemd による定期実行を組み合わせ、JSONデータの高度な処理とDevOpsの自動化を実現する手法について解説します。安全なBashスクリプトの書き方、権限分離、トラブルシューティングまでを網羅し、実運用に耐えうるシステム構築を目指します。
1. 要件と前提
本記事で解説するシステムは、以下の環境とツールを前提とします。
OS: Linux (systemdが利用可能なディストリビューション、例: CentOS, Ubuntu, Debian)
シェル: Bash 4.x 以降
jq: JSON処理ツール (バージョン1.6以上を推奨)
curl: データ転送ツール (バージョン7.x以上、特に --json オプションを利用する場合は 7.82.0 以降。curl 7.82.0 は2022年03月30日にリリースされました [1])
systemd: システムおよびサービスマネージャー
実行ユーザー: 非特権ユーザーで実行することを基本とし、必要に応じて sudo を利用。
2. 実装
2.1. 全体の処理フロー
以下のフローは、systemd タイマーによって定期的に起動されるサービスが、Bashスクリプトを実行し、curl で外部APIからJSONデータを取得、そのデータを jq で処理する一連の流れを示しています。
graph TD
A["systemd Timer"] --> |定期実行| B["systemd Service Unit"];
B --> |起動| C{"Bash Script"};
C --> |APIリクエスト| D("外部API");
D --> |JSON応答| C;
C --> |jqで高度処理| E["整形・変換済みJSONデータ"];
E --> |ログ/DB/ファイルへ出力| F["データストア"];
2.2. 安全で冪等なBashスクリプトフレームワーク
まず、DevOps環境で安全かつ冪等な処理を実現するためのBashスクリプトの基本構造を定義します。
#!/bin/bash
# スクリプト名: process_data.sh
# set -euo pipefail:
# -e: コマンドが失敗した場合、スクリプトを即座に終了
# -u: 未定義の変数を参照した場合、スクリプトを終了
# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、その失敗をパイプライン全体の終了ステータスとする
set -euo pipefail
# 処理対象のデータ取得・処理を行う関数
main() {
# 一時ディレクトリの作成
# mktemp -d: 一意な一時ディレクトリを作成。これにより、複数実行時の競合を防止し冪等性を高める。
local tmpdir
tmpdir=$(mktemp -d -t data_process_XXXXXX)
# trap: スクリプト終了時に一時ディレクトリを確実に削除
# EXITシグナルは、スクリプトが正常終了またはエラー終了した場合のいずれでも発生
trap 'rm -rf "$tmpdir"' EXIT
# 変数の定義 (読み取り専用を推奨)
readonly API_ENDPOINT="https://api.example.com/data"
readonly API_KEY="your_api_key_here" # 本番では環境変数やシークレット管理ツールを使用
readonly OUTPUT_DIR="/var/log/my_app"
readonly LOG_FILE="$OUTPUT_DIR/process_data-$(date +%Y%m%d).log"
mkdir -p "$OUTPUT_DIR" # 出力ディレクトリが存在しない場合は作成
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト開始" | tee -a "$LOG_FILE"
# curlを用いた堅牢なAPI呼び出し
local json_response
json_response=$(fetch_data) # fetch_data関数でデータを取得
# jqを用いた高度なJSON処理
local processed_data
processed_data=$(process_json "$json_response") # process_json関数でデータを処理
# 処理結果の保存
echo "$processed_data" | tee -a "$LOG_FILE"
echo "$processed_data" > "$tmpdir/output_$(date +%Y%m%d%H%M%S).json"
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: スクリプト終了" | tee -a "$LOG_FILE"
}
# curlで外部APIからデータを取得する関数
# エラーハンドリング、リトライ、TLS設定を含む
fetch_data() {
local max_retries=5
local retry_delay_sec=5
local connect_timeout_sec=10
local max_time_sec=30
local http_code
local response_body
local api_url="$API_ENDPOINT/metrics" # 例: メトリクス取得API
for ((i=1; i<=max_retries; i++)); do
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し試行 $i/$max_retries" | tee -a "$LOG_FILE" >&2
# --fail-with-body: HTTPエラー時でもレスポンスボディを表示
# --location: リダイレクトを自動でフォロー
# --cacert: 証明書の検証 (本番環境では適切に設定)
# --retry, --retry-delay, --retry-max-time: ネットワーク一時障害への対応
# --connect-timeout, --max-time: タイムアウト設定
# -H "Authorization: Bearer $API_KEY": 認証ヘッダーの例
response_body=$(
curl -sS \
--fail-with-body \
--location \
--cacert /etc/ssl/certs/ca-certificates.crt \
--retry "$max_retries" \
--retry-delay "$retry_delay_sec" \
--retry-max-time "$((max_retries * retry_delay_sec * 2))" \
--connect-timeout "$connect_timeout_sec" \
--max-time "$max_time_sec" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
"$api_url" 2>&1
)
http_code=$? # curlの終了コードを取得
if [[ $http_code -eq 0 ]]; then
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: API呼び出し成功" | tee -a "$LOG_FILE" >&2
echo "$response_body"
return 0
else
echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: API呼び出し失敗 (exit code: $http_code)" | tee -a "$LOG_FILE" >&2
echo "$response_body" | tee -a "$LOG_FILE" >&2
if [[ $i -lt $max_retries ]]; then
echo "$(date +%Y/%m/%d_%H:%M:%S) - INFO: $retry_delay_sec秒後にリトライ..." | tee -a "$LOG_FILE" >&2
sleep "$retry_delay_sec"
fi
fi
done
echo "$(date +%Y/%m/%d_%H:%M:%S) - ERROR: 最大リトライ回数を超過。API呼び出しに失敗しました。" | tee -a "$LOG_FILE" >&2
exit 1 # スクリプトをエラー終了させる
}
# jqでJSONデータを処理する関数
# 複数の処理ステップをパイプで繋ぐ例
process_json() {
local json_input="$1"
# jqの具体的な処理例:
# 1. 配列内の各オブジェクトから 'id' と 'status' を抽出し、'timestamp' を追加
# 2. 'status' が "active" の要素のみをフィルタリング
# 3. 'value' フィールドの合計値を計算し、新しいJSONオブジェクトとして出力
# 4. 'metadata' を削除し、'processed_at' を追加
echo "$json_input" | jq -c '
[
.data.items[] | {
id: .id,
status: .status,
value: .value,
timestamp: (.timestamp | fromdateiso8601) # ISO 8601文字列をUNIXタイムスタンプに変換
} | select(.status == "active") # "active"なアイテムのみを抽出
]
| {
total_active_items: length,
sum_of_values: (map(.value) | add), # activeアイテムのvalueの合計
processed_at: (now | todateiso8601),
items: . # フィルタリング・変換後のアイテムリスト
}
# この例では、APIレスポンス全体ではなく、必要なデータ構造のみを抽出・変換しています。
# 必要に応じて、別の jq 処理を追加することも可能です。
'
# 計算量: N個の要素を持つ配列処理の場合、O(N)
# メモリ条件: 入力JSONのサイズと、変換後のJSONサイズに依存。大規模なJSONでは注意が必要。
}
# スクリプト実行
main "$@"
解説:
set -euo pipefail: スクリプトの堅牢性を高めるための基本的な設定です。
mktemp -d と trap: 一時ファイルを安全に管理し、スクリプト終了時にクリーンアップを保証します。冪等性のためにも重要です。
readonly: 重要な変数を誤って変更しないように保護します。
curl オプション:
--fail-with-body: エラー時にボディを出力し、デバッグを容易にします。
--retry --retry-delay --retry-max-time: 一時的なネットワーク障害やAPIの過負荷に対応するためのリトライ戦略を設定します。
--connect-timeout --max-time: 接続および全体のタイムアウトを設定し、ハングアップを防止します。
--cacert: TLS証明書の検証パスを指定し、セキュアな通信を確保します。
-H "Content-Type: application/json": JSONデータを送信する場合に必須です。curl 7.82.0 以降では --json オプションも利用できますが、ここでは広く互換性のある -H を使用しています。
jq 処理:
[ ... ] | map(...) | select(...): 配列内のオブジェクトを変換し、特定の条件でフィルタリングする高度なパターンです。
fromdateiso8601, now | todateiso8601: 日付時刻の変換例です。
map(.value) | add: 配列内の特定フィールドの合計値を計算します。
length, add: jq の組み込み関数で、配列の要素数や数値の合計を算出します。
2.3. systemd unitとtimerの設定
このBashスクリプトを定期的に実行するため、systemd のサービスユニットとタイマーユニットを設定します。
2.3.1. サービスユニット (my-app-processor.service)
/etc/systemd/system/my-app-processor.service を作成します。
[Unit]
Description=My Application Data Processor
Documentation=https://example.com/docs/my-app-processor
After=network.target
[Service]
# Root権限の扱いと権限分離:
# ExecStartはroot権限で実行されますが、User/Groupディレクティブで非特権ユーザーに切り替えます。
# これにより、スクリプト自体は必要最小限の権限で実行され、セキュリティリスクを低減します。
User=myuser # 実行ユーザー (root以外の既存ユーザーを指定)
Group=myuser # 実行グループ
WorkingDirectory=/opt/my_app # スクリプトの作業ディレクトリ
ExecStart=/bin/bash /opt/my_app/process_data.sh # 実行するスクリプトのフルパス
# StandardOutput/StandardError: スクリプトの出力をsystemd journalに送る
StandardOutput=journal
StandardError=journal
# Restart: サービスが異常終了した場合の再起動ポリシー
Restart=on-failure
# RestartSec: 再起動を試みるまでの待機時間
RestartSec=5s
[Install]
WantedBy=multi-user.target # 通常はtimerユニットから呼び出されるため直接enableはしない
注意点:
2.3.2. タイマーユニット (my-app-processor.timer)
/etc/systemd/system/my-app-processor.timer を作成します。
[Unit]
Description=Run My Application Data Processor every 10 minutes
Requires=my-app-processor.service # このタイマーはサービスに依存
Documentation=https://example.com/docs/my-app-processor
[Timer]
# OnCalendar: 定期実行スケジュール (例: 10分ごと)
# 具体的な実行例: OnCalendar=*-*-* *:00/10:00 (毎時00, 10, 20, 30, 40, 50分に実行)
OnCalendar=*:0/10:00
# AccuracySec: 実行時間の精度 (例: 1分以内に起動)
AccuracySec=1min
# Persistent: サービス停止中に起動タイミングを逃した場合、次回起動時に即時実行するか
# (例: サーバー再起動後に、停止中にスキップされた実行があればすぐ実行)
Persistent=true
[Install]
WantedBy=timers.target
解説:
3. 検証
3.1. スクリプト単体での検証
スクリプトを直接実行し、意図した通りに動作するか確認します。
# スクリプトに実行権限を付与
chmod +x /opt/my_app/process_data.sh
# スクリプトを直接実行
/opt/my_app/process_data.sh
# ログファイルと出力ファイルの確認
ls -l /var/log/my_app/
cat /var/log/my_app/process_data-$(date +%Y%m%d).log
3.2. systemdサービスの起動とログ確認
systemd ユニットをリロードし、サービスを直接起動して動作を確認します。
# systemdに新しいユニットファイルを認識させる
sudo systemctl daemon-reload
# サービスを手動で開始
sudo systemctl start my-app-processor.service
# サービスのステータス確認
sudo systemctl status my-app-processor.service
# サービスログの確認 (journalctl)
# -u: ユニット指定, -f: フォローモード, --since "N minutes ago": 過去N分間のログ
sudo journalctl -u my-app-processor.service -f
ログにスクリプトの出力(INFO: スクリプト開始など)が表示され、エラーがないことを確認します。
4. 運用
4.1. systemdサービスの有効化と無効化
タイマーを有効化することで、定期実行が開始されます。
# タイマーを有効化 (OS起動時に自動でタイマーが起動するようになる)
sudo systemctl enable my-app-processor.timer
# タイマーを起動 (即座にタイマーが開始される)
sudo systemctl start my-app-processor.timer
# タイマーのステータス確認
sudo systemctl status my-app-processor.timer
# 定期実行を停止する場合
sudo systemctl stop my-app-processor.timer
sudo systemctl disable my-app-processor.timer
タイマーを enable した後、システムを再起動してもタイマーが自動的に起動し、スケジュール通りにサービスが実行されることを確認することをお勧めします。
4.2. ログ監視
journalctl を利用して、定期的にサービスログを監視します。
# 直近のログを表示
sudo journalctl -u my-app-processor.service --since "1 day ago"
# エラーのみをフィルタリング
sudo journalctl -u my-app-processor.service -p err
# リアルタイムでログを監視
sudo journalctl -u my-app-processor.service -f
4.3. 設定変更時の手順
スクリプトやsystemdユニットファイルを変更した場合は、以下の手順で適用します。
スクリプト変更: process_data.sh を修正。
サービス/タイマーユニット変更: /etc/systemd/system/*.service や *.timer を修正。
systemdデーモンのリロード: sudo systemctl daemon-reload
タイマーの再起動: sudo systemctl restart my-app-processor.timer (サービスはタイマーから起動されるため、タイマーを再起動すれば良い)
5. トラブルシュート
5.1. スクリプトのエラーハンドリング
set -euo pipefail: これにより、多くのエラーが即座に捕捉され、スクリプトが異常終了します。終了ステータスやログを確認して原因を特定します。
curl エラー: fetch_data 関数内で curl の終了コード ($?) を確認し、エラーメッセージをログに出力しています。
ネットワークの問題: curl: (6) Could not resolve host, (7) Failed to connect
API認証エラー: HTTP 401/403 (レスポンスボディを確認)
APIサーバーエラー: HTTP 5xx (レスポンスボディを確認)
jq パースエラー: jq は不正なJSON入力に対してエラーを出力し、スクリプトが中断されます。json_input の内容が正しいJSON形式であるか確認してください。
5.2. systemdユニットのステータスとログ
サービスが起動しない:
タイマーが動作しない:
sudo systemctl status my-app-processor.timer で Active: active かつ Next: ... が期待通りか確認。
sudo systemctl list-timers --all で全てのタイマーとその状態を確認。
タイマーユニットの OnCalendar 設定が正しいか再確認。
5.3. 権限の問題
スクリプトが書き込みを試みるディレクトリ (/var/log/my_app/, /tmp) に対して、systemd サービスで指定された User が書き込み権限を持っているか確認します。
- 例:
sudo -u myuser test -w /var/log/my_app/ && echo "Writable" || echo "Not Writable"
APIキーや秘密情報へのアクセス権限も確認が必要です。
6. まとめ
、jq を用いたJSONデータの高度処理を核とし、curl による堅牢なAPI連携、そして systemd による確実な定期実行を組み合わせたDevOps自動化のフレームワークを提示しました。set -euo pipefail や trap を活用した安全なBashスクリプト、一時ディレクトリの適切な利用、そして systemd の User ディレクティブによる権限分離は、運用負荷を軽減し、システムの安定性とセキュリティを向上させるために不可欠です。
DevOpsエンジニアは、これらのツールと原則を組み合わせることで、複雑なデータ処理パイプラインを構築し、日々の運用作業を効率的に自動化することが可能です。本記事で紹介した内容は、様々なAPI連携やデータ変換の基盤として応用できるでしょう。
[1] curl. (2022-03-30). curl 7.82.0. Retrieved from https://curl.se/changes.html
コメント