0Pricing
Linux Command Line & Bash Scripting Mastery · レッスン

ヘルスプローブ、準備完了ゲート、待機ループ

依存関係の待機ループと生存確認プローブを実装し、コンテナ化されたサービスを確実に起動できるようにします。

「ヘルスプローブ、準備完了ゲート、待機ループ」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

サービスは起動時になぜ失敗するのか

コンテナ化された環境では、サービスが単独で起動することはほとんどありません。Webアプリケーションには準備済みのデータベースが必要であり、マイクロサービスには利用可能なメッセージブローカーが必要です。また、ワーカーはジョブを処理する前にキャッシュが準備されている必要があります。

調整を行わないと、コンテナは並列に起動し、依存先が正常になる前に最初のリクエストが到着します。その結果、接続拒否エラー、Null Pointer Exception、破損した起動状態が発生し、手動での再起動が必要になります。

  • ライブネスプローブ — プロセスがまだ実行中で、デッドロックしていないかを確認します。
  • レディネスプローブ — サービスがトラフィックを受け付ける準備が整っているかを確認します。
  • 待機ループ — 依存先に到達できるようになるまでブロックする起動スクリプトです。

Kubernetesには組み込みのプローブ機構がありますが、initContainersやentrypoint.shラッパー、スタンドアロンのヘルスチェックを動かすシェルスクリプトはBashで記述します。これらを使いこなすことは、DevOpsにおける基本スキルです。

wait-forパターン

最も単純な依存先の待機ループは、一定間隔で対象をポーリングし、到達可能になるまで待機します。標準的な形では、nc(netcat)またはcurlを使ってTCPポートやHTTPエンドポイントをプローブするwhileループを使用します。

主な設計上の判断:

  • タイムアウト — 壊れた依存先によってPodが永久にハングしないよう、N秒後に打ち切ります。
  • バックオフ間隔 — 復旧中のサービスに過剰な負荷をかけないよう、プローブの間にスリープします。
  • 終了コード — タイムアウト時に1で終了し、コンテナを再起動する(またはinit containerを明示的に失敗させる)ようにします。

以下のスニペットは、メインプロセスを起動する前に、TCPポートが接続を受け付けるまで最大60秒待機します。

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

HOST="${DB_HOST:-postgres}"
PORT="${DB_PORT:-5432}"
TIMEOUT=60
INTERVAL=2
ELAPSED=0

echo "[wait-for] Waiting for ${HOST}:${PORT}..."

until nc -z "$HOST" "$PORT" 2>/dev/null; do
  if (( ELAPSED >= TIMEOUT )); then
    echo "[wait-for] Timed out after ${TIMEOUT}s waiting for ${HOST}:${PORT}" >&2
    exit 1
  fi
  echo "[wait-for] ${HOST}:${PORT} not ready — retrying in ${INTERVAL}s (${ELAPSED}s elapsed)"
  sleep "$INTERVAL"
  (( ELAPSED += INTERVAL ))
done

echo "[wait-for] ${HOST}:${PORT} is up — continuing"
exec "$@"

curlによるHTTPレディネスプローブ

TCP接続で分かるのはポートが開いていることだけであり、その背後にあるアプリケーションがリクエストを処理できる状態かどうかまでは分かりません。多くのサービスは、すべての内部サブシステムの初期化が完了した場合にのみHTTP 200を返す専用の/healthまたは/readyzエンドポイントを公開します。

curl --fail --silent --output /dev/nullを使ってHTTPレディネスエンドポイントをプローブします。--failフラグを指定すると、4xx/5xxレスポンスでcurlが終了コード22を返すため、リトライロジックを簡潔に制御できます。

知っておくべき重要なフラグ:

  • --fail — HTTPエラーをcurlのエラーとして扱います(ゼロ以外の終了ステータス)。
  • --silent — 進捗出力を抑制します。
  • --max-time N — リクエストごとのタイムアウトを秒単位で指定します。
  • --retry N --retry-delay S — curl独自のリトライ層です(単純なケースで便利です)。
#!/usr/bin/env bash
set -euo pipefail

HEALTH_URL="${HEALTH_URL:-http://localhost:8080/healthz}"
TIMEOUT=90
INTERVAL=3
ELAPSED=0

echo "[probe] Polling readiness at ${HEALTH_URL}"

until curl --fail --silent --output /dev/null \
           --max-time 2 "${HEALTH_URL}"; do
  if (( ELAPSED >= TIMEOUT )); then
    echo "[probe] Service not ready after ${TIMEOUT}s" >&2
    exit 1
  fi
  printf '[probe] Not ready yet (%ds elapsed)\n' "$ELAPSED"
  sleep "$INTERVAL"
  (( ELAPSED += INTERVAL ))
done

echo "[probe] Service is ready"
exec "$@"

待機ループにおける指数バックオフ

固定間隔のリトライループでは、復旧中のサービスに一定の頻度で負荷をかけ続けます。指数バックオフでは試行するたびに待機時間を倍増させるため、復旧中の負荷を軽減しながら、サービスがすぐに起動した場合にも速やかに収束できます。

標準的な式は次のとおりです:sleep_time = min(base * 2^attempt, max_sleep)。ジッター成分(ランダムな小数オフセット)を加えると、多数のコンテナが同時に再起動した際のthundering herd問題を防止できます。

このパターンは、wait-for-it、AWS SDKのリトライ、Kubernetesコントローラーの調整ループなど、プロダクション環境のツールで使用されています。

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

HOST="${HOST:-redis}"
PORT="${PORT:-6379}"
MAX_ATTEMPTS=8
BASE_SLEEP=1
MAX_SLEEP=30

for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
  if nc -z "$HOST" "$PORT" 2>/dev/null; then
    echo "[backoff] Connected to ${HOST}:${PORT} on attempt ${attempt}"
    exec "$@"
  fi

  # Exponential backoff with jitter
  raw=$(( BASE_SLEEP * (2 ** (attempt - 1)) ))
  capped=$(( raw < MAX_SLEEP ? raw : MAX_SLEEP ))
  jitter=$(( RANDOM % 3 ))
  sleep_time=$(( capped + jitter ))

  echo "[backoff] Attempt ${attempt}/${MAX_ATTEMPTS} failed — sleeping ${sleep_time}s"
  sleep "$sleep_time"
done

echo "[backoff] ${HOST}:${PORT} unreachable after ${MAX_ATTEMPTS} attempts" >&2
exit 1

複数の依存先をプローブする

実際のアプリケーションには、データベース、キャッシュ、メッセージブローカー、場合によっては外部APIなど、複数の依存先があります。これらを順番にプローブすると、起動時間を無駄にします。よりよい方法は、すべての依存先を並列にプローブし、すべてが成功するまで待機することです。

Bashの&演算子は各プローブをバックグラウンドで実行し、waitがそれらの終了コードを収集します。いずれかのプローブが失敗すると、エントリーポイントがゼロ以外のステータスで終了し、コンテナの再起動がトリガーされます。

重要なテクニック:$!でバックグラウンドプロセスのPIDを取得し、それらを明示的にwaitに渡すことで、個々の終了コードを確認できます。

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

wait_tcp() {
  local host="$1" port="$2" timeout="${3:-30}"
  local elapsed=0
  until nc -z "$host" "$port" 2>/dev/null; do
    (( elapsed >= timeout )) && { echo "TIMEOUT ${host}:${port}" >&2; return 1; }
    sleep 2; (( elapsed += 2 ))
  done
  echo "[ok] ${host}:${port}"
}

# Launch all probes in parallel
wait_tcp postgres 5432 60 &  PID_PG=$!
wait_tcp redis    6379 30 &  PID_RD=$!
wait_tcp rabbitmq 5672 45 &  PID_RQ=$!

# Collect results — fail fast if any probe failed
FAILED=0
for pid in $PID_PG $PID_RD $PID_RQ; do
  wait "$pid" || (( FAILED++ ))
done

if (( FAILED > 0 )); then
  echo "[entrypoint] ${FAILED} dependency probe(s) failed — aborting" >&2
  exit 1
fi

echo "[entrypoint] All dependencies ready"
exec "$@"

ライブネスとレディネス:プローブごとに異なるスクリプト

Kubernetesではライブネスプローブとレディネスプローブを区別しており、それぞれ異なる役割を持たせる必要があります。

  • ライブネスプローブ — プロセスがまだ生存していて、デッドロックしていないかを確認します。高速に実行し、内部状態だけを確認する必要があります(例:プロセスのPIDファイルが存在する、ローカルのヘルスエンドポイントが200を返す)。ライブネスプローブが失敗すると、コンテナが強制終了されて再起動します。
  • レディネスプローブ — このPodがトラフィックを受け取るべきかを確認します。下流の依存先を確認しても構いません。レディネスプローブが失敗すると、PodはServiceのロードバランサーから除外されますが、再起動はされません。

低速な依存先チェックをライブネスプローブに入れてはいけません。短時間のデータベース停止によって、すべてのアプリケーションPodが誤って再起動され、問題が悪化する可能性があります。

#!/usr/bin/env bash
# liveness.sh — fast local-only check
# Used in: livenessProbe.exec.command
set -euo pipefail

PID_FILE="/var/run/app/app.pid"
HEALTH_URL="http://127.0.0.1:8080/internal/live"

# Check 1: process is running
[[ -f "$PID_FILE" ]] || { echo "PID file missing" >&2; exit 1; }
kill -0 "$(cat "$PID_FILE")" 2>/dev/null || { echo "Process dead" >&2; exit 1; }

# Check 2: local endpoint responds (2s timeout — never block)
curl --fail --silent --max-time 2 --output /dev/null "$HEALTH_URL" || {
  echo "Liveness endpoint unresponsive" >&2
  exit 1
}

echo "live"
exit 0

Startup ProbesとinitContainers

Kubernetesには、3つ目のプローブタイプとしてstartupProbeがあります。これは一度成功するまでライブネスプローブやレディネスプローブの代わりに実行されるため、起動に時間のかかるアプリケーション(JVMのウォームアップやデータベースのマイグレーションなど)でも、誤ったライブネス失敗を発生させずに初期化する時間を確保できます。

依存先の待機には、エントリーポイントスクリプトよりもinitContainersのほうが適していることがよくあります。init containerはアプリコンテナの起動前に実行され、Kubernetesがリトライと再起動のロジックを自動的に処理します。init containerのイメージに必要なのはsh、nc、またはcurlだけです。最小構成のbusyboxやalpineイメージを使用できます。

PodマニフェストでのinitContainer仕様の例:

# kubernetes/pod-with-init.yaml (illustrative — not runnable as bash)
# initContainers run sequentially before app containers
initContainers:
  - name: wait-for-postgres
    image: busybox:1.36
    command:
      - sh
      - -c
      - |
        set -e
        echo 'Waiting for postgres...'
        until nc -z postgres 5432; do
          echo 'postgres not ready — sleeping 2s'
          sleep 2
        done
        echo 'postgres is up'

  - name: run-migrations
    image: myapp:latest
    command: ['python', 'manage.py', 'migrate', '--noinput']
    envFrom:
      - secretRef:
          name: app-secrets

JSON出力を行うヘルスチェックスクリプト

プロダクションシステムでは、複数のサブシステムのヘルスステータスを集約し、構造化されたJSONペイロードとして公開することがよくあります。これは、ロードバランサー、オーケストレーター、監視ダッシュボードによって利用されます。

Bashのヘルスチェックスクリプトでは、printfやjqを使ってJSON出力を直接構築できます。終了コードは引き続き自動化を制御し、JSON本文はオペレーターや監視システム向けに使用します。

一般的な規約では、正常な場合は{"status":"ok"}とともにHTTP 200を返し、異常な場合は{"status":"degraded", "checks":{...}}とともにHTTP 503を返します。以下のスクリプトは、socatのような軽量HTTPラッパーから提供するか、Kubernetesのexecプローブから直接呼び出すことを想定しています。

#!/usr/bin/env bash
# health_check.sh — composite health with JSON output
set -uo pipefail

check_postgres() {
  pg_isready -h "${DB_HOST:-postgres}" -p "${DB_PORT:-5432}" \
             -U "${DB_USER:-app}" -t 2 &>/dev/null
}

check_redis() {
  redis-cli -h "${REDIS_HOST:-redis}" -p "${REDIS_PORT:-6379}" \
             PING 2>/dev/null | grep -q PONG
}

check_disk() {
  local usage
  usage=$(df / | awk 'NR==2{gsub(/%/,"",$5); print $5}')
  (( usage < 90 ))
}

PG_OK=0; RD_OK=0; DSK_OK=0
check_postgres && PG_OK=1
check_redis    && RD_OK=1
check_disk     && DSK_OK=1

OVERALL=$(( PG_OK && RD_OK && DSK_OK ))
STATUS=$( (( OVERALL )) && echo 'ok' || echo 'degraded' )

printf '{"status":"%s","checks":{"postgres":%s,"redis":%s,"disk":%s}}\n' \
  "$STATUS" "$PG_OK" "$RD_OK" "$DSK_OK"

(( OVERALL )) && exit 0 || exit 1

deadlineパターンによるタイムアウトユーティリティ

GNUのtimeoutコマンドは任意のコマンドをラップし、指定した時間内に完了しなければ強制終了します。バックグラウンドジョブを手動で管理せずに、待機ループやヘルスプローブに厳格な期限を設ける最も簡潔な方法です。

timeout DURATION COMMAND [ARGS...]

timeoutの終了コード:

  • 0 — コマンドが期限内に成功しました。
  • コマンドの終了コード — コマンドは実行されましたが、ゼロ以外で終了しました。
  • 124 — コマンドがタイムアウトしました(SIGTERMが送信されました)。
  • 137 — --kill-afterの経過後に、SIGKILLでコマンドが強制終了されました。

終了コード124を検出すれば、一般的なエラーではなく、タイムアウトを明確に示すメッセージを出力できます。

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

HOST="${DB_HOST:-postgres}"
PORT="${DB_PORT:-5432}"
DEADLINE=60  # seconds

# Inline poll loop, wrapped by timeout
timeout "$DEADLINE" bash -c "
  until nc -z '$HOST' '$PORT' 2>/dev/null; do
    echo '[wait] ${HOST}:${PORT} not ready...'
    sleep 2
  done
" && echo "[wait] ${HOST}:${PORT} is up" || {
  code=$?
  if (( code == 124 )); then
    echo "[wait] Timed out after ${DEADLINE}s waiting for ${HOST}:${PORT}" >&2
  else
    echo "[wait] Probe failed with exit code ${code}" >&2
  fi
  exit "$code"
}

exec "$@"

Pure Bashによる自己完結型wait-for-it

最小構成のコンテナイメージには、nc(netcat)が含まれていないことがよくあります。Pure Bashでは、/dev/tcpへのプロセス置換を使ってTCP接続を開けます。これは標準仕様ではありませんが、広くサポートされているBash組み込み機能であり、外部ツールを一切必要としません。

構文:/dev/tcp/HOST/PORT — このパスへのリダイレクト、またはこのパスからのリダイレクトを行うと、BashがTCP接続を開きます。接続が拒否された場合やタイムアウトした場合は、エラー(ゼロ以外の終了ステータス)になります。

これは、多くのDocker Compose構成に同梱されている人気のwait-for-it.shスクリプトで使用されている手法です。以下のスクリプトは、任意のDockerfileにCOPYできる完全なスタンドアロン実装です。

#!/usr/bin/env bash
# wait-for-it.sh (pure bash, no nc/curl required)
set -uo pipefail

usage() { echo "Usage: $0 HOST:PORT [-t TIMEOUT] [-- COMMAND]"; exit 1; }

parse_hostport() {
  HOST="${1%%:*}"
  PORT="${1##*:}"
  [[ -n "$HOST" && "$PORT" =~ ^[0-9]+$ ]] || usage
}

[[ $# -ge 1 ]] || usage
parse_hostport "$1"; shift

TIMEOUT=30
[[ "${1:-}" == "-t" ]] && { TIMEOUT="$2"; shift 2; }
[[ "${1:-}" == "--" ]] && shift

wait_for() {
  local elapsed=0
  while (( elapsed < TIMEOUT )); do
    # Pure bash TCP probe — no nc, no curl
    if (exec 3<>"/dev/tcp/${HOST}/${PORT}") 2>/dev/null; then
      exec 3>&-
      return 0
    fi
    sleep 1
    (( elapsed++ ))
  done
  return 1
}

echo "Waiting for ${HOST}:${PORT} (timeout ${TIMEOUT}s)..."
if wait_for; then
  echo "${HOST}:${PORT} is available"
  [[ $# -gt 0 ]] && exec "$@"
else
  echo "Timed out waiting for ${HOST}:${PORT}" >&2
  exit 1
fi

プローブをentrypoint.shに統合する

エントリーポイントパターンは、待機ループ、環境変数の検証、プロセスの起動を、保守しやすい1つのスクリプトにまとめる標準的な方法です。DockerのENTRYPOINTがこのスクリプトを呼び出し、スクリプトはexec "$@"で終了して、同じPIDのままCMDに処理を引き渡します(これによりシグナルを適切に転送できます)。

プロダクション品質のentrypoint.shでは、通常次の処理を行います。

  • 必要な環境変数を早期に検証します(fail fast)。
  • 依存先の待機ループを実行します。
  • 該当する場合はデータベースのマイグレーションを実行します。
  • 最後にセルフチェックを実行します。
  • exec "$@"で制御を引き渡します。

execの使用は重要です。これによりシェルプロセスが置き換えられ、アプリケーションがPID 1になり、DockerやKubernetesからの正常なシャットダウン用のSIGTERMを直接受け取れるようになります。

#!/usr/bin/env bash
# docker/entrypoint.sh
set -euo pipefail

# ── 1. Validate required env vars ────────────────────────────────────
for var in DATABASE_URL REDIS_URL SECRET_KEY; do
  [[ -n "${!var:-}" ]] || { echo "FATAL: ${var} is not set" >&2; exit 1; }
done

# ── 2. Parse DB host/port from DATABASE_URL ──────────────────────────
DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+).*|\1|')
DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|')

# ── 3. Wait for dependencies ─────────────────────────────────────────
timeout 60 bash -c "
  until nc -z '${DB_HOST}' '${DB_PORT}' 2>/dev/null; do sleep 2; done
" || { echo "Database unreachable" >&2; exit 1; }

# ── 4. Run migrations ────────────────────────────────────────────────
echo "[entrypoint] Running migrations..."
python manage.py migrate --noinput

# ── 5. Hand off to CMD (exec preserves PID 1 for signal handling) ────
echo "[entrypoint] Starting application: $*"
exec "$@"

理解度チェック:ライブネスプローブとレディネスプローブ

このレッスンで扱った主要な概念についての理解度を確認します。

振り返り:ヘルスプローブ、レディネスゲート、待機ループ

このレッスンでは、信頼性の高いコンテナ起動調整のためのBashツールキットを構築しました。扱った内容は次のとおりです。

  • wait-forパターン — 厳格なタイムアウトと経過時間ガードを備えたuntil nc -z HOST PORTループにより、壊れた依存先による無限ブロックを防ぎます。
  • HTTPレディネスプローブ — curl --fail --max-timeにより、ポートが開いているだけでなく、アプリケーションが実際に準備できていることを検証します。
  • 指数バックオフ — リトライ間のスリープ間隔を倍増させることで、thundering herdによる負荷を軽減し、復旧中のサービスが安定する時間を確保します。
  • 依存先の並列プローブ — &でプローブをバックグラウンド実行し、wait $PIDで結果を収集することで、複数の依存先がある場合の起動遅延を短縮します。
  • ライブネスとレディネス — ライブネスプローブは高速かつローカルのみの確認にし、レディネスプローブでは下流システムを確認できます。両者を決して混同しないでください。
  • startupProbe — 起動に時間のかかるアプリケーションを、初期化中の誤ったライブネス失敗から保護します。
  • /dev/tcpプローブ — Pure BashによるTCPチェックは外部ツールを必要とせず、最小構成のコンテナイメージに適しています。
  • entrypoint.sh — 環境変数の検証、待機ループ、マイグレーション、exec "$@"が、コンテナ化されたサービスの標準的な起動パターンを形成します。

これらのパターンは、Docker Compose、Kubernetes、ECS全体における自己修復型でプロダクション品質のコンテナデプロイの基盤になります。

よくある質問

「ヘルスプローブ、準備完了ゲート、待機ループ」レッスンは無料ですか?

はい。「ヘルスプローブ、準備完了ゲート、待機ループ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

「ヘルスプローブ、準備完了ゲート、待機ループ」で何を学びますか?

依存関係の待機ループと生存確認プローブを実装し、コンテナ化されたサービスを確実に起動できるようにします。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?

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

「ヘルスプローブ、準備完了ゲート、待機ループ」レッスンにはどのくらい時間がかかりますか?

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

このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?

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

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

  1. 軽量なDockerfileとシェルエントリポイントの作成
  2. envsubstとheredocによる設定のテンプレート化
  3. CLIとjqによるクラウドリソースのスクリプト操作
  4. ヘルスプローブ、準備完了ゲート、待機ループ
← Linux Command Line & Bash Scripting Masteryに戻る