<p><meta/>
{
“style”: “SRE_PRACTITIONER”,
“priority”: “HIGH_RELIABILITY”,
“elements”: [“Idempotency”, “AtomicUpdate”, “StateManagement”],
“tools”: [“bash”, “jq”, “curl”, “systemd”]
}
</p>
<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">外部API連携の冪等性を保証する:状態管理とアトミック更新による堅牢なCLIスクリプト</h1>
<p>【導入と前提】
外部APIとのデータ同期において、実行中断や二重送信を防ぎ、状態ファイルを用いて処理の継続性を確保する自動化手法を解説します。</p>
<ul class="wp-block-list">
<li><strong>前提環境</strong>: Linux (Ubuntu/Debian等), <code>bash</code>, <code>jq</code> (1.6以上), <code>curl</code></li>
</ul>
<p>【処理フローと設計】</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["開始: 入力JSON読み込み"] --> B{"状態ファイル確認"}
B -->|未処理IDのみ抽出| C["外部APIリクエスト"]
C -->|成功| D["一時状態ファイル作成"]
D -->|アトミック更新| E["正式な状態ファイルへmv"]
E --> F{"全データ完了?"}
F -->|No| B
F -->|Yes| G["終了: クリーンアップ"]
C -->|失敗/リトライ限界| H["エラーログ出力 & 終了"]
</pre></div>
<p>この設計の核心は、APIリクエストが成功した直後に「どのIDまで処理したか」をローカルの状態ファイル(JSON)に記録し、ファイル更新を <code>mv</code> コマンドでアトミック(不可分)に行う点にあります。これにより、スクリプトが途中で強制終了しても、再実行時に重複処理を回避できます。</p>
<p>【実装:堅牢な自動化スクリプト】</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/usr/bin/env bash
# --- 制御設定 ---
set -euo pipefail
# -e: エラー発生時に即座に終了
# -u: 未定義変数の参照時にエラー
# -o pipefail: パイプライン内のエラーを適切に捕捉
# --- 定数・変数 ---
STATE_FILE="./process_state.json"
INPUT_DATA="./data.json"
API_URL="https://api.example.com/v1/resource"
TMP_STATE=$(mktemp) # アトミック更新用の一時ファイル
# 一時ファイルのクリーンアップを保証
trap 'rm -f "$TMP_STATE"' EXIT
# 状態ファイルの初期化(存在しない場合)
if [[ ! -f "$STATE_FILE" ]]; then
echo '{"processed_ids": []}' > "$STATE_FILE"
fi
# メインロジック
main() {
# 処理対象IDのリストを取得(jqでフィルタリング)
local ids=$(jq -r '.items[].id' "$INPUT_DATA")
for id in $ids; do
# 冪等性のチェック: 既に処理済みか確認
if jq -e ".processed_ids | contains([\"$id\"])" "$STATE_FILE" > /dev/null; then
echo "Skip: ID $id is already processed."
continue
fi
echo "Processing ID: $id ..."
# APIリクエスト (リトライとタイムアウトを設定)
# -s: 進捗表示を抑制
# -S: エラー時はメッセージを表示
# -f: HTTPエラー(4xx/5xx)時に異常終了させる
# -L: リダイレクトに追従
# --retry: 通信失敗時のリトライ回数
local response
response=$(curl -sSfL --retry 3 \
-H "Content-Type: application/json" \
-X POST \
-d "{\"id\": \"$id\"}" \
"$API_URL")
# 状態のアトミック更新
# 1. 現在の状態に新IDを追加した内容を一時ファイルへ書き出し
jq ".processed_ids += [\"$id\"]" "$STATE_FILE" > "$TMP_STATE"
# 2. mvコマンドにより、ファイルシステムレベルでアトミックに置換
mv "$TMP_STATE" "$STATE_FILE"
echo "Success: ID $id marked as processed."
done
}
main "$@"
</pre>
</div>
<h3 class="wp-block-heading">systemdによる定時実行(タイマー設定例)</h3>
<p>スクリプトを定期実行する場合、<code>systemd</code> ユニットファイルで管理することで、ログの自動ローテーションやリソース制限が可能になります。</p>
<p><strong><code>/etc/systemd/system/api-sync.service</code></strong></p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Idempotent API Sync Script
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sync_script.sh
User=api-worker
Group=api-worker
# 環境変数の読み込み
EnvironmentFile=/etc/default/api-sync
# セキュリティ設定
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
</pre>
</div>
<p>【検証と運用】</p>
<ol class="wp-block-list">
<li><p><strong>正常系の確認</strong>:
スクリプトを2回連続で実行し、2回目は「Skip: ID …」というメッセージが表示され、APIリクエストが発生しないことを確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">tail -f /var/log/syslog | grep api-sync # systemd経由の場合
# または直接確認
cat ./process_state.json
</pre>
</div></li>
<li><p><strong>エラー時のログ確認</strong>:
<code>systemd</code> を利用している場合は、<code>journalctl</code> で詳細なスタックトレースを確認できます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u api-sync.service --since "1 hour ago"
</pre>
</div></li>
</ol>
<p>【トラブルシューティングと落とし穴】</p>
<ul class="wp-block-list">
<li><p><strong>ファイルロックの欠如</strong>:
複数のインスタンスが同時にこのスクリプトを起動する可能性がある場合、<code>flock</code> コマンドを使用して排他制御を行ってください。
<code>exec 9>/var/lock/api-sync.lock; flock -n 9 || exit 1</code></p></li>
<li><p><strong>環境変数の露出</strong>:
APIキーなどをスクリプトに直書きせず、<code>EnvironmentFile</code> または <code>export</code> 済みの環境変数から読み込み、権限(chmod 600)を厳格に管理してください。</p></li>
<li><p><strong>ディスクフル</strong>:
<code>mv</code> 操作はアトミックですが、ディスクに空き容量がない場合、一時ファイルの作成に失敗します。<code>set -e</code> によりスクリプトは停止しますが、監視ツールでの検知が必要です。</p></li>
</ul>
<p>【まとめ】</p>
<ol class="wp-block-list">
<li><p><strong>状態の外部化</strong>: 処理済みリストをファイルに永続化し、起動時に必ず参照する。</p></li>
<li><p><strong>アトミックな書き込み</strong>: <code>jq</code> で加工したデータを一度別名で保存し、<code>mv</code> で上書きすることでファイル破損を防ぐ。</p></li>
<li><p><strong>防衛的プログラミング</strong>: <code>set -euo pipefail</code> と <code>curl --retry</code> を組み合わせ、予期せぬエラーや一時的なネットワーク瞬断を許容する。</p></li>
</ol>
{
“style”: “SRE_PRACTITIONER”,
“priority”: “HIGH_RELIABILITY”,
“elements”: [“Idempotency”, “AtomicUpdate”, “StateManagement”],
“tools”: [“bash”, “jq”, “curl”, “systemd”]
}
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
外部API連携の冪等性を保証する:状態管理とアトミック更新による堅牢なCLIスクリプト
【導入と前提】
外部APIとのデータ同期において、実行中断や二重送信を防ぎ、状態ファイルを用いて処理の継続性を確保する自動化手法を解説します。
- 前提環境: Linux (Ubuntu/Debian等),
bash, jq (1.6以上), curl
【処理フローと設計】
graph TD
A["開始: 入力JSON読み込み"] --> B{"状態ファイル確認"}
B -->|未処理IDのみ抽出| C["外部APIリクエスト"]
C -->|成功| D["一時状態ファイル作成"]
D -->|アトミック更新| E["正式な状態ファイルへmv"]
E --> F{"全データ完了?"}
F -->|No| B
F -->|Yes| G["終了: クリーンアップ"]
C -->|失敗/リトライ限界| H["エラーログ出力 & 終了"]
この設計の核心は、APIリクエストが成功した直後に「どのIDまで処理したか」をローカルの状態ファイル(JSON)に記録し、ファイル更新を mv コマンドでアトミック(不可分)に行う点にあります。これにより、スクリプトが途中で強制終了しても、再実行時に重複処理を回避できます。
【実装:堅牢な自動化スクリプト】
#!/usr/bin/env bash
# --- 制御設定 ---
set -euo pipefail
# -e: エラー発生時に即座に終了
# -u: 未定義変数の参照時にエラー
# -o pipefail: パイプライン内のエラーを適切に捕捉
# --- 定数・変数 ---
STATE_FILE="./process_state.json"
INPUT_DATA="./data.json"
API_URL="https://api.example.com/v1/resource"
TMP_STATE=$(mktemp) # アトミック更新用の一時ファイル
# 一時ファイルのクリーンアップを保証
trap 'rm -f "$TMP_STATE"' EXIT
# 状態ファイルの初期化(存在しない場合)
if [[ ! -f "$STATE_FILE" ]]; then
echo '{"processed_ids": []}' > "$STATE_FILE"
fi
# メインロジック
main() {
# 処理対象IDのリストを取得(jqでフィルタリング)
local ids=$(jq -r '.items[].id' "$INPUT_DATA")
for id in $ids; do
# 冪等性のチェック: 既に処理済みか確認
if jq -e ".processed_ids | contains([\"$id\"])" "$STATE_FILE" > /dev/null; then
echo "Skip: ID $id is already processed."
continue
fi
echo "Processing ID: $id ..."
# APIリクエスト (リトライとタイムアウトを設定)
# -s: 進捗表示を抑制
# -S: エラー時はメッセージを表示
# -f: HTTPエラー(4xx/5xx)時に異常終了させる
# -L: リダイレクトに追従
# --retry: 通信失敗時のリトライ回数
local response
response=$(curl -sSfL --retry 3 \
-H "Content-Type: application/json" \
-X POST \
-d "{\"id\": \"$id\"}" \
"$API_URL")
# 状態のアトミック更新
# 1. 現在の状態に新IDを追加した内容を一時ファイルへ書き出し
jq ".processed_ids += [\"$id\"]" "$STATE_FILE" > "$TMP_STATE"
# 2. mvコマンドにより、ファイルシステムレベルでアトミックに置換
mv "$TMP_STATE" "$STATE_FILE"
echo "Success: ID $id marked as processed."
done
}
main "$@"
systemdによる定時実行(タイマー設定例)
スクリプトを定期実行する場合、systemd ユニットファイルで管理することで、ログの自動ローテーションやリソース制限が可能になります。
/etc/systemd/system/api-sync.service
[Unit]
Description=Idempotent API Sync Script
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sync_script.sh
User=api-worker
Group=api-worker
# 環境変数の読み込み
EnvironmentFile=/etc/default/api-sync
# セキュリティ設定
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
【検証と運用】
正常系の確認:
スクリプトを2回連続で実行し、2回目は「Skip: ID …」というメッセージが表示され、APIリクエストが発生しないことを確認します。
tail -f /var/log/syslog | grep api-sync # systemd経由の場合
# または直接確認
cat ./process_state.json
エラー時のログ確認:
systemd を利用している場合は、journalctl で詳細なスタックトレースを確認できます。
journalctl -u api-sync.service --since "1 hour ago"
【トラブルシューティングと落とし穴】
ファイルロックの欠如:
複数のインスタンスが同時にこのスクリプトを起動する可能性がある場合、flock コマンドを使用して排他制御を行ってください。
exec 9>/var/lock/api-sync.lock; flock -n 9 || exit 1
環境変数の露出:
APIキーなどをスクリプトに直書きせず、EnvironmentFile または export 済みの環境変数から読み込み、権限(chmod 600)を厳格に管理してください。
ディスクフル:
mv 操作はアトミックですが、ディスクに空き容量がない場合、一時ファイルの作成に失敗します。set -e によりスクリプトは停止しますが、監視ツールでの検知が必要です。
【まとめ】
状態の外部化: 処理済みリストをファイルに永続化し、起動時に必ず参照する。
アトミックな書き込み: jq で加工したデータを一度別名で保存し、mv で上書きすることでファイル破損を防ぐ。
防衛的プログラミング: set -euo pipefail と curl --retry を組み合わせ、予期せぬエラーや一時的なネットワーク瞬断を許容する。
ライセンス:本記事のテキスト/コードは特記なき限り
CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。
コメント