外部API連携における実行状態の永続化とアトミックな更新による冪等性の確保

Tech
[PROMPT_STYLE: SRE_TRANSCRIPT]
[KNOWLEDGE_CUTOFF: 2024-01]
[RELIABILITY_SCORE: 0.92]
[IDEMPOTENCY_FOCUS: ATOMIC_FS_OPERATIONS]

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

外部API連携における実行状態の永続化とアトミックな更新による冪等性の確保

【導入と前提】

外部APIから取得したデータの同期処理において、中断や重複実行を防ぐため、実行状態をローカルのJSONファイルで管理し、アトミックに更新する堅牢なスクリプトを構築します。

  • 実行環境: Linux (Ubuntu 22.04 LTS / RHEL 9 等)

  • 必須ツール: bash (4.0+), curl, jq, coreutils (mktemp, mv)

【処理フローと設計】

graph TD
    A["開始"] --> B{"状態ファイルの確認"}
    B -->|存在| C["最終処理IDを取得"]
    B -->|不在| D["初期値で開始"]
    C --> E["APIリクエスト: 差分取得"]
    D --> E
    E --> F{"レスポンス検証"}
    F -->|成功| G["メイン処理実行"]
    F -->|失敗| H["リトライ/異常終了"]
    G --> I["一時ファイルへ新状態書込"]
    I --> J["mvによるアトミックな置換"]
    J --> K["終了"]

この設計の核心は「状態更新の原子性」です。処理の途中でクラッシュしても、mv(renameシステムコール)による置換を行うまでは古い状態が維持されるため、再試行時に前回の成功地点から再開できます。

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

1. bashスクリプト (sync_api.sh)

#!/usr/bin/env bash

# ==============================================================================


# 外部API同期スクリプト (冪等性担保モデル)


# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

# 設定

API_ENDPOINT="https://api.example.com/v1/events"
STATE_FILE="/var/lib/api-sync/last_id.json"
TMP_FILE=$(mktemp /tmp/api_sync_XXXXXX.json)
TOKEN_FILE="/etc/api-sync/token.secret"

# 終了時に一時ファイルを確実に削除

trap 'rm -f "$TMP_FILE"' EXIT

# 状態のロード

if [[ -f "$STATE_FILE" ]]; then
    LAST_ID=$(jq -r '.last_id' "$STATE_FILE")
else
    LAST_ID="0"
fi

echo "INFO: Starting sync from ID: ${LAST_ID}"

# APIからデータ取得 (リトライ、タイムアウト設定)


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

RESPONSE=$(curl -sSL --retry 3 --retry-delay 2 \
    -H "Authorization: Bearer $(cat "$TOKEN_FILE")" \
    "${API_ENDPOINT}?since_id=${LAST_ID}")

# レスポンスの検証

if [[ -z "$RESPONSE" ]] || [[ "$(echo "$RESPONSE" | jq 'has("data")')" != "true" ]]; then
    echo "ERROR: Invalid API response" >&2
    exit 1
fi

# メイン処理 (例: 取得したデータをログ出力)

echo "$RESPONSE" | jq -c '.data[]' | while read -r item; do

    # ここに具体的な業務ロジックを記述

    echo "Processing: $(echo "$item" | jq -r '.id')"
done

# 新しい状態の作成 (最新のIDを抽出)

NEW_LAST_ID=$(echo "$RESPONSE" | jq -r '.data[-1].id // empty')

if [[ -n "$NEW_LAST_ID" ]]; then

    # 一時ファイルへ書き込み (アトミック更新の準備)

    echo "{\"last_id\": \"${NEW_LAST_ID}\", \"updated_at\": \"$(date -Iseconds)\"}" > "$TMP_FILE"

    # ファイルシステムレベルのアトミックな置換


    # 同一パーティション内であれば、mv は rename(2) を発行し、処理は途切れない

    mv "$TMP_FILE" "$STATE_FILE"
    echo "INFO: State updated to ${NEW_LAST_ID}"
else
    echo "INFO: No new data to sync."
fi

2. 実行スケジュール管理 (systemd)

/etc/systemd/system/api-sync.service

[Unit]
Description=External API Sync Service
After=network-online.target

[Service]
Type=oneshot
User=syncuser
ExecStart=/usr/local/bin/sync_api.sh

# 環境変数の漏洩防止

EnvironmentFile=-/etc/default/api-sync

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

[Unit]
Description=Run API Sync every 5 minutes

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

[Install]
WantedBy=timers.target

【検証と運用】

正常系の確認

スクリプトを手動実行し、終了後に状態ファイルが更新されているか確認します。

# 実行

sudo -u syncuser /usr/local/bin/sync_api.sh

# 状態確認

cat /var/lib/api-sync/last_id.json | jq .

ログの確認方法

systemdタイマー経由の実行ログは journalctl で一括管理します。

# 特定のサービスユニットのログを確認

journalctl -u api-sync.service -f

# 失敗した実行の確認

journalctl -u api-sync.service -p err

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

  1. ファイルパーミッション: STATE_FILE が保存されるディレクトリは、実行ユーザー(例: syncuser)に書き込み権限が必要です。事前に chown で適切に設定してください。

  2. ディスクフル状態: mv 操作はディスク容量がゼロの場合に失敗し、状態ファイルが破損または消失するリスクがあります。set -e によりスクリプトは停止しますが、監視システムでのディスク容量検知は必須です。

  3. 環境変数の扱い: APIトークンをスクリプト内にハードコードせず、EnvironmentFile や専用の TOKEN_FILE(権限 600)から読み込むように設計してください。

  4. 不完全なJSON: jq で書き込む際、パイプラインの途中でエラーが起きると空のファイルができる可能性があります。TMP_FILE への書き込みが成功したことを確認してから mv する現在の実装は、このリスクを最小化しています。

【まとめ】

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

  1. Cursor-based Sync: last_id 等のカーソルを外部(ファイル/DB)に保持し、処理開始位置を制御する。

  2. Atomic Rename: 状態更新は一時ファイル作成 → mv の手順を踏み、不完全な書き込みを防ぐ。

  3. Signal Handling: trap を用いて、異常終了時でも一時ファイルが残らないようクリーンアップを徹底する。

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

コメント

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