<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">jqコマンドでJSONを効率処理:安全なAPI連携と定期実行</h1>
<p>DevOpsの現場では、Web APIからのデータ取得やログ解析など、JSONデータの処理が日常的に発生します。本記事では、軽量かつ強力なCLIツール <code>jq</code> を用いたJSON処理の効率化に焦点を当てます。特に、<code>curl</code> を使った安全なAPI連携、<code>systemd</code> による定期実行、そして安全で冪等(idempotent)なシェルスクリプトの書き方について、具体的な例を交えて解説します。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<p>このガイドでは、以下の要件と前提に基づき進めます。</p>
<p><strong>要件:</strong></p>
<ul class="wp-block-list">
<li><p><code>jq</code> コマンドでJSONデータを効率的に処理する。</p></li>
<li><p><code>curl</code> を用いて、TLS検証や再試行機能を備えた安全なAPI呼び出しを行う。</p></li>
<li><p><code>systemd</code> のUnitとTimerを活用し、定期的に処理を実行する。</p></li>
<li><p>シェルスクリプトは <code>set -euo pipefail</code>、<code>trap</code>、一時ディレクトリの利用など、安全で冪等な書き方を遵守する。</p></li>
<li><p><code>root</code> 権限を必要とする操作は最小限に留め、原則として専用ユーザーでの実行を前提とする。</p></li>
</ul>
<p><strong>前提:</strong></p>
<ul class="wp-block-list">
<li><p>Linux環境(systemdが動作するディストリビューション、例: CentOS, Ubuntu Server)が利用可能であること。</p></li>
<li><p><code>jq</code>、<code>curl</code> がインストールされていること。</p></li>
<li><p>基本的なシェルスクリプトの知識があること。</p></li>
<li><p>特定のAPIを呼び出すことを想定しますが、APIエンドポイントは仮のものとして扱います。</p></li>
</ul>
<h2 class="wp-block-heading">実装</h2>
<p>ここでは、外部APIからJSONデータを取得し、<code>jq</code> で処理してログに出力する一連のプロセスを、安全なシェルスクリプトと <code>systemd</code> を用いて実装します。</p>
<h3 class="wp-block-heading">1. JSON処理のフロー</h3>
<p>まず、今回実装する処理の全体像をMermaidで示します。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["スクリプト開始"] --> B{"一時ディレクトリ作成とクリーンアップ設定"};
B --> C["API呼び出し (curl)"];
C --|JSONデータ取得|--> D{"HTTPステータスコードとエラーチェック"};
D --|成功 (HTTP 2xx)|--> E["jqで必要なデータを抽出・変換"];
D --|失敗 (HTTP 4xx/5xx)|--> F["エラーログ出力とスクリプト終了"];
E --> G["処理結果のログ出力とファイル保存"];
G --> H["スクリプト終了"];
</pre></div>
<p>このフローに従い、API呼び出しから<code>jq</code>処理、そしてログ出力までを一貫して行います。</p>
<h3 class="wp-block-heading">2. 安全なシェルスクリプトの基礎</h3>
<p>冪等性を保ち、予期せぬエラーでスクリプトが中断しないよう、以下のベストプラクティスを採用します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# シェルスクリプトの安全な実行設定
# -e: コマンドが失敗した場合、即座にスクリプトを終了
# -u: 未定義の変数を参照した場合、エラーとして終了
# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、パイプライン全体を失敗とする
set -euo pipefail
# スクリプト名
SCRIPT_NAME=$(basename "$0")
# 一時ディレクトリのパスをグローバル変数で定義
# mktemp -d: 一意な名前の一時ディレクトリを作成。予測可能なパスへの攻撃を防ぐ。
# 作成に失敗した場合はエラー終了 (-e の恩恵)
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
# クリーンアップ関数
# スクリプトが終了する際に必ず一時ディレクトリを削除する (成功/失敗問わず)
# trap 'cleanup' EXIT: EXITシグナル (スクリプト終了時) でcleanup関数を実行
cleanup() {
log_info "一時ディレクトリ '${TMP_DIR}' を削除します。"
rm -rf "${TMP_DIR}"
log_info "スクリプト '${SCRIPT_NAME}' が終了しました。"
}
trap 'cleanup' EXIT
# ログ出力関数
log_info() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"
}
log_error() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2
}
# --- ここからメインロジック ---
log_info "スクリプト '${SCRIPT_NAME}' が開始されました。"
log_info "一時ディレクトリ: ${TMP_DIR}"
# メイン処理 (後述)
# ...
# --- ここまでメインロジック ---
exit 0 # 正常終了
</pre>
</div>
<p><strong>解説:</strong></p>
<ul class="wp-block-list">
<li><p><code>set -euo pipefail</code>: シェルスクリプトの実行を堅牢にするための必須設定です。</p></li>
<li><p><code>mktemp -d</code>: 予測不能な一時ディレクトリ名を生成し、セキュリティを強化します。</p></li>
<li><p><code>trap 'cleanup' EXIT</code>: スクリプトが正常終了しても異常終了しても、<code>cleanup</code> 関数が実行され、作成した一時ファイルが確実に削除されます。これにより冪等性が保たれ、ディスクスペースの浪費や機密データの残存を防ぎます。</p></li>
<li><p>ログ関数: 統一されたフォーマットで情報ログとエラーログを出力します。</p></li>
</ul>
<h3 class="wp-block-heading">3. <code>curl</code> を用いたAPI連携と <code>jq</code> 処理</h3>
<p>安全なシェルスクリプトの枠組みの中に、<code>curl</code> と <code>jq</code> を組み込みます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# (上記「安全なシェルスクリプトの基礎」のコードをここに含める)
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
cleanup() { rm -rf "${TMP_DIR}"; log_info "スクリプト終了。"; }
trap 'cleanup' EXIT
log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"; }
log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2; }
log_info "スクリプト開始。一時ディレクトリ: ${TMP_DIR}"
# --- API設定 ---
API_URL="https://api.example.com/data"
API_KEY="YOUR_API_KEY" # 環境変数などで渡すのが安全
OUTPUT_FILE="${TMP_DIR}/api_response.json"
PROCESSED_DATA_FILE="${TMP_DIR}/processed_data.txt"
# --- curl設定 ---
# --silent: 進捗表示を抑制
# --show-error: エラー発生時にメッセージを表示
# --fail-with-body: HTTPエラー時もレスポンスボディを出力
# --retry 5: 5回まで再試行
# --retry-delay 5: 最初のリトライまで5秒待機
# --retry-max-time 60: 全てのリトライを含め最大60秒
# --cacert /etc/pki/tls/certs/ca-bundle.crt: システムのCA証明書パス (環境により異なる)
# デフォルトで信頼されている場合は不要だが、明示推奨
# --header: ヘッダー追加。APIキーなど認証情報
# --output: 結果をファイルに出力
log_info "API (${API_URL}) からデータを取得します..."
CURL_COMMAND=(
curl
--silent
--show-error
--fail-with-body
--retry 5
--retry-delay 5
--retry-max-time 60
--cacert /etc/ssl/certs/ca-certificates.crt # Ubuntu/Debianの場合
# --cacert /etc/pki/tls/certs/ca-bundle.crt # RHEL/CentOSの場合
--header "Authorization: Bearer ${API_KEY}"
--header "Content-Type: application/json"
--output "${OUTPUT_FILE}"
"${API_URL}"
)
# API呼び出し実行
if ! "${CURL_COMMAND[@]}"; then
log_error "API呼び出しに失敗しました。"
# エラー時のレスポンスボディがあればログに出力
if [[ -f "${OUTPUT_FILE}" && -s "${OUTPUT_FILE}" ]]; then
log_error "APIエラーレスポンス: $(cat "${OUTPUT_FILE}")"
fi
exit 1
fi
log_info "APIレスポンスを '${OUTPUT_FILE}' に保存しました。"
# --- jqによるJSON処理 ---
# -r: Raw output (文字列を引用符なしで出力)
# .[]: 配列の各要素をイテレート
# .id, .name, .value: 各要素のフィールドを抽出
# | @tsv: TSV形式で出力 (タブ区切り)
# jqの計算量: 通常、JSONサイズに比例 (O(N))
# メモリ条件: JSON全体をメモリにロードするため、非常に大規模なJSONでは注意が必要。
# しかし、jqはストリーミング処理も可能で、一部のフィルタではメモリ効率が良い。
log_info "取得したJSONデータをjqで処理します..."
if ! jq -r '.data[] | select(.status == "active") | "\(.id)\t\(.name)\t\(.value)"' "${OUTPUT_FILE}" > "${PROCESSED_DATA_FILE}"; then
log_error "jqによるJSON処理に失敗しました。"
exit 1
fi
log_info "処理結果を '${PROCESSED_DATA_FILE}' に保存しました。"
# 処理結果の表示 (例: 標準出力へ)
log_info "--- 処理結果 ---"
cat "${PROCESSED_DATA_FILE}"
log_info "--- 処理結果ここまで ---"
exit 0
</pre>
</div>
<p><strong>curl コマンドのセキュリティと信頼性:</strong></p>
<ul class="wp-block-list">
<li><p><code>--cacert</code>: サーバー証明書の検証に使用するCA証明書バンドルを指定します。これにより、中間者攻撃(Man-in-the-Middle attack)を防ぎ、通信の信頼性を確保します。適切なパス(例: <code>/etc/ssl/certs/ca-certificates.crt</code> for Ubuntu/Debian, <code>/etc/pki/tls/certs/ca-bundle.crt</code> for RHEL/CentOS)を指定することが重要です。</p></li>
<li><p><code>--retry</code> オプション群: ネットワークの一時的な障害やAPIサーバーの負荷状況によるエラーを許容し、自動的に再試行します。これにより、処理の信頼性が向上します。指数バックオフは <code>--retry-delay</code> と <code>--retry-max-time</code> の組み合わせで擬似的に実現されますが、より複雑なロジックが必要な場合はスクリプト内で <code>sleep</code> を用いたループを実装します。</p></li>
<li><p><code>--fail-with-body</code>: エラーが発生した場合でも、サーバーからの応答ボディを取得し、問題解決のための情報を得やすくします。</p></li>
</ul>
<p><strong>jq コマンドの効率的な使い方:</strong></p>
<ul class="wp-block-list">
<li><p><code>.data[] | select(.status == "active") | ...</code>: 配列 <code>data</code> の各要素をイテレートし、<code>status</code> が <code>"active"</code> のものだけを抽出し、指定されたフィールド (<code>id</code>, <code>name</code>, <code>value</code>) をタブ区切りで出力しています。</p></li>
<li><p><code>jq</code> はC言語で書かれており非常に高速です。複雑なフィルタリングやデータ変換も効率的に行えます。</p></li>
<li><p>大規模なJSONを扱う場合でも、<code>jq</code> はストリーミング処理が可能な場合があり、メモリ消費を抑えられます。ただし、全てのフィルタがストリーミング対応なわけではないため注意が必要です。</p></li>
</ul>
<h3 class="wp-block-heading">4. <code>systemd</code> による定期実行の設定</h3>
<p>スクリプトを定期的に実行するために <code>systemd</code> の <code>.service</code> と <code>.timer</code> を設定します。これにより、<code>cron</code> よりも柔軟で詳細な管理が可能になります。</p>
<p><strong>a. 実行ユーザーの作成</strong></p>
<p>セキュリティ強化のため、専用のシステムユーザー <code>jsonproc</code> を作成し、このユーザーでスクリプトを実行します。<code>root</code> で直接実行することは避けるべきです。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 専用ユーザーとグループを作成
# -r: システムユーザーとして作成
# -s /sbin/nologin: シェルログインを禁止
sudo useradd -r -s /sbin/nologin jsonproc
</pre>
</div>
<p><strong>b. スクリプトの配置</strong></p>
<p>作成したシェルスクリプト <code>process_json.sh</code> を <code>/usr/local/bin/</code> など、適切なパスに配置し、実行権限を付与します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># スクリプトファイルを /usr/local/bin/ に配置
# 例: vim /usr/local/bin/process_json.sh で上記スクリプトを記述
# 実行権限を付与
sudo chmod +x /usr/local/bin/process_json.sh
# 所有者を専用ユーザーに変更 (オプションだが推奨)
sudo chown jsonproc:jsonproc /usr/local/bin/process_json.sh
</pre>
</div>
<p><strong>c. Systemd Service Unit の作成 (<code>/etc/systemd/system/process-json.service</code>)</strong></p>
<p>このファイルは、<code>process_json.sh</code> スクリプトの実行方法を定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /etc/systemd/system/process-json.service
[Unit]
Description=Process JSON data from API
# network-online.target: ネットワークが利用可能になってからサービスを開始
Requires=network-online.target
After=network-online.target
[Service]
# One-shot: 短時間で完了するタスク向け
Type=oneshot
# ExecStart: 実行するコマンド
# スクリプトの絶対パスを指定
ExecStart=/usr/local/bin/process_json.sh
# User/Group: 実行ユーザーとグループを指定し、最小権限の原則を適用
User=jsonproc
Group=jsonproc
# WorkingDirectory: スクリプトの作業ディレクトリ (オプション)
# WorkingDirectory=/var/lib/jsonproc
# StandardOutput/StandardError: 標準出力/エラーの出力先
# journalに記録されるように設定
StandardOutput=journal
StandardError=journal
# Environment: 環境変数 (APIキーなど、機密情報はVaultなどで管理を推奨)
# Environment="API_KEY=YOUR_API_KEY"
[Install]
# WantedBy: timerユニットによって起動されるため、ここではmulti-user.targetは不要
# Timerによって起動されるサービスは、通常明示的にWantedByを持たない
</pre>
</div>
<p><strong>d. Systemd Timer Unit の作成 (<code>/etc/systemd/system/process-json.timer</code>)</strong></p>
<p>このファイルは、<code>process-json.service</code> をいつ、どのくらいの頻度で実行するかを定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /etc/systemd/system/process-json.timer
[Unit]
Description=Run JSON processing script hourly
# サービスユニットが起動する前に、タイマーユニットの準備が完了していることを保証
After=process-json.service
[Timer]
# OnCalendar: スケジュール定義 (例: "hourly", "daily", "*-*-* 03:00:00")
# ここでは毎時実行を指定
OnCalendar=hourly
# Persistent=true: タイマーが非アクティブだった期間のイベントも遡って実行
# (例: サーバー停止中に実行予定だったタスクを起動時に実行)
Persistent=true
# AccuracySec: 実行精度の設定。デフォルトは1分
# AccuracySec=10s # 10秒の精度で実行
[Install]
# WantedBy: システム起動時にタイマーが有効になるように設定
# cronと同様にmulti-user.targetに追加
WantedBy=timers.target
</pre>
</div>
<p><strong>e. Systemd Timer の有効化と起動</strong></p>
<p><code>systemd</code> に新しいUnitファイルを読み込ませ、Timerを有効化して起動します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># systemdの構成をリロード
sudo systemctl daemon-reload
# timerを有効化 (システム起動時に自動起動するように)
sudo systemctl enable process-json.timer
# timerを即時起動
sudo systemctl start process-json.timer
</pre>
</div>
<h2 class="wp-block-heading">検証</h2>
<p>Timerが正しく動作しているか確認します。</p>
<ol class="wp-block-list">
<li><p><strong>Timerのステータス確認:</strong></p>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl status process-json.timer
</pre>
</div>
<p>出力例:</p>
<pre data-enlighter-language="generic">● process-json.timer - Run JSON processing script hourly
Loaded: loaded (/etc/systemd/system/process-json.timer; enabled; vendor preset: disabled)
Active: active (waiting) since Wed 2024-05-15 10:00:00 JST; 1min ago
Trigger: Wed 2024-05-15 11:00:00 JST; 59min left
Triggers: ● process-json.service
</pre>
<p><code>Active: active (waiting)</code> と <code>Trigger</code> の次回の実行日時を確認します。</p></li>
<li><p><strong>Serviceのステータス確認 (実行後):</strong>
Timerがトリガーされると、<code>process-json.service</code> が実行されます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl status process-json.service
</pre>
</div>
<p>出力例:</p>
<pre data-enlighter-language="generic">● process-json.service - Process JSON data from API
Loaded: loaded (/etc/systemd/system/process-json.service; static; vendor preset: disabled)
Active: inactive (dead) since Wed 2024-05-15 10:00:00 JST; 1min ago
Process: 12345 ExecStart=/usr/local/bin/process_json.sh (code=exited, status=0/SUCCESS)
</pre>
<p><code>Active: inactive (dead)</code> と <code>status=0/SUCCESS</code> が表示されていれば、正常終了です。</p></li>
<li><p><strong>ログの確認:</strong>
スクリプトの標準出力と標準エラー出力は <code>journalctl</code> で確認できます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u process-json.service --since "1 hour ago"
</pre>
</div>
<p>スクリプト内で定義した <code>log_info</code> や <code>log_error</code> のメッセージが表示されることを確認します。<code>process_json.sh</code> が出力した「— 処理結果 —」の内容もここで確認できます。</p></li>
</ol>
<h2 class="wp-block-heading">運用</h2>
<h3 class="wp-block-heading">ログ監視</h3>
<p><code>systemd</code> によって実行されるスクリプトのログは <code>journalctl</code> で集中管理されます。</p>
<ul class="wp-block-list">
<li><p><strong>エラーログの監視:</strong> <code>journalctl -u process-json.service -p err</code> でエラーのみをフィルタリングしたり、<code>rsyslog</code> や <code>fluentd</code> などのログ転送エージェントと連携して、監視システム(Prometheus/Grafana, ELK Stackなど)に連携することで、異常発生時にアラートを発することができます。</p></li>
<li><p><strong>継続的な確認:</strong> <code>journalctl -f -u process-json.service</code> でリアルタイムにログを追跡できます。</p></li>
</ul>
<h3 class="wp-block-heading">権限管理</h3>
<ul class="wp-block-list">
<li><p><strong>最小権限の原則:</strong> <code>systemd</code> の <code>User=</code> および <code>Group=</code> オプションは、スクリプトを専用の非特権ユーザー(例: <code>jsonproc</code>)で実行するための重要な機能です。これにより、スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。</p></li>
<li><p><strong><code>root</code> 権限の扱い:</strong> この例のように、<code>systemd</code> ユニットの配置や <code>useradd</code> の実行など、システムレベルの設定変更には <code>root</code> 権限が必要ですが、実際のスクリプト実行は非特権ユーザーで行うべきです。もしスクリプト内で <code>root</code> 権限が必要な処理がある場合は、<code>sudo</code> を使用し、特定のコマンドのみを実行できるよう <code>sudoers</code> ファイルで厳密に制御することを検討してください。</p></li>
</ul>
<h2 class="wp-block-heading">トラブルシュート</h2>
<ul class="wp-block-list">
<li><p><strong>スクリプトが実行されない:</strong></p>
<ul>
<li><p><code>systemctl status process-json.timer</code> でタイマーが <code>active (waiting)</code> か、次回の実行時刻が適切か確認します。</p></li>
<li><p><code>sudo systemctl daemon-reload</code> を実行し、Unitファイルが読み込まれていることを確認します。</p></li>
<li><p><code>sudo systemctl enable process-json.timer</code> でタイマーが有効化されているか確認します。</p></li>
</ul></li>
<li><p><strong>スクリプトがエラーになる:</strong></p>
<ul>
<li><p><code>journalctl -u process-json.service</code> でログを確認し、スクリプト内のエラーメッセージや <code>jq</code>, <code>curl</code> の出力を確認します。</p></li>
<li><p>スクリプトを直接実行し、<code>bash -x /usr/local/bin/process_json.sh</code> でトレース出力を見てデバッグします。</p></li>
<li><p><code>User=</code> をコメントアウトして <code>root</code> で実行してみる(デバッグ用途のみ、運用は非推奨)ことで、権限の問題かを確認できる場合があります。</p></li>
</ul></li>
<li><p><strong>APIからの応答がない/エラー:</strong></p>
<ul>
<li><p><code>curl</code> コマンドをシェルで直接実行し、ネットワーク接続や認証情報の問題がないか確認します。</p></li>
<li><p><code>--verbose</code> オプションを追加して詳細なリクエスト/レスポンス情報を確認します。</p></li>
</ul></li>
<li><p><strong>jqのフィルタリングがおかしい:</strong></p>
<ul>
<li><p>APIから取得したJSONファイル (<code>${TMP_DIR}/api_response.json</code>) を直接 <code>jq</code> に渡し、様々なフィルタを試してデバッグします。</p></li>
<li><p>例: <code>jq '.' /tmp/api_response.json</code> でJSON構造全体を確認します。</p></li>
</ul></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>、<code>jq</code> コマンドを使ったJSONデータの効率的な処理、<code>curl</code> による安全なAPI連携、そして <code>systemd</code> を利用した定期実行の手順について解説しました。
特に、<code>set -euo pipefail</code> や <code>trap</code>、一時ディレクトリの使用など、シェルスクリプトの安全な書き方に焦点を当て、冪等性と堅牢性を確保しました。
また、<code>systemd</code> の <code>User=</code> オプションによる権限分離の重要性にも触れ、セキュリティを意識した運用が可能であることを示しました。これらの技術を組み合わせることで、DevOpsの現場で信頼性の高い自動処理システムを構築できます。
本記事の内容は2024年5月15日 JST時点の情報に基づいています。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
jqコマンドでJSONを効率処理:安全なAPI連携と定期実行
DevOpsの現場では、Web APIからのデータ取得やログ解析など、JSONデータの処理が日常的に発生します。本記事では、軽量かつ強力なCLIツール jq
を用いたJSON処理の効率化に焦点を当てます。特に、curl
を使った安全なAPI連携、systemd
による定期実行、そして安全で冪等(idempotent)なシェルスクリプトの書き方について、具体的な例を交えて解説します。
要件と前提
このガイドでは、以下の要件と前提に基づき進めます。
要件:
jq
コマンドでJSONデータを効率的に処理する。
curl
を用いて、TLS検証や再試行機能を備えた安全なAPI呼び出しを行う。
systemd
のUnitとTimerを活用し、定期的に処理を実行する。
シェルスクリプトは set -euo pipefail
、trap
、一時ディレクトリの利用など、安全で冪等な書き方を遵守する。
root
権限を必要とする操作は最小限に留め、原則として専用ユーザーでの実行を前提とする。
前提:
Linux環境(systemdが動作するディストリビューション、例: CentOS, Ubuntu Server)が利用可能であること。
jq
、curl
がインストールされていること。
基本的なシェルスクリプトの知識があること。
特定のAPIを呼び出すことを想定しますが、APIエンドポイントは仮のものとして扱います。
実装
ここでは、外部APIからJSONデータを取得し、jq
で処理してログに出力する一連のプロセスを、安全なシェルスクリプトと systemd
を用いて実装します。
1. JSON処理のフロー
まず、今回実装する処理の全体像をMermaidで示します。
graph TD
A["スクリプト開始"] --> B{"一時ディレクトリ作成とクリーンアップ設定"};
B --> C["API呼び出し (curl)"];
C --|JSONデータ取得|--> D{"HTTPステータスコードとエラーチェック"};
D --|成功 (HTTP 2xx)|--> E["jqで必要なデータを抽出・変換"];
D --|失敗 (HTTP 4xx/5xx)|--> F["エラーログ出力とスクリプト終了"];
E --> G["処理結果のログ出力とファイル保存"];
G --> H["スクリプト終了"];
このフローに従い、API呼び出しからjq
処理、そしてログ出力までを一貫して行います。
2. 安全なシェルスクリプトの基礎
冪等性を保ち、予期せぬエラーでスクリプトが中断しないよう、以下のベストプラクティスを採用します。
#!/bin/bash
# シェルスクリプトの安全な実行設定
# -e: コマンドが失敗した場合、即座にスクリプトを終了
# -u: 未定義の変数を参照した場合、エラーとして終了
# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、パイプライン全体を失敗とする
set -euo pipefail
# スクリプト名
SCRIPT_NAME=$(basename "$0")
# 一時ディレクトリのパスをグローバル変数で定義
# mktemp -d: 一意な名前の一時ディレクトリを作成。予測可能なパスへの攻撃を防ぐ。
# 作成に失敗した場合はエラー終了 (-e の恩恵)
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
# クリーンアップ関数
# スクリプトが終了する際に必ず一時ディレクトリを削除する (成功/失敗問わず)
# trap 'cleanup' EXIT: EXITシグナル (スクリプト終了時) でcleanup関数を実行
cleanup() {
log_info "一時ディレクトリ '${TMP_DIR}' を削除します。"
rm -rf "${TMP_DIR}"
log_info "スクリプト '${SCRIPT_NAME}' が終了しました。"
}
trap 'cleanup' EXIT
# ログ出力関数
log_info() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"
}
log_error() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2
}
# --- ここからメインロジック ---
log_info "スクリプト '${SCRIPT_NAME}' が開始されました。"
log_info "一時ディレクトリ: ${TMP_DIR}"
# メイン処理 (後述)
# ...
# --- ここまでメインロジック ---
exit 0 # 正常終了
解説:
set -euo pipefail
: シェルスクリプトの実行を堅牢にするための必須設定です。
mktemp -d
: 予測不能な一時ディレクトリ名を生成し、セキュリティを強化します。
trap 'cleanup' EXIT
: スクリプトが正常終了しても異常終了しても、cleanup
関数が実行され、作成した一時ファイルが確実に削除されます。これにより冪等性が保たれ、ディスクスペースの浪費や機密データの残存を防ぎます。
ログ関数: 統一されたフォーマットで情報ログとエラーログを出力します。
3. curl を用いたAPI連携と jq 処理
安全なシェルスクリプトの枠組みの中に、curl
と jq
を組み込みます。
#!/bin/bash
# (上記「安全なシェルスクリプトの基礎」のコードをここに含める)
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
cleanup() { rm -rf "${TMP_DIR}"; log_info "スクリプト終了。"; }
trap 'cleanup' EXIT
log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] ${SCRIPT_NAME}: $*"; }
log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${SCRIPT_NAME}: $*" >&2; }
log_info "スクリプト開始。一時ディレクトリ: ${TMP_DIR}"
# --- API設定 ---
API_URL="https://api.example.com/data"
API_KEY="YOUR_API_KEY" # 環境変数などで渡すのが安全
OUTPUT_FILE="${TMP_DIR}/api_response.json"
PROCESSED_DATA_FILE="${TMP_DIR}/processed_data.txt"
# --- curl設定 ---
# --silent: 進捗表示を抑制
# --show-error: エラー発生時にメッセージを表示
# --fail-with-body: HTTPエラー時もレスポンスボディを出力
# --retry 5: 5回まで再試行
# --retry-delay 5: 最初のリトライまで5秒待機
# --retry-max-time 60: 全てのリトライを含め最大60秒
# --cacert /etc/pki/tls/certs/ca-bundle.crt: システムのCA証明書パス (環境により異なる)
# デフォルトで信頼されている場合は不要だが、明示推奨
# --header: ヘッダー追加。APIキーなど認証情報
# --output: 結果をファイルに出力
log_info "API (${API_URL}) からデータを取得します..."
CURL_COMMAND=(
curl
--silent
--show-error
--fail-with-body
--retry 5
--retry-delay 5
--retry-max-time 60
--cacert /etc/ssl/certs/ca-certificates.crt # Ubuntu/Debianの場合
# --cacert /etc/pki/tls/certs/ca-bundle.crt # RHEL/CentOSの場合
--header "Authorization: Bearer ${API_KEY}"
--header "Content-Type: application/json"
--output "${OUTPUT_FILE}"
"${API_URL}"
)
# API呼び出し実行
if ! "${CURL_COMMAND[@]}"; then
log_error "API呼び出しに失敗しました。"
# エラー時のレスポンスボディがあればログに出力
if [[ -f "${OUTPUT_FILE}" && -s "${OUTPUT_FILE}" ]]; then
log_error "APIエラーレスポンス: $(cat "${OUTPUT_FILE}")"
fi
exit 1
fi
log_info "APIレスポンスを '${OUTPUT_FILE}' に保存しました。"
# --- jqによるJSON処理 ---
# -r: Raw output (文字列を引用符なしで出力)
# .[]: 配列の各要素をイテレート
# .id, .name, .value: 各要素のフィールドを抽出
# | @tsv: TSV形式で出力 (タブ区切り)
# jqの計算量: 通常、JSONサイズに比例 (O(N))
# メモリ条件: JSON全体をメモリにロードするため、非常に大規模なJSONでは注意が必要。
# しかし、jqはストリーミング処理も可能で、一部のフィルタではメモリ効率が良い。
log_info "取得したJSONデータをjqで処理します..."
if ! jq -r '.data[] | select(.status == "active") | "\(.id)\t\(.name)\t\(.value)"' "${OUTPUT_FILE}" > "${PROCESSED_DATA_FILE}"; then
log_error "jqによるJSON処理に失敗しました。"
exit 1
fi
log_info "処理結果を '${PROCESSED_DATA_FILE}' に保存しました。"
# 処理結果の表示 (例: 標準出力へ)
log_info "--- 処理結果 ---"
cat "${PROCESSED_DATA_FILE}"
log_info "--- 処理結果ここまで ---"
exit 0
curl コマンドのセキュリティと信頼性:
--cacert
: サーバー証明書の検証に使用するCA証明書バンドルを指定します。これにより、中間者攻撃(Man-in-the-Middle attack)を防ぎ、通信の信頼性を確保します。適切なパス(例: /etc/ssl/certs/ca-certificates.crt
for Ubuntu/Debian, /etc/pki/tls/certs/ca-bundle.crt
for RHEL/CentOS)を指定することが重要です。
--retry
オプション群: ネットワークの一時的な障害やAPIサーバーの負荷状況によるエラーを許容し、自動的に再試行します。これにより、処理の信頼性が向上します。指数バックオフは --retry-delay
と --retry-max-time
の組み合わせで擬似的に実現されますが、より複雑なロジックが必要な場合はスクリプト内で sleep
を用いたループを実装します。
--fail-with-body
: エラーが発生した場合でも、サーバーからの応答ボディを取得し、問題解決のための情報を得やすくします。
jq コマンドの効率的な使い方:
.data[] | select(.status == "active") | ...
: 配列 data
の各要素をイテレートし、status
が "active"
のものだけを抽出し、指定されたフィールド (id
, name
, value
) をタブ区切りで出力しています。
jq
はC言語で書かれており非常に高速です。複雑なフィルタリングやデータ変換も効率的に行えます。
大規模なJSONを扱う場合でも、jq
はストリーミング処理が可能な場合があり、メモリ消費を抑えられます。ただし、全てのフィルタがストリーミング対応なわけではないため注意が必要です。
4. systemd による定期実行の設定
スクリプトを定期的に実行するために systemd
の .service
と .timer
を設定します。これにより、cron
よりも柔軟で詳細な管理が可能になります。
a. 実行ユーザーの作成
セキュリティ強化のため、専用のシステムユーザー jsonproc
を作成し、このユーザーでスクリプトを実行します。root
で直接実行することは避けるべきです。
# 専用ユーザーとグループを作成
# -r: システムユーザーとして作成
# -s /sbin/nologin: シェルログインを禁止
sudo useradd -r -s /sbin/nologin jsonproc
b. スクリプトの配置
作成したシェルスクリプト process_json.sh
を /usr/local/bin/
など、適切なパスに配置し、実行権限を付与します。
# スクリプトファイルを /usr/local/bin/ に配置
# 例: vim /usr/local/bin/process_json.sh で上記スクリプトを記述
# 実行権限を付与
sudo chmod +x /usr/local/bin/process_json.sh
# 所有者を専用ユーザーに変更 (オプションだが推奨)
sudo chown jsonproc:jsonproc /usr/local/bin/process_json.sh
c. Systemd Service Unit の作成 (/etc/systemd/system/process-json.service
)
このファイルは、process_json.sh
スクリプトの実行方法を定義します。
# /etc/systemd/system/process-json.service
[Unit]
Description=Process JSON data from API
# network-online.target: ネットワークが利用可能になってからサービスを開始
Requires=network-online.target
After=network-online.target
[Service]
# One-shot: 短時間で完了するタスク向け
Type=oneshot
# ExecStart: 実行するコマンド
# スクリプトの絶対パスを指定
ExecStart=/usr/local/bin/process_json.sh
# User/Group: 実行ユーザーとグループを指定し、最小権限の原則を適用
User=jsonproc
Group=jsonproc
# WorkingDirectory: スクリプトの作業ディレクトリ (オプション)
# WorkingDirectory=/var/lib/jsonproc
# StandardOutput/StandardError: 標準出力/エラーの出力先
# journalに記録されるように設定
StandardOutput=journal
StandardError=journal
# Environment: 環境変数 (APIキーなど、機密情報はVaultなどで管理を推奨)
# Environment="API_KEY=YOUR_API_KEY"
[Install]
# WantedBy: timerユニットによって起動されるため、ここではmulti-user.targetは不要
# Timerによって起動されるサービスは、通常明示的にWantedByを持たない
d. Systemd Timer Unit の作成 (/etc/systemd/system/process-json.timer
)
このファイルは、process-json.service
をいつ、どのくらいの頻度で実行するかを定義します。
# /etc/systemd/system/process-json.timer
[Unit]
Description=Run JSON processing script hourly
# サービスユニットが起動する前に、タイマーユニットの準備が完了していることを保証
After=process-json.service
[Timer]
# OnCalendar: スケジュール定義 (例: "hourly", "daily", "*-*-* 03:00:00")
# ここでは毎時実行を指定
OnCalendar=hourly
# Persistent=true: タイマーが非アクティブだった期間のイベントも遡って実行
# (例: サーバー停止中に実行予定だったタスクを起動時に実行)
Persistent=true
# AccuracySec: 実行精度の設定。デフォルトは1分
# AccuracySec=10s # 10秒の精度で実行
[Install]
# WantedBy: システム起動時にタイマーが有効になるように設定
# cronと同様にmulti-user.targetに追加
WantedBy=timers.target
e. Systemd Timer の有効化と起動
systemd
に新しいUnitファイルを読み込ませ、Timerを有効化して起動します。
# systemdの構成をリロード
sudo systemctl daemon-reload
# timerを有効化 (システム起動時に自動起動するように)
sudo systemctl enable process-json.timer
# timerを即時起動
sudo systemctl start process-json.timer
検証
Timerが正しく動作しているか確認します。
Timerのステータス確認:
systemctl status process-json.timer
出力例:
● process-json.timer - Run JSON processing script hourly
Loaded: loaded (/etc/systemd/system/process-json.timer; enabled; vendor preset: disabled)
Active: active (waiting) since Wed 2024-05-15 10:00:00 JST; 1min ago
Trigger: Wed 2024-05-15 11:00:00 JST; 59min left
Triggers: ● process-json.service
Active: active (waiting)
と Trigger
の次回の実行日時を確認します。
Serviceのステータス確認 (実行後):
Timerがトリガーされると、process-json.service
が実行されます。
systemctl status process-json.service
出力例:
● process-json.service - Process JSON data from API
Loaded: loaded (/etc/systemd/system/process-json.service; static; vendor preset: disabled)
Active: inactive (dead) since Wed 2024-05-15 10:00:00 JST; 1min ago
Process: 12345 ExecStart=/usr/local/bin/process_json.sh (code=exited, status=0/SUCCESS)
Active: inactive (dead)
と status=0/SUCCESS
が表示されていれば、正常終了です。
ログの確認:
スクリプトの標準出力と標準エラー出力は journalctl
で確認できます。
journalctl -u process-json.service --since "1 hour ago"
スクリプト内で定義した log_info
や log_error
のメッセージが表示されることを確認します。process_json.sh
が出力した「— 処理結果 —」の内容もここで確認できます。
運用
ログ監視
systemd
によって実行されるスクリプトのログは journalctl
で集中管理されます。
エラーログの監視: journalctl -u process-json.service -p err
でエラーのみをフィルタリングしたり、rsyslog
や fluentd
などのログ転送エージェントと連携して、監視システム(Prometheus/Grafana, ELK Stackなど)に連携することで、異常発生時にアラートを発することができます。
継続的な確認: journalctl -f -u process-json.service
でリアルタイムにログを追跡できます。
権限管理
最小権限の原則: systemd
の User=
および Group=
オプションは、スクリプトを専用の非特権ユーザー(例: jsonproc
)で実行するための重要な機能です。これにより、スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。
root
権限の扱い: この例のように、systemd
ユニットの配置や useradd
の実行など、システムレベルの設定変更には root
権限が必要ですが、実際のスクリプト実行は非特権ユーザーで行うべきです。もしスクリプト内で root
権限が必要な処理がある場合は、sudo
を使用し、特定のコマンドのみを実行できるよう sudoers
ファイルで厳密に制御することを検討してください。
トラブルシュート
スクリプトが実行されない:
systemctl status process-json.timer
でタイマーが active (waiting)
か、次回の実行時刻が適切か確認します。
sudo systemctl daemon-reload
を実行し、Unitファイルが読み込まれていることを確認します。
sudo systemctl enable process-json.timer
でタイマーが有効化されているか確認します。
スクリプトがエラーになる:
journalctl -u process-json.service
でログを確認し、スクリプト内のエラーメッセージや jq
, curl
の出力を確認します。
スクリプトを直接実行し、bash -x /usr/local/bin/process_json.sh
でトレース出力を見てデバッグします。
User=
をコメントアウトして root
で実行してみる(デバッグ用途のみ、運用は非推奨)ことで、権限の問題かを確認できる場合があります。
APIからの応答がない/エラー:
jqのフィルタリングがおかしい:
まとめ
、jq
コマンドを使ったJSONデータの効率的な処理、curl
による安全なAPI連携、そして systemd
を利用した定期実行の手順について解説しました。
特に、set -euo pipefail
や trap
、一時ディレクトリの使用など、シェルスクリプトの安全な書き方に焦点を当て、冪等性と堅牢性を確保しました。
また、systemd
の User=
オプションによる権限分離の重要性にも触れ、セキュリティを意識した運用が可能であることを示しました。これらの技術を組み合わせることで、DevOpsの現場で信頼性の高い自動処理システムを構築できます。
本記事の内容は2024年5月15日 JST時点の情報に基づいています。
コメント