外部API連携の冪等性を担保する:状態ファイルを用いたアトミックな同期スクリプトの設計

Tech

{ “style_prompt”: “SRE_Expert_Manual”, “priority”: “Idempotency_and_Atomicity”, “context”: “External_API_Sync_Automation”, “tools”: [“bash”, “jq”, “curl”, “systemd”, “flock”] } 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

外部API連携の冪等性を担保する:状態ファイルを用いたアトミックな同期スクリプトの設計

【導入と前提】

API呼び出しの重複を避け、実行状態を永続化することで、障害発生時も安全に再試行可能な堅牢な同期フローを構築します。

  • OS: GNU/Linux (Debian/Ubuntu/RHEL)

  • 必須ツール: curl (7.34.0+), jq (1.5+), flock (util-linux)

  • 権限: 状態保存ディレクトリへの書き込み権限

【処理フローと設計】

graph TD
    A["開始"] --> B{"排他ロック取得"}
    B -- 失敗 --> C["終了: 二重起動防止"]
    B -- 成功 --> D["前回の状態読み込み"]
    D --> E["外部APIリクエスト"]
    E --> F{"レスポンス検証"}
    F -- 成功 --> G["一時ファイルへ状態書き込み"]
    G --> H["アトミックな置換: mv"]
    H --> I["ロック解除/終了"]
    F -- 失敗 --> J["リトライまたはエラー終了"]

この設計の核心は「アトミックなファイル更新」にあります。直接状態ファイルを上書きするのではなく、一時ファイルを作成してから mv コマンドで置換することで、書き込み中のクラッシュによるファイル破損(壊れたJSONなど)を防止します。

【実装:堅牢な自動化スクリプト】

#!/usr/bin/env bash

# ---------------------------------------------------------


# 安全のためのシェルオプション


# -e: エラー発生時に即座に終了


# -u: 未定義変数の参照をエラーとする


# -o pipefail: パイプ内のエラーを伝播させる


# ---------------------------------------------------------

set -euo pipefail

# --- 設定項目 ---

STATE_DIR="/var/lib/my-sync-app"
STATE_FILE="${STATE_DIR}/last_sync.json"
LOCK_FILE="/run/lock/my-sync-app.lock"
API_ENDPOINT="https://api.example.com/v1/resource"
API_TOKEN_FILE="/etc/my-sync-app/token" # 環境変数ではなくファイルからの読み込みを推奨

# 一時ファイルの生成

TMP_STATE_FILE=$(mktemp "${STATE_FILE}.XXXXXX")

# クリーンアップ処理

trap 'rm -f "${TMP_STATE_FILE}"' EXIT

# 1. 排他ロックの取得 (二重起動の防止)

exec 9>"${LOCK_FILE}"
if ! flock -n 9; then
    echo "Error: Another instance is running." >&2
    exit 1
fi

echo "Starting sync process..."

# 2. 前回の状態を確認 (jq で安全にパース)

LAST_ID=$(jq -r '.last_id // "0"' "${STATE_FILE}" 2>/dev/null || echo "0")

# 3. APIリクエスト (リトライとタイムアウトを設定)


# -s: 進捗非表示, -S: エラー表示, -L: リダイレクト追従, --retry: 一時的エラー時の再試行

RESPONSE=$(curl -sSL --retry 3 --connect-timeout 10 \
    -H "Authorization: Bearer $(cat "${API_TOKEN_FILE}")" \
    -H "Content-Type: application/json" \
    "${API_ENDPOINT}?since_id=${LAST_ID}")

# 4. レスポンスの検証と一時保存


# APIが成功し、有効なJSONを返しているか確認

if ! echo "${RESPONSE}" | jq -e '.data' > /dev/null; then
    echo "Error: Invalid API response." >&2
    exit 1
fi

# 5. アトミックな更新


# 新しい状態を一時ファイルに書き出し

NEW_LAST_ID=$(echo "${RESPONSE}" | jq -r '.data[-1].id')
echo "{\"last_id\": \"${NEW_LAST_ID}\", \"updated_at\": \"$(date -Iseconds)\"}" > "${TMP_STATE_FILE}"

# 同期(fsync相当)を確実にし、mv でアトミックに置換

sync "${TMP_STATE_FILE}"
mv "${TMP_STATE_FILE}" "${STATE_FILE}"

echo "Sync completed. New ID: ${NEW_LAST_ID}"

systemd タイマーによる定期実行例

/etc/systemd/system/my-sync.timer

[Unit]
Description=Run API Sync every 5 minutes

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target

【検証と運用】

  • 正常系の確認: cat /var/lib/my-sync-app/last_sync.json を実行し、last_id が最新のAPIレスポンスと一致しているか確認します。

  • ログの監視: systemd経由で実行している場合、以下のコマンドで実行履歴を確認できます。 journalctl -u my-sync.service

  • 擬似的な異常系テスト: APIリクエスト中にスクリプトを kill -9 し、last_sync.json が破損していない(古い状態のまま維持されている)ことを確認します。

【トラブルシューティングと落とし穴】

  1. 権限問題: STATE_DIRroot 所有の場合、一般ユーザーで実行すると mktemp で失敗します。chown で適切な実行ユーザーに権限を付与してください。

  2. ディスクフル: mv 自体はアトミックですが、ディスクに空きがないと一時ファイルの書き込みが不完全になります。set -e によりその時点で停止しますが、監視システムでの検知が必要です。

  3. 環境変数の露出: APIトークンを export してスクリプトに渡すと、ps コマンド等で他ユーザーから見えるリスクがあります。前述の例のように、ファイル(chmod 600)から読み込む手法が安全です。

  4. 不完全なJSON: APIが空のレスポンスや200以外のステータスコードを返した場合に備え、jq -e による構造チェックを必ず挟んでください。

【まとめ:運用の冪等性を維持するための3つのポイント】

  1. 排他制御 (flock): 同一リソースに対する並列処理を物理的に遮断する。

  2. アトミック更新 (mv): 状態ファイルの「書き込み途中」という中間状態をシステムに存在させない。

  3. 検証後の永続化: APIレスポンスが期待通りであることを jq 等で確認してから、初めて状態を更新する。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました