<p><meta/>
{
“style_prompt_version”: “1.0”,
“category”: “SRE_Automation_Design”,
“task”: “API_Idempotency_CLI_Design”,
“technical_focus”: [“Bash”, “jq”, “Atomic_File_Update”, “curl_retry”]
}
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">外部API連携の冪等性を保証するCLIスクリプト:状態管理とアトミック更新の自動化設計</h1>
<h3 class="wp-block-heading">【導入と前提】</h3>
<p>外部APIから取得した差分のみを安全に処理するため、処理済みIDを状態ファイルで管理し、アトミックな更新により不整合を防止する堅牢なシェルスクリプトを構築します。</p>
<ul class="wp-block-list">
<li><p><strong>OS</strong>: Linux (Ubuntu 22.04+ / RHEL 8+ 推奨)</p></li>
<li><p><strong>依存ツール</strong>: <code>curl</code> (7.60+), <code>jq</code> (1.6+)</p></li>
<li><p><strong>権限</strong>: 状態保存ディレクトリへの書込権限</p></li>
</ul>
<h3 class="wp-block-heading">【処理フローと設計】</h3>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["開始"] --> B{"状態ファイルの確認"}
B -->|存在しない| C["初期状態の作成"]
B -->|存在する| D["前回同期情報の読み込み"]
C --> E["APIリクエスト実行"]
D --> E
E --> F{"新規データの抽出"}
F -->|差分なし| G["終了"]
F -->|差分あり| H["個別タスク実行"]
H --> I["一時状態ファイルの生成"]
I --> J["アトミックな移動 mv で更新"]
J --> G
</pre></div>
<p>このフローでは、処理途中のクラッシュによる「二重処理」や「状態の不整合」を防ぐため、作業用の一時ファイルを最後に <code>mv</code> コマンドで上書きする設計を採用しています。</p>
<h3 class="wp-block-heading">【実装:堅牢な自動化スクリプト】</h3>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# ==============================================================================
# API Idempotent Sync Script
# Description: 外部APIからデータを取得し、未処理分のみを処理して状態を保存する
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# --- 設定項目 ---
API_URL="https://api.example.com/v1/resource"
API_TOKEN="${API_KEY:-'default_token'}"
STATE_FILE="/var/lib/my-app/last_processed_id.json"
TMP_STATE_FILE="${STATE_FILE}.tmp"
LOG_TAG="api-sync-script"
# --- トラップ設定(一時ファイルの掃除) ---
trap 'rm -f "${TMP_STATE_FILE}"' EXIT
# --- ロギング関数 ---
log() {
logger -t "${LOG_TAG}" "$1"
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $1"
}
# --- 前提確認 ---
if [[ ! -f "$STATE_FILE" ]]; then
log "Initial run: Creating state file."
echo '{"last_id": 0}' > "$STATE_FILE"
fi
# --- 1. APIからのデータ取得 ---
# -s: 進捗非表示, -L: リダイレクト追従, -f: HTTPエラー時にエラーコードを返す
# --retry: 通信失敗時の再試行回数
log "Fetching data from API..."
RESPONSE=$(curl -sLf --retry 3 --retry-delay 2 \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Accept: application/json" \
"${API_URL}")
# --- 2. 状態の読み込みと差分抽出 ---
LAST_ID=$(jq -r '.last_id' "$STATE_FILE")
# 現在のIDより大きい(未処理)アイテムを抽出
NEW_ITEMS=$(echo "$RESPONSE" | jq -c --arg last_id "$LAST_ID" '.items[] | select(.id > ($last_id | tonumber))')
if [[ -z "$NEW_ITEMS" ]]; then
log "No new items to process."
exit 0
fi
# --- 3. 処理の実行とアトミックな更新 ---
NEW_LAST_ID=$LAST_ID
for item in $NEW_ITEMS; do
ITEM_ID=$(echo "$item" | jq -r '.id')
# ここにメインの業務ロジックを記述
log "Processing item ID: ${ITEM_ID}"
# 成功した最新のIDを保持
NEW_LAST_ID=$ITEM_ID
done
# 新しい状態を一時ファイルに書き出し
jq -n --arg id "$NEW_LAST_ID" '{"last_id": ($id | tonumber)}' > "$TMP_STATE_FILE"
# アトミックな更新(mvは同一ファイルシステム内であればアトミックに動作する)
mv "$TMP_STATE_FILE" "$STATE_FILE"
log "Sync completed. Last processed ID: ${NEW_LAST_ID}"
</pre>
</div>
<h4 class="wp-block-heading">systemdによる定期実行(Timer)の設定例</h4>
<p><code>/etc/systemd/system/api-sync.timer</code></p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run API Sync every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
</pre>
</div>
<h3 class="wp-block-heading">【検証と運用】</h3>
<h4 class="wp-block-heading">正常系の確認</h4>
<p>スクリプトを手動実行し、状態ファイルが更新されるか確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 実行
sudo ./api-sync.sh
# 状態ファイルの確認
cat /var/lib/my-app/last_processed_id.json
</pre>
</div>
<h4 class="wp-block-heading">ログの確認</h4>
<p><code>logger</code> を使用しているため、標準出力だけでなくシステムログからも追跡可能です。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 直近のログを確認
journalctl -t api-sync-script -n 20
</pre>
</div>
<h3 class="wp-block-heading">【トラブルシューティングと落とし穴】</h3>
<ol class="wp-block-list">
<li><p><strong>アトミック性の崩壊</strong>:
<code>mv</code> コマンドがアトミックに動作するのは、移動元と移動先が同一ファイルシステム(パーティション)内の場合のみです。<code>/tmp</code>(tmpfs)から <code>/var/lib</code>(ext4/xfs)へ移動する場合はコピーが発生するため、状態ファイルと同じディレクトリに一時ファイルを作成するようにしています。</p></li>
<li><p><strong>APIキーの露出</strong>:
環境変数 <code>API_KEY</code> を直接スクリプトに書かず、systemdの <code>EnvironmentFile</code> やシークレット管理ツールから読み込むようにしてください。</p></li>
<li><p><strong>jqのエラー処理</strong>:
APIが不正なJSONを返した場合、<code>set -e</code> によりスクリプトは即座に停止します。これは「不正な状態で状態を更新しない」ために意図的な設計ですが、原因調査のために <code>curl</code> の出力を一時保存するデバッグモードの検討も有効です。</p></li>
</ol>
<h3 class="wp-block-heading">【まとめ】</h3>
<p>運用の冪等性を維持するための3つのポイント:</p>
<ol class="wp-block-list">
<li><p><strong>状態の外部化</strong>: どのリソースまで処理したかを、スクリプトの外部(JSON等)で永続化する。</p></li>
<li><p><strong>アトミック・ライト</strong>: 新しい状態を直接上書きせず、一時ファイルを作成してから <code>mv</code> で置換する。</p></li>
<li><p><strong>ガード節とリトライ</strong>: <code>curl</code> のリトライオプションと <code>set -euo pipefail</code> で、予期せぬ失敗時に中途半端な更新を防ぐ。</p></li>
</ol>
{
“style_prompt_version”: “1.0”,
“category”: “SRE_Automation_Design”,
“task”: “API_Idempotency_CLI_Design”,
“technical_focus”: [“Bash”, “jq”, “Atomic_File_Update”, “curl_retry”]
}
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
外部API連携の冪等性を保証するCLIスクリプト:状態管理とアトミック更新の自動化設計
【導入と前提】
外部APIから取得した差分のみを安全に処理するため、処理済みIDを状態ファイルで管理し、アトミックな更新により不整合を防止する堅牢なシェルスクリプトを構築します。
【処理フローと設計】
graph TD
A["開始"] --> B{"状態ファイルの確認"}
B -->|存在しない| C["初期状態の作成"]
B -->|存在する| D["前回同期情報の読み込み"]
C --> E["APIリクエスト実行"]
D --> E
E --> F{"新規データの抽出"}
F -->|差分なし| G["終了"]
F -->|差分あり| H["個別タスク実行"]
H --> I["一時状態ファイルの生成"]
I --> J["アトミックな移動 mv で更新"]
J --> G
このフローでは、処理途中のクラッシュによる「二重処理」や「状態の不整合」を防ぐため、作業用の一時ファイルを最後に mv コマンドで上書きする設計を採用しています。
【実装:堅牢な自動化スクリプト】
#!/bin/bash
# ==============================================================================
# API Idempotent Sync Script
# Description: 外部APIからデータを取得し、未処理分のみを処理して状態を保存する
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# --- 設定項目 ---
API_URL="https://api.example.com/v1/resource"
API_TOKEN="${API_KEY:-'default_token'}"
STATE_FILE="/var/lib/my-app/last_processed_id.json"
TMP_STATE_FILE="${STATE_FILE}.tmp"
LOG_TAG="api-sync-script"
# --- トラップ設定(一時ファイルの掃除) ---
trap 'rm -f "${TMP_STATE_FILE}"' EXIT
# --- ロギング関数 ---
log() {
logger -t "${LOG_TAG}" "$1"
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $1"
}
# --- 前提確認 ---
if [[ ! -f "$STATE_FILE" ]]; then
log "Initial run: Creating state file."
echo '{"last_id": 0}' > "$STATE_FILE"
fi
# --- 1. APIからのデータ取得 ---
# -s: 進捗非表示, -L: リダイレクト追従, -f: HTTPエラー時にエラーコードを返す
# --retry: 通信失敗時の再試行回数
log "Fetching data from API..."
RESPONSE=$(curl -sLf --retry 3 --retry-delay 2 \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Accept: application/json" \
"${API_URL}")
# --- 2. 状態の読み込みと差分抽出 ---
LAST_ID=$(jq -r '.last_id' "$STATE_FILE")
# 現在のIDより大きい(未処理)アイテムを抽出
NEW_ITEMS=$(echo "$RESPONSE" | jq -c --arg last_id "$LAST_ID" '.items[] | select(.id > ($last_id | tonumber))')
if [[ -z "$NEW_ITEMS" ]]; then
log "No new items to process."
exit 0
fi
# --- 3. 処理の実行とアトミックな更新 ---
NEW_LAST_ID=$LAST_ID
for item in $NEW_ITEMS; do
ITEM_ID=$(echo "$item" | jq -r '.id')
# ここにメインの業務ロジックを記述
log "Processing item ID: ${ITEM_ID}"
# 成功した最新のIDを保持
NEW_LAST_ID=$ITEM_ID
done
# 新しい状態を一時ファイルに書き出し
jq -n --arg id "$NEW_LAST_ID" '{"last_id": ($id | tonumber)}' > "$TMP_STATE_FILE"
# アトミックな更新(mvは同一ファイルシステム内であればアトミックに動作する)
mv "$TMP_STATE_FILE" "$STATE_FILE"
log "Sync completed. Last processed ID: ${NEW_LAST_ID}"
systemdによる定期実行(Timer)の設定例
/etc/systemd/system/api-sync.timer
[Unit]
Description=Run API Sync every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
【検証と運用】
正常系の確認
スクリプトを手動実行し、状態ファイルが更新されるか確認します。
# 実行
sudo ./api-sync.sh
# 状態ファイルの確認
cat /var/lib/my-app/last_processed_id.json
ログの確認
logger を使用しているため、標準出力だけでなくシステムログからも追跡可能です。
# 直近のログを確認
journalctl -t api-sync-script -n 20
【トラブルシューティングと落とし穴】
アトミック性の崩壊:
mv コマンドがアトミックに動作するのは、移動元と移動先が同一ファイルシステム(パーティション)内の場合のみです。/tmp(tmpfs)から /var/lib(ext4/xfs)へ移動する場合はコピーが発生するため、状態ファイルと同じディレクトリに一時ファイルを作成するようにしています。
APIキーの露出:
環境変数 API_KEY を直接スクリプトに書かず、systemdの EnvironmentFile やシークレット管理ツールから読み込むようにしてください。
jqのエラー処理:
APIが不正なJSONを返した場合、set -e によりスクリプトは即座に停止します。これは「不正な状態で状態を更新しない」ために意図的な設計ですが、原因調査のために curl の出力を一時保存するデバッグモードの検討も有効です。
【まとめ】
運用の冪等性を維持するための3つのポイント:
状態の外部化: どのリソースまで処理したかを、スクリプトの外部(JSON等)で永続化する。
アトミック・ライト: 新しい状態を直接上書きせず、一時ファイルを作成してから mv で置換する。
ガード節とリトライ: curl のリトライオプションと set -euo pipefail で、予期せぬ失敗時に中途半端な更新を防ぐ。
コメント