0Pricing
DevOps Bootcamp · レッスン

冪等なスクリプトと指数バックオフによるリトライ

安全に再実行できる処理を設計し、不安定な外部呼び出しに指数バックオフを追加します。

「冪等なスクリプトと指数バックオフによるリトライ」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。

冪等性とは何か、なぜ重要なのか

冪等性とは、同じ操作を複数回実行しても、1回だけ実行した場合と同じ結果になる性質です。Bash スクリプトでは、スクリプトがクラッシュしたり、ネットワークが切断されたり、人が誤って再実行したりするため、これは非常に重要です。

  • 冪等でないスクリプトでユーザーを2回作成すると、失敗したりデータが重複したりする可能性があります。
  • 冪等なスクリプトは、まず「これはすでに存在するか」を確認します。
  • 冪等なスクリプトは、cron ジョブ、CI パイプライン、リトライループで安全に使用できます。

基本原則は、実行する前に確認することです。破壊的な操作や作成操作には、必ず事前条件のチェックを設けてください。

ファイルとディレクトリの作成を保護する

最も一般的な冪等性のパターンは、リソースを作成する前に、それがすでに存在するかどうかを確認することです。Bash では、これを簡潔なワンライナーで記述できます。

  • [ -d dir ] — ディレクトリが存在する場合に真
  • [ -f file ] — 通常ファイルが存在する場合に真
  • mkdir -p — ディレクトリが存在しない場合にのみ作成(組み込みの冪等性)

利用できる場合は、手動のチェックよりも -p や --no-clobber のような組み込みフラグを優先してください。これらはアトミックで、競合状態が発生しないよう設計されています。

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

冪等なユーザー・グループ管理

ユーザーやグループの追加などのシステム管理タスクは冪等でなければなりません。同じマシンでスクリプトを再実行しても、エラーになったり重複が作成されたりしないようにします。

  • id username は、ユーザーが存在する場合に 0 を返します。
  • getent group groupname は、グループの存在を確認します。
  • 各操作をガードで囲み、スクリプトを安全に再実行できるようにします。

このパターンは、Ansible のような構成管理ツールの基盤です。すべてのタスクが、ガードされた冪等な操作になっています。

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

ロックファイルを使用して同時実行を防ぐ

冪等なスクリプトでも、2つのインスタンスが同時に実行されると問題を引き起こす可能性があります。ロックファイルによって、一度に1つのインスタンスだけが実行されるようにできます。

  • 起動時にロックファイルを作成し、終了時に削除します。
  • trap を使用して、スクリプトが中断された場合でもロックをクリーンアップします。
  • 単一のパスに対する mkdir は、ほとんどの Linux ファイルシステムでアトミックに動作するため、ロックには touch より安全です。

ロックがないと、時間のかかる cron ジョブと手動の再実行が衝突し、共有状態が破損する可能性があります。

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

状態ファイルで完了したステップを追跡する

複数のステップで構成されるスクリプト(マイグレーション、インストール、プロビジョニングなど)では、状態ファイルを使用して、どのステップがすでに完了したかを追跡できます。各ステップは実行前に状態ファイルを確認し、完了したら状態ファイルに書き込みます。

  • 低コストで移植性が高く、データベースは必要ありません。
  • スクリプトが失敗しても、中断した場所から再開できます。
  • 状態は /var/lib/myapp/ や ~/.myapp/state/ のような予測可能な場所に保存します。

このパターンは、apt、cloud-init、データベースマイグレーションフレームワークなどの主要なツールで使用されています。

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

リトライロジックの概要

HTTP リクエスト、DNS ルックアップ、クラウド API 呼び出しなどの外部呼び出しは、本質的に信頼できません。1回の失敗でスクリプト全体を中止すべきではありません。リトライロジックは、失敗したコマンドを自動的に再試行します。

  • 単純なリトライ: コマンドが成功するまで、N 回ループします。
  • 無限ループを避けるため、必ず最大リトライ回数を設定します。
  • 失敗を診断できるよう、試行するたびにログを記録します。

最も簡単なリトライ関数は、任意のコマンドをラップし、一定の遅延を挟んで固定回数まで再試行します。これは多くの用途には十分ですが、高負荷時に重大な欠点があります。次に説明します。

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

Thundering Herd 問題

障害の後に多数のクライアントが同時にリトライすると、thundering herd(大量のクライアントが一斉に再試行する現象)が発生します。すべてのクライアントが同じ固定間隔で再試行するため、まったく同じ瞬間にサーバーへ殺到し、復旧が不可能になります。

  • 100 個のスクリプトが5秒ごとにリトライすると、5秒ごとに100件のリクエストが同時に発生します。
  • サーバーはすでに負荷に苦しんでおり、同期した負荷によって状況はさらに悪化します。
  • 解決策は指数バックオフです。失敗するたびに待機時間を2倍にします。
  • リトライのタイミングをクライアント間でずらすため、ジッター(ランダムな揺らぎ)を追加します。

ジッター付きの指数バックオフは、AWS SDK、Google Cloud クライアント、主要な分散システムで採用されている業界標準です。

指数バックオフの実装

指数バックオフでは、失敗するたびに待機時間を指数関数的に増やします(1秒、2秒、4秒、8秒、16秒…)。これにより、リモートシステムが復旧する時間を確保しながら、全体の負荷を軽減できます。

計算式:delay = base * (2 ^ attempt)

  • 待機時間が際限なく増えないように、上限(最大遅延時間)を設定します。
  • 調整するパラメーター:base_delay、max_delay、max_attempts

この関数は再利用できます。任意のコマンドを引数として渡してください。

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

バックオフへのジッターの追加

ジッターは、バックオフの待機時間にランダム性を加えます。指数バックオフを使用していても、すべてのクライアントが同時に開始すると、再試行のタイミングが揃ってしまいます。ジッターはこの同期を崩します。

一般的なジッター戦略は次の2つです。

  • Full jitter:sleep random(0, cap) — 待機時間を最大限に分散し、ピーク時の負荷を最小限に抑えます。
  • Equal jitter:sleep cap/2 + random(0, cap/2) — 最低待機時間を保証し、直ちに大量のリクエストを送ることを防ぎます。

Bashでは、$RANDOM(0~32767)を使って乱数を生成します。剰余演算で遅延時間の範囲に合わせてスケーリングしてください。

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

冪等性と再試行を実際のワークフローで組み合わせる

本番環境のスクリプトでは、冪等性と再試行ロジックを組み合わせて使用します。一般的なデプロイワークフローでは、次のような処理を行います。

  1. ロックを取得する(同時実行を防止)
  2. 状態ファイルを確認する(完了済みのステップをスキップ)
  3. 外部呼び出し(ダウンロード、API、DNS)にバックオフ付き再試行を使用する
  4. 成功を確認してから、ステップを完了済みとして記録する
  5. trapでロックを解放する

この組み合わせにより、クラッシュ、タイムアウト、手動中断の後など、どの時点からでも安全にスクリプトを再実行できるようになります。その際、システムを壊れた状態のまま残しません。

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

再試行できないエラーへの対処

すべてのエラーを再試行すべきとは限りません。404 Not Foundや403 Forbiddenを再試行しても、人の手による対応なしには成功しないため無駄になります。再試行ロジックでは、次のエラーを区別する必要があります。

  • 一時的なエラー — ネットワークタイムアウト、503 Service Unavailable、DNS障害 → 再試行
  • 永続的なエラー — 401 Unauthorized、404 Not Found、入力値の不正 → 直ちに失敗

curlでは、HTTPステータスコードを確認し、4xxレスポンスに対する再試行をスキップします。--write-out '%{http_code}'を使用すると、本文とは別にステータスを取得できます。

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

理解度チェック:バックオフ戦略の選択

あるデプロイスクリプトが、S3バケットからリリース成果物をダウンロードしています。最近発生した障害では、一時的なS3の停止により、80個のパイプラインインスタンスがすべて同時に失敗しました。10秒後にS3が復旧すると、80個のインスタンスがまったく同じタイミングで再試行したため、さらなる過負荷が発生し、停止時間が3分延長されました。

今後このような大量同時再試行による連鎖障害を防ぐには、どの再試行戦略が最も効果的でしょうか。

振り返り:冪等性とバックオフ付き再試行

このレッスンでは、安全に再実行でき、一時的な障害に耐えられる Bashスクリプトの設計方法を学びました。

冪等性のパターン:

  • 存在確認([ -f ]、[ -d ]、id、getent)で各操作を保護します。
  • 利用できる場合は、mkdir -pなど、組み込みの冪等性フラグを使用します。
  • ロックファイル(アトミックなmkdir)を使って同時実行を防ぎます。
  • 状態ファイル(ステップごとのマーカーファイル)を使って、失敗後に処理を再開できるようにします。

バックオフ付き再試行のパターン:

  • 必ず最大試行回数を設定し、無限に再試行しないようにします。
  • 指数バックオフを使用し、失敗するたびに遅延時間を2倍にします。
  • ジッター(ランダム性)を加えて、大量同時再試行の同期を防ぎます。
  • 一時的なエラー(再試行)と永続的なエラー(即時失敗)を区別します。

これらのパターンを組み合わせることで、安全で可観測性があり、自動復旧できる、本番環境に適したスクリプトを作成できます。

よくある質問

「冪等なスクリプトと指数バックオフによるリトライ」レッスンは無料ですか?

はい。「冪等なスクリプトと指数バックオフによるリトライ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。

「冪等なスクリプトと指数バックオフによるリトライ」で何を学びますか?

安全に再実行できる処理を設計し、不安定な外部呼び出しに指数バックオフを追加します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

DevOps Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「冪等なスクリプトと指数バックオフによるリトライ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このDevOps Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. set -euo pipefailによる厳格モード
  2. クリーンアップとシグナル処理のためのtrapハンドラー
  3. 安全な一時ファイルとロックディレクトリ
  4. 冪等なスクリプトと指数バックオフによるリトライ
← DevOps Bootcampに戻る