ユーザーの目的:外部APIとの連携処理をCLIスクリプトで自動化し、重複実行(多重決済や二重登録など)を防ぐために「状態ファイル」を用いた冪等性管理を設計・実装すること。
トーン&マナー:実戦経験豊富なSREエンジニアが執筆した、信頼性が高く具体的で、本番環境に耐えうる技術ドキュメント。
技術的正確性:
set -euo pipefailによるエラーハンドリング。trapを用いた確実な一時ファイル・ロックファイルのクリーンアップ。mvコマンドを用いたアトミック(原子性)な状態ファイル更新(書き込み中のプロセス競合やクラッシュから状態破損を防ぐため、同一ファイルシステム内でのリネーム)。curlによる段階的なリトライ設計(リトライ回数、リレー、タイムアウト)。systemdによる定期実行(Timer)とシステムロギング(stdout/stderrからjournaldへのフォワード)。 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
外部API連携における状態管理の冪等性を担保するCLIスクリプト自動化設計
【導入と前提】
本スクリプトは、外部APIとのデータ同期において、状態ファイルのアトミック更新と冪等性キーの管理により、二重実行を防止する堅牢なCLI自動化機構です。
実行環境・前提条件
稼働OS: Linux (RHEL 8/9, Ubuntu 22.04 LTS 以降推奨)
必須ツール:
curl(バージョン 7.71.0 以上: リトライ制御の最適化)jq(バージョン 1.6 以上: 高度なJSONストリーム処理)uuidgenまたはshasum(冪等性キーの動的生成)
【処理フローと設計】
graph TD
A["Start: 実行開始"] --> B["ロック取得 & 状態ファイル読込"]
B --> C{"既存状態の解析"}
C -- "未完了/新規" --> D["冪等性キーの生成/ロード"]
C -- "完了済" --> E["処理スキップ & 終了"]
D --> F["一時作業ファイルへの中間書き込み"]
F --> G["APIリクエスト送信: curl"]
G --> H{"レスポンス判定"}
H -- "200/201 OK" --> I["状態JSONの更新 & 一時保存"]
H -- "一時エラー/タイムアウト" --> J["リトライ実施"]
J --> G
H -- "致命的エラー (4xx/500)" --> K["エラーハンドリング & 異常終了"]
I --> L["アトミックなmv置換による状態確定"]
L --> M["ロック解放 & 正常終了"]
K --> N["ロック解放 & 異常終了"]
設計上の技術的アプローチ
排他制御(ロック機構): 多重起動を防止するため、
flockコマンドを利用したファイル記述子ベースの排他制御を実施。アトミック(原子性)な状態更新: 稼働中のディスクフルや強制終了による状態ファイルの破損を防ぐため、状態の書き出しは常に同一ディレクトリ内の一時ファイル(
.tmp)に行い、最後にmvコマンドを用いて一瞬で置換します。冪等性の担保: APIヘッダーに
Idempotency-Keyを付与し、APIサーバー側での二重処理を物理的に防止します。
【実装:堅牢な自動化スクリプト】
1. 状態管理同期スクリプト (/usr/local/bin/sync_state.sh)
#!/usr/bin/env bash
# ==============================================================================
# 外部API連携 状態管理&冪等性担保CLIスクリプト
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# --- 構成定義 ---
readonly APP_DIR="/var/lib/api-syncer"
readonly STATE_FILE="${APP_DIR}/state.json"
readonly LOCK_FILE="${APP_DIR}/sync.lock"
readonly API_URL="https://api.example.com/v1/transactions"
readonly TIMEOUT_SEC=15
readonly MAX_RETRIES=3
# 環境変数(API認証キー等)の検証
if [[ -z "${API_TOKEN:-}" ]]; then
echo "[ERROR] API_TOKEN が環境変数に設定されていません。" >&2
exit 1
fi
# 実行環境のセットアップ
mkdir -p "${APP_DIR}"
touch "${STATE_FILE}"
# 状態ファイルが空の場合は初期JSONを出力
if [[ ! -s "${STATE_FILE}" ]]; then
echo '{"last_status": "INIT", "idempotency_key": "", "updated_at": ""}' > "${STATE_FILE}"
fi
# --- 排他制御 (ファイルロック) ---
exec 9>"${LOCK_FILE}"
if ! flock -n 9; then
echo "[WARNING] 他の同期プロセスが現在実行中です。処理をスキップします。" >&2
exit 0
fi
# 一時ファイルのクリーンアップ関数
cleanup() {
local exit_code=$?
# スクリプト終了時に一時ファイルを確実に削除する
if [[ -f "${TEMP_STATE:-}" ]]; then
rm -f "${TEMP_STATE}"
fi
# ロック解除
flock -u 9
exit "${exit_code}"
}
trap cleanup EXIT INT TERM
# --- メインロジック ---
# 現在の状態をロード
CURRENT_STATUS=$(jq -r '.last_status' "${STATE_FILE}")
SAVED_KEY=$(jq -r '.idempotency_key' "${STATE_FILE}")
# ステータス判定(前回完了している場合はスキップ)
if [[ "${CURRENT_STATUS}" == "SUCCESS" ]]; then
echo "[INFO] 前回のトランザクションは既に完了しています。新規実行をスキップします。"
exit 0
fi
# 冪等性キーの設定(未処理からのリトライ時は前回のキーを再利用)
if [[ -z "${SAVED_KEY}" ]]; then
IDEMPOTENCY_KEY=$(uuidgen)
echo "[INFO] 新規冪等性キーを発行しました: ${IDEMPOTENCY_KEY}"
else
IDEMPOTENCY_KEY="${SAVED_KEY}"
echo "[INFO] 既存の冪等性キーを再利用してリトライします: ${IDEMPOTENCY_KEY}"
fi
# 同一ファイルシステム内の一時ファイル(アトミック更新の担保)
TEMP_STATE=$(mktemp "${STATE_FILE}.XXXXXX")
# 実行中状態を一時的に保存(クラッシュ時の回復用)
jq --arg key "${IDEMPOTENCY_KEY}" \
--arg time "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'.last_status = "RUNNING" | .idempotency_key = $key | .updated_at = $time' \
"${STATE_FILE}" > "${TEMP_STATE}"
# 一時ファイルを本番の状態ファイルへアトミックに置換
mv "${TEMP_STATE}" "${STATE_FILE}"
# API送信用ペイロードの構築
PAYLOAD=$(jq -n \
--arg id "${IDEMPOTENCY_KEY}" \
'{"transaction_id": $id, "amount": 1000, "currency": "JPY"}')
echo "[INFO] APIリクエストを送信中..."
# curlによるHTTP通信の実行
# -s: 進捗バー非表示 (Silent)
# -S: エラー時のみエラーメッセージ出力
# -f: HTTPステータス4xx/500番台で異常終了ステータスを返す
# --retry: 通信エラー時の一時的リトライ回数
# --max-time: 最大タイムアウト設定
HTTP_RESPONSE=$(curl -sS -f \
-X POST \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d "${PAYLOAD}" \
--max-time "${TIMEOUT_SEC}" \
--retry "${MAX_RETRIES}" \
--retry-delay 2 \
--retry-connrefused \
"${API_URL}")
# APIレスポンス解析
RESPONSE_STATUS=$(echo "${HTTP_RESPONSE}" | jq -r '.status // empty')
if [[ "${RESPONSE_STATUS}" == "completed" ]]; then
echo "[SUCCESS] API処理が正常に完了しました。"
# 成功ステータスで状態をアトミックに書き換え
TEMP_STATE=$(mktemp "${STATE_FILE}.XXXXXX")
jq --arg time "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'.last_status = "SUCCESS" | .updated_at = $time' \
"${STATE_FILE}" > "${TEMP_STATE}"
mv "${TEMP_STATE}" "${STATE_FILE}"
else
echo "[ERROR] API応答のステータスが不正です: ${HTTP_RESPONSE}" >&2
exit 1
fi
2. 永続化のための systemd ユニット設定
定期実行( cron の代替)および実行環境をカプセル化するために、systemdタイマーを配置します。
① サービス定義ファイル (/etc/systemd/system/api-syncer.service)
[Unit] Description=External API Idempotent Sync Service After=network-online.target Wants=network-online.target [Service] Type=oneshot User=api-worker Group=api-worker EnvironmentFile=/etc/api-syncer/env ExecStart=/usr/local/bin/sync_state.sh StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target
② タイマー定義ファイル (/etc/systemd/system/api-syncer.timer)
[Unit] Description=Run API Sync Service every 5 minutes [Timer] OnBootSec=2min OnUnitActiveSec=5min AccuracySec=1s [Install] WantedBy=timers.target
【検証と運用】
1. 正常系の動作確認
手動でスクリプトを実行し、状態ファイルの変化とログを確認します。
# 環境変数をロードして手動テスト実行 export API_TOKEN="test_token_xyz123" sudo -u api-worker /usr/local/bin/sync_state.sh
期待される初期実行後の state.json の出力:
{
"last_status": "SUCCESS",
"idempotency_key": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"updated_at": "2023-10-27T08:30:00Z"
}
この状態で再度スクリプトを実行した場合に、「新規実行をスキップします」と表示されることを確認します。
2. systemd タイマーの有効化とログ確認
# タイマー設定の有効化と即時反映 sudo systemctl daemon-reload sudo systemctl enable --now api-syncer.timer # タイマーの稼働状況一覧確認 systemctl list-timers --all | grep api-syncer # systemd 経由の実行ログ監視 journalctl -u api-syncer.service -f
【トラブルシューティングと落とし穴】
① 状態ファイル更新時の「パーミッションエラー」と「デバイス境界不整合」
事象:
mv: cannot move '...' to '...': Device or resource busyや権限エラー。原因: 一時ファイルを生成するデフォルトの
/tmpディレクトリと、/var/lib/api-syncerが異なるファイルシステム(マウントポイント)上に存在する場合、mvコマンドがOSレイヤーで「物理的なコピー&削除」にフォールバックし、アトミックな更新ではなくなります。対策:
mktemp実行時に、必ず状態ファイルと同一のディレクトリ内に一時ファイルを生成するようにパスを指定します(本スクリプトの実装例:mktemp "${STATE_FILE}.XXXXXX")。
② APIトークン等、秘匿環境変数の漏洩防止
事象:
ps auxやプロセスの環境変数ダンプから認証キーが漏洩する。対策:
systemdサービス経由で実行する際は、コマンド引数にトークンを含めず、パーミッション0600に制限した環境変数ファイル(/etc/api-syncer/env)から読み込ませます。
# 環境変数ファイルの適切なパーミッション設定 sudo chown api-worker:api-worker /etc/api-syncer/env sudo chmod 0600 /etc/api-syncer/env
【まとめ】
運用の冪等性を維持するための3大重要ポイント:
書き込み完了の原子性(Atomicity): データをファイルへ直接追記・編集せず、一時ファイルへの完全書き出し後に
mvで上書きする。冪等性キーの一貫性保持: 送信失敗やタイムアウト発生時、同一データパケットに対して「同一のUUID」をリトライ時にも一貫して使い回す。
シグナルハンドリングの徹底:
trapによる不意のスクリプト中断(SIGINT/SIGTERM)時におけるゴミファイルの自動クリーンアップ。

