Kubernetes Liveness・Readiness・Startup プローブの違いと設計|再起動ループを防ぐ実践ガイド

(更新: 2026年6月21日 ) Kubernetes DevOps コンテナ ヘルスチェック Express インフラ 可観測性
結論
  • liveness は「死んでいるなら再起動」readiness は「準備できていなければトラフィックを止める」startup は「起動完了まで liveness を待たせる」
  • Express では /health(軽量・依存なし)と /ready(DB等チェック)を分け、deployment.yaml で startupProbe → livenessProbe → readinessProbe の順に設計する。
  • DB 接続プール枯渇は liveness ではなく readiness で検知し、PostgreSQL接続プール枯渇の対処法の設計原則に従う。
  • プローブ失敗の調査は 構造化ログ(JSON)と Loki 設計 で requestId 単位に追跡する。
  • 同じ厳しい条件を liveness に入れると再起動ループの最大原因になる。

なぜ Kubernetes プローブが必要か

Pod をデプロイしたあと、Kubernetes は「コンテナが動いている」ことと「リクエストを処理できる」ことを別々に判断する必要がある。

コンテナプロセスが生きていても、次のような状態は日常茶飯事だ。

  • DBマイグレーション中でまだリクエストを受け付けられない
  • 依存サービスの一時障害でエラー率が上がっている
  • 接続プール枯渇で新規クエリがタイムアウトしている
  • GCやスレッドプール枯渇でハング気味だがプロセスは存在する
  • ローリングデプロイで新旧 Pod が混在している

プローブなしでは、kubelet は「プロセスが動いていれば正常」とみなし、Service は準備できていない Pod にもトラフィックを送り続ける。結果として 502/503、デプロイ中のエラー率上昇、障害 Pod への再起動が遅れる——といった問題が起きる。

Kubernetes 1.16 以降、3種類のプローブが標準化されている。2026年の本番クラスタ(EKS 1.31、GKE 1.30 前後)でもこの3分類が基本設計の軸である。

プローブ主な質問失敗時の動作
livenessProbeプロセスは生きているか?コンテナ再起動
readinessProbeトラフィックを受け付けられるか?Endpoints から除外
startupProbe起動は完了したか?liveness/readiness の開始を遅延

この記事では liveness readiness 違い を軸に、HTTP / TCP / exec 各プローブ種別、failureThreshold の設計、グレースフルシャットダウン、2026年のベストプラクティスまで、Express 実装と deployment.yaml 完全例つきで解説する。

3種類のプローブ — 役割の根本的な違い

プローブ 設計の要点
livenessProbe デッドロック・ハング検知。チェックは軽量に。失敗 = 再起動
readinessProbe 依存関係・負荷状態を確認。失敗 = トラフィック停止(再起動なし)
startupProbe 起動猶予。成功まで他プローブ無効。遅い起動のアプリ向け
同じURLを共用 アンチパターン。liveness が DB 障害で失敗 → 再起動ループ
HTTP / exec / tcp HTTP推奨(アプリ層)。exec は補助、tcp はリッスンのみ確認

livenessProbe — 生存確認

liveness は 「このコンテナはもう復旧不能か?」 を問う。失敗すると kubelet は SIGTERM → grace period → SIGKILL の順でコンテナを再起動する。

向いているケース:

  • デッドロックで HTTP 応答が永久に返らない
  • メインスレッドがハングし、プロセスだけ残っている
  • アプリ内部の watchdog が異常を検知した

向いていないケース:

  • DB接続プールの一時枯渇(readiness で扱うべき。対処ガイド参照)
  • 外部 API のレイテンシ悪化(readiness またはサーキットブレーカー)
  • 起動直後のウォームアップ中(startupProbe で扱うべき)
liveness に DB チェックを入れない

PostgreSQL メンテナンスや PgBouncer の再起動で /ready が 503 になるのは正常な劣化だ。これを liveness に載せると CrashLoopBackOff になる。枯渇の根本原因はプール設計側で直し、トラフィック制御は readiness に任せる。

readinessProbe — 準備完了確認

readiness は 「ロードバランサーに載せてよいか?」 を問う。失敗しても Pod は再起動されない。Service の Endpoints コントローラが該当 Pod の IP をリストから外し、Ingress / Service 経由のトラフィックが届かなくなる。

ローリングデプロイでは、新 Pod が readiness 成功するまで旧 Pod にトラフィックが流れ続ける。これにより ゼロダウンタイム が実現する。

readiness で確認すべき依存関係の例:

依存チェック方法失敗時の意味
PostgreSQLSELECT 1書き込み・読み取り不可
RedisPINGセッション・キャッシュ不可
メッセージキュー接続確認非同期処理不可
必須の外部 APIHEAD/GET(タイムアウト短め)機能の一部が使えない

オプション依存(機能フラグで無効化できるもの)は readiness 失敗にしない設計も検討する。checks オブジェクトで ok / fail / skip を返し、必須のみで 503 を決める。

startupProbe — 起動猶予

Kubernetes 1.16 で追加。起動に時間がかかるアプリで、liveness の initialDelaySeconds だけでは足りない場合に使う。

startupProbe が設定されている間、liveness と readiness は無効。startup が成功した時点から通常の liveness/readiness が動き始める。

最大起動猶予の計算:

猶予秒数 = failureThreshold × periodSeconds

例: failureThreshold: 30, periodSeconds: 10 → 最大 300 秒(5 分)の起動猶予。

Spring Boot + Flyway マイグレーション、大規模なインメモリキャッシュのウォームアップ、GPU モデルのロードなど、起動完了の定義が「ポートが開いた」では足りないケースで startup が効く。

プローブの仕組み — kubelet が何をしているか

kubelet は各プローブについて、設定された間隔(periodSeconds)でチェックを実行する。

Pod 起動

  ├─ startupProbe あり?
  │     ├─ YES → startup 成功まで liveness/readiness はスキップ
  │     └─ NO  → 即座に liveness/readiness 開始

  ├─ livenessProbe 失敗が failureThreshold 回連続
  │     └─ コンテナ再起動(restartCount++)

  └─ readinessProbe 失敗
        └─ Endpoints から Pod IP を削除(再起動なし)

主要パラメータ一覧

パラメータデフォルト意味
initialDelaySeconds0コンテナ起動後、最初のチェックまでの待機
periodSeconds10チェック間隔
timeoutSeconds11回のチェックのタイムアウト
successThreshold1成功とみなす連続成功回数(liveness/startup は常に1)
failureThreshold3失敗とみなす連続失敗回数
terminationGracePeriodSeconds30liveness 失敗後の SIGTERM から強制終了まで

failureThreshold の設計 — 2026年の実務指針

failureThreshold は「何回連続で失敗したらアクションするか」を決める。単独では意味がなく、periodSeconds とセットで解釈する。

プローブ推奨 failureThreshold意図
liveness3〜5一時的な GC 停止を再起動にしない。ただしハング検知が遅れる
readiness1〜31 なら即座にトラフィック停止。DB 瞬断でフラップするなら 2〜3
startup起動時間 ÷ periodSeconds × 1.5起動の p99 + バッファ

再起動までの時間(startup なし) の目安:

initialDelaySeconds + (failureThreshold × periodSeconds)

例: initialDelaySeconds: 10, failureThreshold: 3, periodSeconds: 5 → 最初の失敗判定まで最短 10 秒、その後 15 秒で再起動。

readiness の successThreshold はデフォルト 1 だが、2 に上げると「2回連続成功してから Endpoints に載せる」ため、起動直後のフラップを減らせる。ローリングデプロイで新 Pod がすぐ ready → not ready を繰り返す場合に有効。

実測してから閾値を決める

ステージングで kubectl run の debug Pod から while true; do curl -so /dev/null -w '%{http_code} %{time_total}\n' http://api:3000/ready; sleep 1; done を回し、p99 応答時間を取る。timeoutSeconds は p99 の 2 倍、failureThreshold × periodSeconds は許容するダウンタイム以下に収める。

HTTP / TCP / exec プローブの使い分け

プローブ種別 長所・短所・向き不向き
httpGet アプリ層の応答を確認。パス・ヘッダー・ステータスコードで細かく制御可能。Express/Spring/Go の標準選択
grpc(1.24+) gRPC ヘルスプロトコル対応サービス向け。HTTP/2 ネイティブ。マイクロサービス間で一貫したチェック
tcpSocket ポート LISTEN のみ。実装が不要だがデッドロックを見逃す。レガシー・サイドカーなしの最小構成向け
exec シェル・PID ファイル・カスタムスクリプト。オーバーヘッド大。HTTP が使えないバッチ・ワーカー向け

httpGet — 成功・失敗の判定

  • 成功条件: HTTP ステータス 200〜399(2xx/3xx)
  • 失敗条件: 4xx/5xx、接続拒否、タイムアウト
  • kubelet はコンテナ内から Pod IP(または host 指定時はそのアドレス)へリクエストする

認証が必要なアプリでは /health/ready だけ認証をバイパスするか、httpHeaders で内部トークンを送る。

tcpSocket — 限定的な用途

ポートが開いていることしか分からない。次のようなケースでのみ検討する。

  • ヘルスエンドポイントを実装できないレガシーバイナリ
  • プローブ対象がプロキシ(nginx の listen のみ確認)

本番 API サーバーでは httpGet を強く推奨する。

exec — バッチ・ワーカー向け

exec はコンテナ内でコマンドを実行する。fork + exec のコストがあり、高頻度(periodSeconds: 1)では node の負荷になる。

livenessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - "test -f /var/run/worker.pid && kill -0 $(cat /var/run/worker.pid)"
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3

スクリプトの終了コード 0 が成功、非 0 が失敗。シェルの引用符ミスで常に成功するバグに注意。

Express で /health と /ready を実装する

Node.js + Express での定番パターンは、エンドポイントを2つに分けることだ。

  • GET /health — liveness 用。プロセスが応答できるかだけ見る。DB に触れない。
  • GET /ready — readiness 用。DB・Redis・必須の外部依存を確認する。

完全な server.js 例

// server.js
const express = require('express');
const { Pool } = require('pg');

const app = express();
const port = process.env.PORT || 3000;

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 2000,
  idleTimeoutMillis: 30000,
});

// --- Liveness: プロセス生存のみ ---
app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'ok',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
  });
});

// --- Readiness: 依存関係の確認 ---
app.get('/ready', async (req, res) => {
  const checks = { database: 'unknown' };

  try {
    const client = await pool.connect();
    try {
      await client.query('SELECT 1');
      checks.database = 'ok';
    } finally {
      client.release();
    }

    res.status(200).json({
      status: 'ready',
      checks,
      timestamp: new Date().toISOString(),
    });
  } catch (err) {
    checks.database = 'fail';
    res.status(503).json({
      status: 'not_ready',
      checks,
      error: err.message,
      timestamp: new Date().toISOString(),
    });
  }
});

// --- 本番 API ---
app.get('/api/users', async (req, res) => {
  const result = await pool.query('SELECT id, name FROM users LIMIT 10');
  res.json(result.rows);
});

// --- グレースフルシャットダウン ---
let isShuttingDown = false;

app.use((req, res, next) => {
  if (isShuttingDown) {
    res.set('Connection', 'close');
    return res.status(503).json({ error: 'Server is shutting down' });
  }
  next();
});

const server = app.listen(port, () => {
  console.log(JSON.stringify({
    level: 'info',
    message: 'Server listening',
    port,
    timestamp: new Date().toISOString(),
  }));
});

async function shutdown(signal) {
  console.log(JSON.stringify({
    level: 'info',
    message: 'Graceful shutdown started',
    signal,
    timestamp: new Date().toISOString(),
  }));
  isShuttingDown = true;

  server.close(async () => {
    await pool.end();
    console.log(JSON.stringify({
      level: 'info',
      message: 'HTTP server closed, pool drained',
      timestamp: new Date().toISOString(),
    }));
    process.exit(0);
  });

  setTimeout(() => {
    console.error(JSON.stringify({
      level: 'error',
      message: 'Forced shutdown after timeout',
      timestamp: new Date().toISOString(),
    }));
    process.exit(1);
  }, 25000);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

設計上のポイント

  1. /health は常に 200 — メモリリークでプロセスが応答不能になったときだけ失敗させる。DB が落ちても liveness は成功のままにする。
  2. /ready は 503 を返す — 依存障害時に Endpoints から外れる。接続プール枯渇時もここで 503 にし、再起動ではなくトラフィック分散で耐える(プール枯渇の対処)。
  3. グレースフルシャットダウンSIGTERM 受信後、まず isShuttingDown で readiness 相当の 503 を返し、進行中リクエストを処理してから pool.end() で終了する。
  4. タイムアウト — DB クエリに connectionTimeoutMillis を設定し、プローブの timeoutSeconds 以内に応答する。
  5. 構造化ログ — シャットダウンや ready 失敗を JSON で出すと、Loki で requestId 単位に追跡しやすい。

TypeScript + 構造化ヘルスチェック(拡張版)

// health.ts
import type { Request, Response } from 'express';
import { pool } from './db';
import { redis } from './redis';
import { logger } from './logger';

type CheckResult = 'ok' | 'fail' | 'skip';

interface HealthResponse {
  status: 'ok' | 'not_ready';
  checks: Record<string, CheckResult>;
  timestamp: string;
}

export async function livenessHandler(_req: Request, res: Response): Promise<void> {
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}

export async function readinessHandler(_req: Request, res: Response): Promise<void> {
  const checks: Record<string, CheckResult> = {};
  let allOk = true;

  try {
    const client = await pool.connect();
    try {
      await client.query('SELECT 1');
      checks.postgres = 'ok';
    } finally {
      client.release();
    }
  } catch (err) {
    checks.postgres = 'fail';
    allOk = false;
    logger.warn({ err, event: 'readiness_check_failed', dependency: 'postgres' });
  }

  try {
    const pong = await redis.ping();
    checks.redis = pong === 'PONG' ? 'ok' : 'fail';
    if (checks.redis === 'fail') allOk = false;
  } catch {
    checks.redis = 'fail';
    allOk = false;
  }

  const body: HealthResponse = {
    status: allOk ? 'ok' : 'not_ready',
    checks,
    timestamp: new Date().toISOString(),
  };

  res.status(allOk ? 200 : 503).json(body);
}

deployment.yaml 完全例

以下は、上記 Express アプリを想定した 本番向け Deployment の完全例だ。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
  labels:
    app: api-server
    version: v1.2.0
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
        version: v1.2.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "3000"
        prometheus.io/path: "/metrics"
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: registry.example.com/api-server:1.2.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 3000
              protocol: TCP
          env:
            - name: PORT
              value: "3000"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: api-secrets
                  key: database-url
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

          startupProbe:
            httpGet:
              path: /health
              port: http
              scheme: HTTP
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 24

          livenessProbe:
            httpGet:
              path: /health
              port: http
              scheme: HTTP
            initialDelaySeconds: 0
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
            successThreshold: 1

          readinessProbe:
            httpGet:
              path: /ready
              port: http
              scheme: HTTP
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 2
            successThreshold: 1

          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - "sleep 5"

---
apiVersion: v1
kind: Service
metadata:
  name: api-server
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: api-server
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP

軽量 Node.js API 向け(startupProbe 省略版)

起動が 5〜10 秒以内の Express API では、startup を省略し liveness の initialDelaySeconds で足りることが多い。

# deployment-light.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server-light
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-server-light
  template:
    metadata:
      labels:
        app: api-server-light
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: registry.example.com/api-server:1.2.0
          ports:
            - containerPort: 3000
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2

exec と tcpSocket の YAML 例

# probes-exec-tcp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-worker
spec:
  replicas: 1
  selector:
    matchLabels:
      app: legacy-worker
  template:
    metadata:
      labels:
        app: legacy-worker
    spec:
      containers:
        - name: worker
          image: registry.example.com/legacy-worker:2.0.0
          ports:
            - containerPort: 8080
          livenessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - "pgrep -f 'java -jar app.jar' > /dev/null"
            initialDelaySeconds: 30
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            tcpSocket:
              port: 8080
            periodSeconds: 10
            timeoutSeconds: 1
            failureThreshold: 2

典型的なミス設定と再起動ループ

再起動ループ(CrashLoopBackOff)は、liveness プローブの誤設計が最も多い原因だ。

ミス1: liveness に DB チェックを入れる

# アンチパターン
livenessProbe:
  httpGet:
    path: /ready
    port: 3000

何が起きるか: PostgreSQL のメンテナンスや接続プール枯渇/ready が 503 → liveness 失敗 → コンテナ再起動 → 起動直後また DB 未接続 → 再失敗 → 無限ループ

修正:

livenessProbe:
  httpGet:
    path: /health
    port: 3000
readinessProbe:
  httpGet:
    path: /ready
    port: 3000

ミス2: initialDelaySeconds が短すぎる

アプリの起動に 20 秒かかるのに、periodSeconds: 5 × failureThreshold: 3 = 15 秒で liveness 失敗 → 起動完了前に再起動。

修正: startupProbe を追加する。

startupProbe:
  httpGet:
    path: /health
    port: 3000
  periodSeconds: 5
  failureThreshold: 12

ミス3: timeoutSeconds < アプリ応答時間

/ready の DB クエリに 2 秒かかるのに timeoutSeconds: 1 だと毎回タイムアウト。readiness ならトラフィック停止、liveness なら再起動ループ。

修正: p99 応答時間 + 余裕を timeoutSeconds に設定(通常 2〜5 秒)。

ミス4: 高負荷時に liveness が失敗する

/health 内で重い同期処理やイベントループのブロックがあると、高負荷時にプローブ応答が遅延する。

修正: /health は即座に 200 を返す。CPU limit の見直し。failureThreshold: 5 は一時策に留める。

ミス5: グレースフルシャットダウンなし

terminationGracePeriodSeconds: 30 なのに SIGTERM を無視すると、進行中リクエストが切断される。

修正: isShuttingDown で 503、server.close()pool.end()、preStop の sleep 5

再起動ループの切り分け手順

1

kubectl get pods で STATUS が CrashLoopBackOff か確認

2

kubectl describe pod <name> の Events 末尾で Liveness probe failed を探す

3

kubectl logs <name> --previous で再起動直前のログを確認

4

Loki で {pod="..."} | json | event="readiness_check_failed" を検索([構造化ログ設計](/blog/structured-logging-json-loki-設計/))

5

失敗しているパス(/health か /ready か)と HTTP ステータスを特定

6

liveness ならエンドポイントを軽量化 / startupProbe 追加 / initialDelaySeconds 延長

7

readiness なら timeout 延長 / DB・プール障害の切り分け

kubectl get pods -n production -l app=api-server
kubectl describe pod api-server-7d8f9c-abcde -n production
kubectl logs api-server-7d8f9c-abcde -n production --previous --tail=100

kubectl exec -it api-server-7d8f9c-abcde -n production -- \
  wget -qO- http://localhost:3000/health
kubectl exec -it api-server-7d8f9c-abcde -n production -- \
  wget -qO- http://localhost:3000/ready

グレースフルシャットダウンの完全フロー

ローリングデプロイや kubectl delete pod では、kubelet がコンテナに SIGTERM を送る。アプリがこれを正しく処理しないと、プローブ設計が良くてもユーザーには 502 が見える。

1. kubelet → SIGTERM
2. アプリ: isShuttingDown = true → /ready は 503(readiness 失敗)
3. Endpoints コントローラが Pod IP を削除(数秒)
4. preStop sleep(オプション)で伝播ラグを吸収
5. server.close() で新規接続拒否、進行中リクエスト完了待ち
6. pool.end() / キュー切断 / キャッシュフラッシュ
7. process.exit(0)
8. grace period 超過時 → SIGKILL

terminationGracePeriodSeconds(最大リクエスト処理時間) + (preStop sleep) + (バッファ) 以上にする。API が 60 秒かかるエンドポイントがあるなら、grace period も 60 秒超が必要だ。

readiness と preStop の役割分担

アプリが /ready で 503 を返せるなら、preStop は短い sleep で足りる。レガシーアプリで readiness を制御できない場合は、preStop で curl -X POST localhost:8080/shutdown のように明示的なドレイン API を呼ぶ。

HTTP プローブの詳細設定

livenessProbe:
  httpGet:
    path: /health
    port: 3000
    scheme: HTTP
    httpHeaders:
      - name: X-Probe-Token
        valueFrom:
          secretKeyRef:
            name: probe-token
            key: token
  initialDelaySeconds: 0
  periodSeconds: 10
  timeoutSeconds: 3
  successThreshold: 1
  failureThreshold: 3

gRPC アプリ(Kubernetes 1.24+)

livenessProbe:
  grpc:
    port: 50051
  periodSeconds: 10
readinessProbe:
  grpc:
    port: 50051
    service: "my.service.Health"
  periodSeconds: 5
  failureThreshold: 2

ローリングデプロイとプローブの相互作用

maxUnavailable: 0 と readiness を組み合わせると、次の流れになる。

1. 新 ReplicaSet の Pod が起動
2. startupProbe(あれば)成功
3. readinessProbe 成功 → Endpoints に追加
4. 旧 Pod へ SIGTERM → readiness 失敗 → Endpoints から削除
5. 旧 Pod の grace period 中に残リクエスト処理
6. 旧 Pod 終了

2026年のベストプラクティス

1. プローブは「ユーザーが体験する経路」に近づける

Service メッシュ(Istio/Linkerd)を使う場合、sidecar 経由のトラフィックと kubelet プローブの経路がずれることがある。Kubernetes 1.28+ では native sidecarrestartPolicy: Always の init コンテナ)が安定しており、プローブがアプリコンテナを直接叩く設計を維持しやすくなった。

2. 観測可能性 — プローブとログ・メトリクスの分離

項目Kubernetes プローブPrometheus / Loki
目的自動復旧・トラフィック制御可観測性・アラート
間隔5〜30 秒15〜60 秒が多い
アクション再起動・Endpoints 更新通知・ダッシュボード
チェック深度軽量(特に liveness)詳細メトリクス可

プローブ失敗の原因調査には 構造化ログ JSON + Lokireadiness_check_failed などのイベントを記録する。メトリクス例:

メトリクスアラート条件
kube_pod_container_status_restarts_total5分で 3回以上増加
readiness 失敗率全 Pod の 50% 超が not ready
プローブレイテンシp99 > timeoutSeconds の 80%

3. セキュリティ — プローブエンドポイントの露出

/health/ready は認証なしでも、機密情報を返さない。バージョン情報の露出は最小限に。NetworkPolicy でクラスタ内部からのみ到達可能にする構成も検討する。

4. リソース制限とプローブの相互作用

CPU throttling(limits.cpu ヒット)でイベントループが遅延し、liveness が失敗することがある。requests/limits の比率を見直し、/health は同期 I/O ゼロを維持する。

5. ステージングでのカオス演習

  • DB を止めて /ready が 503 になり Pod が Endpoints から外れることを確認
  • kubectl execkill -STOP し、liveness が再起動をトリガーするか確認
  • デプロイ中に kubectl get endpoints -w で切り替えを観察

6. Helm / Kustomize での標準化

チームごとに probe 設定がバラつくと障害時に混乱する。ベースチャートに startup / liveness / readiness のデフォルト値を定義し、アプリごとに pathfailureThreshold だけ上書きするパターンが 2026 年の定石だ。

liveness / readiness / startup の比較表

観点 liveness vs readiness vs startup
目的 生存 / 受付可否 / 起動完了
失敗時 再起動 / Endpoints除外 / liveness開始を遅延
チェック内容 軽量・依存なし / 依存込み / 起動完了の目安
典型エンドポイント /health / /ready / /health または同じ
periodSeconds 10〜30秒 / 5〜10秒 / 5〜10秒
必須度 推奨 / 本番必須 / 遅い起動時のみ

EKS / GKE / minikube での差分

環境注意点
minikubeローカルなので DB も Pod 内。readiness の依存先を docker-compose と揃える
EKSALB Ingress の healthcheck-path を /ready に合わせる
GKEBackendConfig で healthCheck を設定
Istio1.16+ ではプローブ rewrite が改善。native sidecar と併用を検討

よくある設計判断

liveness は省略できるか?

デッドロック検知手段がなくなる。supervisor が別にあれば省略もあり得るが、Kubernetes では liveness 設定が一般的。

readiness なしでデプロイすると?

新 Pod が起動完了前にトラフィックを受け、500 エラーが出る。readiness は本番必須と考えてよい。

同じ periodSeconds にすべきか?

必須ではない。readiness を 5 秒、liveness を 15 秒のように分けてよい。

チェックリスト — デプロイ前の最終確認

  • /health は DB 等に依存せず、常に高速(< 100ms)で 200 を返す
  • /ready は必須依存のみ確認し、503 で失敗を表現する
  • 接続プール枯渇時は readiness でトラフィックを切り、プール設計を見直す
  • liveness と readiness で別パスを使っている
  • 起動に 30 秒超かかるなら startupProbe を設定した
  • timeoutSeconds ≥ プローブ先の p99 応答時間
  • failureThreshold × periodSeconds が再起動までの許容時間として妥当
  • SIGTERM でグレースフルシャットダウンし、進行中リクエストを捌ける
  • プローブ失敗を 構造化ログ で記録している
  • kubectl describe pod で再起動ループがないことを確認した

関連記事

この記事は API・バックエンド テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。

トピック記事
APIキャッシュ設計ガイドAPIキャッシュ設計ガイド|ETag・Cache-Control・304・CDNキャッシュキーの実務
Web APIのエラーハンドリング設計Web APIのエラーハンドリング設計|RFC 9457・error_code・400/422/409の使い分け
Web APIのべき等性(Idempotency)設計Web APIのべき等性(Idempotency)設計|二重決済を防ぐ実装ガイド
API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較
APIのレート制限(Rate Limiting)実装ガイドAPIのレート制限(Rate Limiting)実装ガイド|アルゴリズム選定と429エラーの適切なハンドリング
APIレート制限と429対応APIレート制限と429対応|X-RateLimit・Retry-Afterを使ったクライアント実装
WebHook設計のベストプラクティスWebHook設計のベストプラクティス|署名・再試行・べき等性で信頼性を担保する
ZodでAPI境界のランタイム検証を設計するZodでAPI境界のランタイム検証を設計する|z.infer・Expressミドルウェア・zod-to-openapi(2026)
OpenAPI 3.1仕様のCIバリデーション完全ガイドOpenAPI 3.1仕様のCIバリデーション完全ガイド|Spectral Lint・Breaking Change検出・GitHub Actions
GraphQL N+1 問題の解決GraphQL N+1 問題の解決|DataLoader バッチング実装と Apollo Server 完全ガイド
Circuit Breaker + Retry パターン実装ガイドCircuit Breaker + Retry パターン実装ガイド|Node.js opossum/cockatiel・指数バックオフ・Jitter
Sagaパターン完全ガイドSagaパターン完全ガイド|Orchestration vs Choreography と補償トランザクション(Node.js)

まとめ

liveness readiness 違いは一言で言えば、「再起動するか、トラフィックを止めるか」の違いだ。Kubernetes プローブ設定で最も重要なのは、liveness を軽く、readiness を厳しくすること。

Express では /health/ready を分離し、deployment.yaml では startupProbe(必要なら)→ livenessProbe → readinessProbe の順で組み立てる。DB や接続プールの問題で liveness が失敗する設定は再起動ループの温床なので、まずそこを疑う。

グレースフルシャットダウンでは SIGTERM 後に readiness を先に落とし、terminationGracePeriodSeconds 内に処理を完了させる。障害調査では 構造化ログと Loki でプローブ失敗とリクエストログを突合する。

デプロイ後は kubectl describe pod の Events と kubectl get endpoints -w で実際の挙動を確認し、閾値を実測値に合わせて調整する——これが 2026 年時点での Kubernetes プローブ設計 の実務的な完成形だ。