<p><!-- METADATA: {"role": "SRE_DevOps_Engineer", "topic": "curl-jq-api-error-handling", "version": "1.1"} -->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">curlとjqを組み合わせた堅牢なREST API監視スクリプトの自動化設計</h1>
<h2 class="wp-block-heading">【導入と前提】</h2>
<p>REST APIの応答監視をシェルスクリプトで堅牢化し、異常検知時の即時ハンドリングと自動復旧を可能にします。</p>
<ul class="wp-block-list">
<li><p><strong>OS/実行環境</strong>: Linux(Ubuntu 22.04 LTS / RHEL 9 動作確認済)</p></li>
<li><p><strong>前提ツール</strong>: <code>curl</code> (7.68.0以降)、<code>jq</code> (1.6以降)、<code>systemd</code> (245以降)</p></li>
</ul>
<hr/>
<h2 class="wp-block-heading">【処理フローと設計】</h2>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A[Start] --> B["Execute curl with retries"]
B --> C{"HTTP Status 200?"}
C -->|No| D["Log error to stderr & Alert"]
C -->|Yes| E["Parse JSON payload via jq"]
E --> F{"Validate fields / status?"}
F -->|Invalid| G["Log validation error"]
F -->|Valid| H["Write metrics & Success Exit"]
D --> I["Clean up temporary files"]
G --> I
H --> I
I --> J["Exit with code"]
</pre></div>
<h3 class="wp-block-heading">処理フローの解説</h3>
<ol class="wp-block-list">
<li><p><strong>耐障害性の高い通信</strong>: <code>curl</code> のリトライ機能を使い、一時的なネットワーク瞬断やAPIサーバーの起動遅延による誤検知を防止します。</p></li>
<li><p><strong>フェイルファストと安全な分離</strong>: HTTPステータスコードの判定と、JSONレスポンスボディの評価を分離して行います。</p></li>
<li><p><strong>JSONパース検証</strong>: <code>jq</code> の終了コード判定オプション(<code>-e</code>)を活用し、パースエラーおよび特定フィールドの期待値検証(スキーマチェック)をスマートに行います。</p></li>
<li><p><strong>確実な後処理</strong>: <code>trap</code> を用いて、スクリプトが途中で異常終了した場合でも、作成した一時ファイルを確実にクリーンアップします。</p></li>
</ol>
<hr/>
<h2 class="wp-block-heading">【実装:堅牢な自動化スクリプト】</h2>
<h3 class="wp-block-heading">1. API監視・検証シェルスクリプト (<code>/usr/local/bin/api_health_check.sh</code>)</h3>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/usr/bin/env bash
# ==============================================================================
# API Health Check & Response Validation Script
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# 定義と環境設定
# ------------------------------------------------------------------------------
API_URL="https://api.example.com/v1/health"
TIMEOUT_SEC=10
MAX_RETRIES=3
RETRY_DELAY=2
# 一時ファイルの作成と確実な削除(trap)
BODY_FILE=$(mktemp /tmp/api_response.XXXXXX.json)
readonly BODY_FILE
trap 'rm -f "${BODY_FILE}"' EXIT
# ------------------------------------------------------------------------------
# メイン処理
# ------------------------------------------------------------------------------
echo "[INFO] Starting API health check for: ${API_URL}"
# curl実行オプションの解説:
# -s : 進捗メーターを非表示 (Silent)
# -S : エラー時はメッセージを出力 (Show error)
# -L : リダイレクトを自動追跡 (Location)
# -w : 出力フォーマットの指定 (HTTPステータスのみ取得)
# -o : レスポンスボディをファイルに保存
# --max-time : タイムアウト値
# --retry : リトライ回数
# --retry-delay : リトライ間隔
# --retry-connrefused : 接続拒否時もリトライ対象にする
HTTP_STATUS=$(curl -s -S -L \
-w "%{http_code}" \
-o "${BODY_FILE}" \
--max-time "${TIMEOUT_SEC}" \
--retry "${MAX_RETRIES}" \
--retry-delay "${RETRY_DELAY}" \
--retry-connrefused \
"${API_URL}")
# HTTPステータスコードの検証
if [ "${HTTP_STATUS}" -ne 200 ]; then
echo "[ERROR] API request failed with HTTP Status: ${HTTP_STATUS}" >&2
if [ -f "${BODY_FILE}" ] && [ -s "${BODY_FILE}" ]; then
echo "[ERROR] Response body: $(cat "${BODY_FILE}")" >&2
fi
exit 1
fi
echo "[INFO] HTTP Connection successful (Status: 200). Validating response payload..."
# jqオプションの解説:
# -e : 処理結果が null または false の場合に終了ステータス「1」を返す (Exit status)
# -r : 生の文字列を出力 (Raw output)
#
# パース検証条件:
# 1. JSONが有効なオブジェクトであること
# 2. .status が "healthy" であること
# 3. .database.connected が true であること
if ! jq -e '.status == "healthy" and .database.connected == true' "${BODY_FILE}" > /dev/null 2>&1; then
echo "[ERROR] API response payload validation failed." >&2
echo "[ERROR] Payload was: $(cat "${BODY_FILE}")" >&2
exit 2
fi
# 検証成功時の処理
echo "[INFO] API validation succeeded."
VERSION=$(jq -r '.version // "unknown"' "${BODY_FILE}")
echo "[INFO] API Version: ${VERSION}"
</pre>
</div>
<h3 class="wp-block-heading">2. systemdによる定期実行設定</h3>
<h4 class="wp-block-heading">① サービスユニット定義 (<code>/etc/systemd/system/api-healthcheck.service</code>)</h4>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=REST API Health Validation Service
After=network-online.target
Wants=network-online.target
[Service]
Type=OneShot
ExecStart=/usr/local/bin/api_health_check.sh
User=nobody
Group=nogroup
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
</pre>
</div>
<h4 class="wp-block-heading">② タイマーユニット定義 (<code>/etc/systemd/system/api-healthcheck.timer</code>)</h4>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run REST API Health Validation every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=1s
[Install]
WantedBy=timers.target
</pre>
</div><hr/>
<h2 class="wp-block-heading">【検証と運用】</h2>
<h3 class="wp-block-heading">1. 手動での動作検証</h3>
<p>作成したスクリプトに実行権限を付与し、動作を確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 実行権限の付与
chmod +x /usr/local/bin/api_health_check.sh
# 正常系のシミュレーション(モックAPIを用意するか、URLを検証対象に変更して実行)
/usr/local/bin/api_health_check.sh
echo $? # -> 0
# 異常系のシミュレーション(意図的に誤ったURLを指定し検証)
# 終了コードが 1(通信エラー) または 2(ペイロード検証エラー)になることを確認
</pre>
</div>
<h3 class="wp-block-heading">2. systemd タイマーの有効化と稼働確認</h3>
<div class="codehilite">
<pre data-enlighter-language="generic"># systemd設定の再読み込み
sudo systemctl daemon-reload
# タイマーの起動と自動起動設定
sudo systemctl enable --now api-healthcheck.timer
# タイマー一覧と次回実行予定の確認
systemctl list-timers --all | grep api-healthcheck
</pre>
</div>
<h3 class="wp-block-heading">3. ログの確認方法</h3>
<p>systemdで実行された監視結果は <code>journalctl</code> で追跡可能です。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 最新の実行ログをリアルタイムで監視
sudo journalctl -u api-healthcheck.service -f
# エラーログ(stderr)のみをフィルタリング抽出
sudo journalctl -u api-healthcheck.service -p err
</pre>
</div><hr/>
<h2 class="wp-block-heading">【トラブルシューティングと落とし穴】</h2>
<ol class="wp-block-list">
<li><p><strong>パイプライン処理と <code>set -eo pipefail</code> の罠</strong></p>
<ul>
<li><p><strong>落とし穴</strong>: <code>curl ... | jq ...</code> と直接パイプで繋ぐと、<code>curl</code> がコネクションエラー(502等)で失敗しても、<code>jq</code> がそれをパースしようとして、意図しないエラーハンドリング経路を辿る可能性があります。</p></li>
<li><p><strong>対策</strong>: 本スクリプトのように、<code>curl</code> の出力を一度一時ファイル (<code>mktemp</code>) に書き出し、HTTPステータスコードを単独で評価してから <code>jq</code> パースへと進める「フェイルファスト設計」が最も安全です。</p></li>
</ul></li>
<li><p><strong>APIキーなどの環境変数リーク</strong></p>
<ul>
<li><p><strong>落とし穴</strong>: <code>curl -H "Authorization: Bearer $API_TOKEN"</code> を含むスクリプトを <code>set -x</code>(デバッグモード)で実行すると、ログに認証トークンが平文で出力されてしまいます。</p></li>
<li><p><strong>対策</strong>: <code>set -x</code> は本番運用時には絶対に使用せず、トークン等の秘匿情報は <code>systemd</code> の <code>EnvironmentFile=</code> オプション等を用いて、権限管理されたファイル(例: <code>chmod 600</code>)から読み込ませます。</p></li>
</ul></li>
<li><p><strong>一時ファイル(<code>/tmp</code>)のクリーンアップ漏れ</strong></p>
<ul>
<li><p><strong>落とし穴</strong>: スクリプトの途中でシグナル(SIGINT, SIGTERM)を受信したり、エラーで即時終了した場合に一時ファイルが残存し、ディスク容量を圧迫します。</p></li>
<li><p><strong>対策</strong>: スクリプト先頭で <code>trap 'rm -f "${BODY_FILE}"' EXIT</code> を宣言することで、正常・異常終了を問わず確実にファイルを削除します。また、systemd側の <code>PrivateTmp=true</code> 設定により、OS全体の共有ディレクトリを汚染するリスクを物理的に排除しています。</p></li>
</ul></li>
</ol>
<hr/>
<h2 class="wp-block-heading">【まとめ】</h2>
<p>運用の冪等性と堅牢性を維持するために、以下の3つのポイントを徹底してください。</p>
<ul class="wp-block-list">
<li><p><strong>冪等性の担保</strong>: 監視スクリプトは何度実行してもシステムの状態を破壊しないよう、副作用のないGETリクエストをベースに設計する。</p></li>
<li><p><strong>適切なリトライ戦略</strong>: 一時的なネットワークエラーによるアラートの誤検知を防ぐため、<code>curl</code> の <code>--retry</code> オプションによる段階的バックオフを組み込む。</p></li>
<li><p><strong>最小権限の法則</strong>: 監視サービスは <code>root</code> ではなく、書き込み権限の限定された <code>nobody</code> ユーザーなどの非特権アカウントで実行する。</p></li>
</ul>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
curlとjqを組み合わせた堅牢なREST API監視スクリプトの自動化設計
【導入と前提】
REST APIの応答監視をシェルスクリプトで堅牢化し、異常検知時の即時ハンドリングと自動復旧を可能にします。
【処理フローと設計】
graph TD
A[Start] --> B["Execute curl with retries"]
B --> C{"HTTP Status 200?"}
C -->|No| D["Log error to stderr & Alert"]
C -->|Yes| E["Parse JSON payload via jq"]
E --> F{"Validate fields / status?"}
F -->|Invalid| G["Log validation error"]
F -->|Valid| H["Write metrics & Success Exit"]
D --> I["Clean up temporary files"]
G --> I
H --> I
I --> J["Exit with code"]
処理フローの解説
耐障害性の高い通信: curl のリトライ機能を使い、一時的なネットワーク瞬断やAPIサーバーの起動遅延による誤検知を防止します。
フェイルファストと安全な分離: HTTPステータスコードの判定と、JSONレスポンスボディの評価を分離して行います。
JSONパース検証: jq の終了コード判定オプション(-e)を活用し、パースエラーおよび特定フィールドの期待値検証(スキーマチェック)をスマートに行います。
確実な後処理: trap を用いて、スクリプトが途中で異常終了した場合でも、作成した一時ファイルを確実にクリーンアップします。
【実装:堅牢な自動化スクリプト】
1. API監視・検証シェルスクリプト (/usr/local/bin/api_health_check.sh)
#!/usr/bin/env bash
# ==============================================================================
# API Health Check & Response Validation Script
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# 定義と環境設定
# ------------------------------------------------------------------------------
API_URL="https://api.example.com/v1/health"
TIMEOUT_SEC=10
MAX_RETRIES=3
RETRY_DELAY=2
# 一時ファイルの作成と確実な削除(trap)
BODY_FILE=$(mktemp /tmp/api_response.XXXXXX.json)
readonly BODY_FILE
trap 'rm -f "${BODY_FILE}"' EXIT
# ------------------------------------------------------------------------------
# メイン処理
# ------------------------------------------------------------------------------
echo "[INFO] Starting API health check for: ${API_URL}"
# curl実行オプションの解説:
# -s : 進捗メーターを非表示 (Silent)
# -S : エラー時はメッセージを出力 (Show error)
# -L : リダイレクトを自動追跡 (Location)
# -w : 出力フォーマットの指定 (HTTPステータスのみ取得)
# -o : レスポンスボディをファイルに保存
# --max-time : タイムアウト値
# --retry : リトライ回数
# --retry-delay : リトライ間隔
# --retry-connrefused : 接続拒否時もリトライ対象にする
HTTP_STATUS=$(curl -s -S -L \
-w "%{http_code}" \
-o "${BODY_FILE}" \
--max-time "${TIMEOUT_SEC}" \
--retry "${MAX_RETRIES}" \
--retry-delay "${RETRY_DELAY}" \
--retry-connrefused \
"${API_URL}")
# HTTPステータスコードの検証
if [ "${HTTP_STATUS}" -ne 200 ]; then
echo "[ERROR] API request failed with HTTP Status: ${HTTP_STATUS}" >&2
if [ -f "${BODY_FILE}" ] && [ -s "${BODY_FILE}" ]; then
echo "[ERROR] Response body: $(cat "${BODY_FILE}")" >&2
fi
exit 1
fi
echo "[INFO] HTTP Connection successful (Status: 200). Validating response payload..."
# jqオプションの解説:
# -e : 処理結果が null または false の場合に終了ステータス「1」を返す (Exit status)
# -r : 生の文字列を出力 (Raw output)
#
# パース検証条件:
# 1. JSONが有効なオブジェクトであること
# 2. .status が "healthy" であること
# 3. .database.connected が true であること
if ! jq -e '.status == "healthy" and .database.connected == true' "${BODY_FILE}" > /dev/null 2>&1; then
echo "[ERROR] API response payload validation failed." >&2
echo "[ERROR] Payload was: $(cat "${BODY_FILE}")" >&2
exit 2
fi
# 検証成功時の処理
echo "[INFO] API validation succeeded."
VERSION=$(jq -r '.version // "unknown"' "${BODY_FILE}")
echo "[INFO] API Version: ${VERSION}"
2. systemdによる定期実行設定
① サービスユニット定義 (/etc/systemd/system/api-healthcheck.service)
[Unit]
Description=REST API Health Validation Service
After=network-online.target
Wants=network-online.target
[Service]
Type=OneShot
ExecStart=/usr/local/bin/api_health_check.sh
User=nobody
Group=nogroup
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
② タイマーユニット定義 (/etc/systemd/system/api-healthcheck.timer)
[Unit]
Description=Run REST API Health Validation every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=1s
[Install]
WantedBy=timers.target
【検証と運用】
1. 手動での動作検証
作成したスクリプトに実行権限を付与し、動作を確認します。
# 実行権限の付与
chmod +x /usr/local/bin/api_health_check.sh
# 正常系のシミュレーション(モックAPIを用意するか、URLを検証対象に変更して実行)
/usr/local/bin/api_health_check.sh
echo $? # -> 0
# 異常系のシミュレーション(意図的に誤ったURLを指定し検証)
# 終了コードが 1(通信エラー) または 2(ペイロード検証エラー)になることを確認
2. systemd タイマーの有効化と稼働確認
# systemd設定の再読み込み
sudo systemctl daemon-reload
# タイマーの起動と自動起動設定
sudo systemctl enable --now api-healthcheck.timer
# タイマー一覧と次回実行予定の確認
systemctl list-timers --all | grep api-healthcheck
3. ログの確認方法
systemdで実行された監視結果は journalctl で追跡可能です。
# 最新の実行ログをリアルタイムで監視
sudo journalctl -u api-healthcheck.service -f
# エラーログ(stderr)のみをフィルタリング抽出
sudo journalctl -u api-healthcheck.service -p err
【トラブルシューティングと落とし穴】
パイプライン処理と set -eo pipefail の罠
落とし穴: curl ... | jq ... と直接パイプで繋ぐと、curl がコネクションエラー(502等)で失敗しても、jq がそれをパースしようとして、意図しないエラーハンドリング経路を辿る可能性があります。
対策: 本スクリプトのように、curl の出力を一度一時ファイル (mktemp) に書き出し、HTTPステータスコードを単独で評価してから jq パースへと進める「フェイルファスト設計」が最も安全です。
APIキーなどの環境変数リーク
落とし穴: curl -H "Authorization: Bearer $API_TOKEN" を含むスクリプトを set -x(デバッグモード)で実行すると、ログに認証トークンが平文で出力されてしまいます。
対策: set -x は本番運用時には絶対に使用せず、トークン等の秘匿情報は systemd の EnvironmentFile= オプション等を用いて、権限管理されたファイル(例: chmod 600)から読み込ませます。
一時ファイル(/tmp)のクリーンアップ漏れ
落とし穴: スクリプトの途中でシグナル(SIGINT, SIGTERM)を受信したり、エラーで即時終了した場合に一時ファイルが残存し、ディスク容量を圧迫します。
対策: スクリプト先頭で trap 'rm -f "${BODY_FILE}"' EXIT を宣言することで、正常・異常終了を問わず確実にファイルを削除します。また、systemd側の PrivateTmp=true 設定により、OS全体の共有ディレクトリを汚染するリスクを物理的に排除しています。
【まとめ】
運用の冪等性と堅牢性を維持するために、以下の3つのポイントを徹底してください。
冪等性の担保: 監視スクリプトは何度実行してもシステムの状態を破壊しないよう、副作用のないGETリクエストをベースに設計する。
適切なリトライ戦略: 一時的なネットワークエラーによるアラートの誤検知を防ぐため、curl の --retry オプションによる段階的バックオフを組み込む。
最小権限の法則: 監視サービスは root ではなく、書き込み権限の限定された nobody ユーザーなどの非特権アカウントで実行する。
ライセンス:本記事のテキスト/コードは特記なき限り
CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。
コメント