<p><meta/>
{
“status”: “ready”,
“persona”: “SRE / DevOps Engineer”,
“tech_stack”: [“Bash”, “curl”, “jq”, “systemd”],
“design_patterns”: [“Idempotency”, “Atomic File Updates”, “State Machine”],
“reliability_features”: [“set -euo pipefail”, “trap cleanup”, “backoff retry”]
}
</p>
<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">外部API連携の冪等性を担保する:状態管理とアトミック更新を用いた堅牢なCLIスクリプト</h1>
<p>【導入と前提】
外部APIへのデータ同期において、二重送信を防止しつつ、異常終了時からの再開を可能にする冪等な同期スクリプトを構築します。</p>
<ul class="wp-block-list">
<li><p><strong>実行環境</strong>: Linux (Ubuntu 22.04 / RHEL 9 等), Bash 4.4+, jq 1.6+, curl 7.68+</p></li>
<li><p><strong>解決する課題</strong>: ネットワーク中断による「どこまで処理したか」の不整合、および中途半端な状態ファイルの破損防止。</p></li>
</ul>
<p>【処理フローと設計】</p>
<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["アトミックな移動 mv"]
G --> H["終了"]
E -->|失敗| I["リトライ/エラー終了"]
</pre></div>
<p>このフローでは、処理済みIDを保存する <code>state.json</code> を参照し、未送信データのみを抽出(フィルタリング)します。更新時は一時ファイルを作成してから <code>mv</code> コマンドで上書きすることで、ファイル書き込み中のクラッシュによる破損を防ぎます(アトミック更新)。</p>
<p>【実装:堅牢な自動化スクリプト】</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/usr/bin/env bash
# --- 安全のためのシェルオプション ---
# -e: エラー発生時に即座に終了
# -u: 未定義変数の参照時にエラー
# -o pipefail: パイプラインの途中のエラーを拾う
set -euo pipefail
# --- 定数定義 ---
readonly STATE_FILE="processed_ids.json"
readonly INPUT_DATA="input_data.json"
readonly API_ENDPOINT="https://api.example.com/v1/sync"
readonly LOG_FILE="/var/log/api_sync.log"
# --- 後処理設定 ---
# 終了時に一時ファイルを確実に削除
TEMP_STATE=$(mktemp)
trap 'rm -f "$TEMP_STATE"' EXIT
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $1" | tee -a "$LOG_FILE"
}
# --- 前提チェック ---
if [[ ! -f "$INPUT_DATA" ]]; then
log "ERROR: Input data not found."
exit 1
fi
# 状態ファイルがなければ初期化
if [[ ! -f "$STATE_FILE" ]]; then
echo "[]" > "$STATE_FILE"
fi
# --- 1. 未処理データの抽出 ---
# jq --argjson を使い、state.jsonに含まれないIDのみを抽出
log "Filtering unprocessed items..."
TARGET_ITEMS=$(jq -c --argjson state "$(cat "$STATE_FILE")" \
'.[] | select(.id as $id | $state | contains([$id]) | not)' "$INPUT_DATA")
if [[ -z "$TARGET_ITEMS" ]]; then
log "No new data to process."
exit 0
fi
# --- 2. メインループ処理 ---
for item in $TARGET_ITEMS; do
item_id=$(echo "$item" | jq -r '.id')
log "Processing ID: $item_id"
# curl オプション解説:
# -s: 進捗非表示, -S: エラー表示, -L: リダイレクト追従
# --retry: 一時的エラー時のリトライ回数
# -w: レスポンスコードの取得
RESPONSE=$(curl -sSL -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "$item" \
--retry 3 \
--retry-delay 2 \
"$API_ENDPOINT")
HTTP_CODE="${RESPONSE: -3}"
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
log "Successfully processed: $item_id"
# --- 3. アトミックな状態更新 ---
# 処理済みIDをJSON配列に追加し、一時ファイルに保存
jq --arg id "$item_id" '. += [$id]' "$STATE_FILE" > "$TEMP_STATE"
# mv は同一ファイルシステム内であればアトミックな操作
mv "$TEMP_STATE" "$STATE_FILE"
else
log "ERROR: Failed to process $item_id (HTTP: $HTTP_CODE)"
exit 1
fi
done
log "Sync completed successfully."
</pre>
</div>
<h3 class="wp-block-heading">補足:Systemd Timerによる定期実行</h3>
<p>cronよりも高度なログ管理(journald)と依存関係制御が可能です。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /etc/systemd/system/api-sync.service
[Unit]
Description=Daily API Sync Script
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/api-sync.sh
User=deploy-user
# 環境変数の保護
EnvironmentFile=/etc/default/api-sync
# /etc/systemd/system/api-sync.timer
[Unit]
Description=Runs API Sync every hour
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
</pre>
</div>
<p>【検証と運用】</p>
<ol class="wp-block-list">
<li><p><strong>正常系確認</strong>:</p>
<ul>
<li><p>スクリプト実行後、<code>processed_ids.json</code> にIDが蓄積されているか確認。</p></li>
<li><p><code>tail -f /var/log/api_sync.log</code> で進行状況を監視。</p></li>
</ul></li>
<li><p><strong>異常系確認</strong>:</p>
<ul>
<li><code>curl</code> 実行中に通信を遮断し、<code>state.json</code> が壊れていないこと、および再開時に未送信データから始まることを確認。</li>
</ul></li>
<li><p><strong>ログ確認</strong>:</p>
<ul>
<li><code>journalctl -u api-sync.service</code> で実行履歴と標準エラー出力を参照。</li>
</ul></li>
</ol>
<p>【トラブルシューティングと落とし穴】</p>
<ul class="wp-block-list">
<li><p><strong>権限問題</strong>: <code>STATE_FILE</code> への書き込み権限がないと <code>mv</code> で失敗します。実行ユーザー(例: <code>deploy-user</code>)がディレクトリの所有権を持っているか確認してください。</p></li>
<li><p><strong>ファイルロック</strong>: 複数のインスタンスが同時に動く可能性がある場合、<code>flock</code> を使用して二重起動を防止してください。</p></li>
<li><p><strong>状態ファイルの肥大化</strong>: 数万件のIDを保持すると <code>jq</code> のルックアップ(<code>contains</code>)が遅くなります。その場合は <code>Set</code> 構造への変換や、データベース(SQLite等)への移行を検討してください。</p></li>
<li><p><strong>環境変数の漏洩</strong>: APIキーなどはスクリプトに直書きせず、<code>EnvironmentFile</code> や <code>export</code> 経由で渡し、<code>ps</code> コマンド等で見えないように配慮してください。</p></li>
</ul>
<p>【まとめ】
運用の冪等性を維持するための3つのポイント:</p>
<ol class="wp-block-list">
<li><p><strong>判定(Load State)</strong>: 送信前に「完了済みリスト」と突き合わせ、重複実行を排除する。</p></li>
<li><p><strong>アトミック更新(Atomic Move)</strong>: <code>></code> による直接上書きを避け、<code>mv</code> を用いて状態ファイルの整合性を守る。</p></li>
<li><p><strong>例外処理(Trap & Retry)</strong>: <code>trap</code> による一時ファイル清掃と <code>curl --retry</code> による一時的な通信障害への耐性を持たせる。</p></li>
</ol>
{
“status”: “ready”,
“persona”: “SRE / DevOps Engineer”,
“tech_stack”: [“Bash”, “curl”, “jq”, “systemd”],
“design_patterns”: [“Idempotency”, “Atomic File Updates”, “State Machine”],
“reliability_features”: [“set -euo pipefail”, “trap cleanup”, “backoff retry”]
}
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
外部API連携の冪等性を担保する:状態管理とアトミック更新を用いた堅牢なCLIスクリプト
【導入と前提】
外部APIへのデータ同期において、二重送信を防止しつつ、異常終了時からの再開を可能にする冪等な同期スクリプトを構築します。
実行環境: Linux (Ubuntu 22.04 / RHEL 9 等), Bash 4.4+, jq 1.6+, curl 7.68+
解決する課題: ネットワーク中断による「どこまで処理したか」の不整合、および中途半端な状態ファイルの破損防止。
【処理フローと設計】
graph TD
A["開始"] --> B{"状態ファイル確認"}
B -->|存在| C["未処理データを抽出"]
B -->|不在| D["全データを対象に設定"]
C --> E["APIリクエスト実行"]
D --> E
E -->|成功| F["一時状態ファイルを作成"]
F --> G["アトミックな移動 mv"]
G --> H["終了"]
E -->|失敗| I["リトライ/エラー終了"]
このフローでは、処理済みIDを保存する state.json を参照し、未送信データのみを抽出(フィルタリング)します。更新時は一時ファイルを作成してから mv コマンドで上書きすることで、ファイル書き込み中のクラッシュによる破損を防ぎます(アトミック更新)。
【実装:堅牢な自動化スクリプト】
#!/usr/bin/env bash
# --- 安全のためのシェルオプション ---
# -e: エラー発生時に即座に終了
# -u: 未定義変数の参照時にエラー
# -o pipefail: パイプラインの途中のエラーを拾う
set -euo pipefail
# --- 定数定義 ---
readonly STATE_FILE="processed_ids.json"
readonly INPUT_DATA="input_data.json"
readonly API_ENDPOINT="https://api.example.com/v1/sync"
readonly LOG_FILE="/var/log/api_sync.log"
# --- 後処理設定 ---
# 終了時に一時ファイルを確実に削除
TEMP_STATE=$(mktemp)
trap 'rm -f "$TEMP_STATE"' EXIT
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $1" | tee -a "$LOG_FILE"
}
# --- 前提チェック ---
if [[ ! -f "$INPUT_DATA" ]]; then
log "ERROR: Input data not found."
exit 1
fi
# 状態ファイルがなければ初期化
if [[ ! -f "$STATE_FILE" ]]; then
echo "[]" > "$STATE_FILE"
fi
# --- 1. 未処理データの抽出 ---
# jq --argjson を使い、state.jsonに含まれないIDのみを抽出
log "Filtering unprocessed items..."
TARGET_ITEMS=$(jq -c --argjson state "$(cat "$STATE_FILE")" \
'.[] | select(.id as $id | $state | contains([$id]) | not)' "$INPUT_DATA")
if [[ -z "$TARGET_ITEMS" ]]; then
log "No new data to process."
exit 0
fi
# --- 2. メインループ処理 ---
for item in $TARGET_ITEMS; do
item_id=$(echo "$item" | jq -r '.id')
log "Processing ID: $item_id"
# curl オプション解説:
# -s: 進捗非表示, -S: エラー表示, -L: リダイレクト追従
# --retry: 一時的エラー時のリトライ回数
# -w: レスポンスコードの取得
RESPONSE=$(curl -sSL -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "$item" \
--retry 3 \
--retry-delay 2 \
"$API_ENDPOINT")
HTTP_CODE="${RESPONSE: -3}"
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
log "Successfully processed: $item_id"
# --- 3. アトミックな状態更新 ---
# 処理済みIDをJSON配列に追加し、一時ファイルに保存
jq --arg id "$item_id" '. += [$id]' "$STATE_FILE" > "$TEMP_STATE"
# mv は同一ファイルシステム内であればアトミックな操作
mv "$TEMP_STATE" "$STATE_FILE"
else
log "ERROR: Failed to process $item_id (HTTP: $HTTP_CODE)"
exit 1
fi
done
log "Sync completed successfully."
補足:Systemd Timerによる定期実行
cronよりも高度なログ管理(journald)と依存関係制御が可能です。
# /etc/systemd/system/api-sync.service
[Unit]
Description=Daily API Sync Script
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/api-sync.sh
User=deploy-user
# 環境変数の保護
EnvironmentFile=/etc/default/api-sync
# /etc/systemd/system/api-sync.timer
[Unit]
Description=Runs API Sync every hour
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
【検証と運用】
正常系確認:
異常系確認:
curl 実行中に通信を遮断し、state.json が壊れていないこと、および再開時に未送信データから始まることを確認。
ログ確認:
journalctl -u api-sync.service で実行履歴と標準エラー出力を参照。
【トラブルシューティングと落とし穴】
権限問題: STATE_FILE への書き込み権限がないと mv で失敗します。実行ユーザー(例: deploy-user)がディレクトリの所有権を持っているか確認してください。
ファイルロック: 複数のインスタンスが同時に動く可能性がある場合、flock を使用して二重起動を防止してください。
状態ファイルの肥大化: 数万件のIDを保持すると jq のルックアップ(contains)が遅くなります。その場合は Set 構造への変換や、データベース(SQLite等)への移行を検討してください。
環境変数の漏洩: APIキーなどはスクリプトに直書きせず、EnvironmentFile や export 経由で渡し、ps コマンド等で見えないように配慮してください。
【まとめ】
運用の冪等性を維持するための3つのポイント:
判定(Load State): 送信前に「完了済みリスト」と突き合わせ、重複実行を排除する。
アトミック更新(Atomic Move): > による直接上書きを避け、mv を用いて状態ファイルの整合性を守る。
例外処理(Trap & Retry): trap による一時ファイル清掃と curl --retry による一時的な通信障害への耐性を持たせる。
コメント