目次
- ヘルスチェックは誰が、何のために使うのか
- ロードバランサー:「このサーバーにリクエストを送ってよいか」
- オーケストレーター:「再起動すべきか」「トラフィックを向けてよいか」
- LivenessとReadinessを分けないと何が起きるか
- 「再起動しても直らないもの」をLivenessに入れてはいけない
- Readinessは「今は受けられない」を伝えるための手段
- DB依存をチェックに含めるべきか:浅いチェックと深いチェック
- 浅いチェック(shallow)と深いチェック(deep)
- 判断基準:「そのサーバーを外すと状況が改善するか」
- 実装例:ExpressとFastAPI
- Express(Node.js)
- FastAPI(Python)
- Kubernetes側の設定例
- トラブル事例:DBの短い遅延で全台が再起動ループに入った
- 症状
- 原因
- 対処
- まとめ
「ヘルスチェック用に/healthを作って200を返すようにした」――多くのチームがここで止まっています。ところが実際の障害では、「プロセスは生きているのにDBに繋がらず、ロードバランサーは正常と判断してリクエストを送り続けた」「DBが少し遅くなっただけで全台が再起動ループに入った」といった、ヘルスチェックの設計そのものが原因の事故が起こります。
この記事では、ロードバランサーやコンテナオーケストレーターがヘルスチェックの結果をどう使うかを起点に、Liveness(再起動の判断)とReadiness(トラフィックを流すかの判断)の違い、DBなどの依存先をチェックに含めるべきかの判断基準、浅いチェックと深いチェックの使い分けを解説します。ExpressとFastAPIの実装例と、誤設定で全台ダウンした事例も取り上げるので、自動復旧が「正しく」機能するヘルスチェックを設計できるようになります。
ヘルスチェックは誰が、何のために使うのか
ロードバランサー:「このサーバーにリクエストを送ってよいか」
ロードバランサー(ALB、Nginx、HAProxyなど)は、配下の各サーバーに数秒〜数十秒おきにHTTPリクエストを送り、一定回数連続で失敗したサーバーを振り分け対象から外します。回復して連続で成功すれば戻します。つまりロードバランサーにとってのヘルスチェックは「このサーバーは今、ユーザーのリクエストをさばけるか」を判断する材料です。ロードバランサーの動作全般はロードバランサーとはで解説しています。
ここで大事なのは、ロードバランサーはサーバーを「直す」ことはしない点です。外すだけです。全台がチェックに失敗した場合の挙動は製品により、ALBは「すべて異常なら全台に送る(fail-open)」という動作をします。全台外して503を返すより、どこかに送った方がまだましだという考え方です。
オーケストレーター:「再起動すべきか」「トラフィックを向けてよいか」
KubernetesやECSのようなコンテナオーケストレーターは、ヘルスチェックの結果を「再起動」にも使います。ここで初めて、2種類のチェックを分ける必要が出てきます。
| 種類 | 問い | 失敗したときの動作 | チェックすべきこと |
|---|---|---|---|
| Liveness(生存) | このプロセスは壊れていて再起動が必要か | コンテナを強制終了して再起動 | プロセス自身の状態のみ(デッドロック、イベントループ停止など) |
| Readiness(準備完了) | 今リクエストを受けられる状態か | ロードバランサーの振り分けから外す(再起動はしない) | 起動処理の完了、依存先への接続、過負荷状態 |
| Startup(起動) | 起動処理はまだ続いているか | 成功するまでLivenessを猶予する | 起動に時間がかかるアプリの初期化完了 |
Kubernetesはこの3つをそれぞれlivenessProbe・readinessProbe・startupProbeとして設定します。ECSのヘルスチェックはコンテナ単位のLiveness相当と、ALBのターゲットグループによるReadiness相当の2段構成になります。Kubernetesの基本はKubernetes入門を参照してください。
LivenessとReadinessを分けないと何が起きるか
「再起動しても直らないもの」をLivenessに入れてはいけない
Livenessの失敗は再起動を引き起こします。再起動で直るのは「プロセス内部の不具合」だけです。DBが落ちている、外部APIが遅い、といった依存先の問題はアプリを再起動しても直りません。それどころか、DBが少し不調になると全コンテナがLivenessに失敗して一斉に再起動し、起動時のコネクション確立でDBにさらに負荷がかかり、再起動ループに入ります。これは「自動復旧」ではなく「自動破壊」です。
原則として、Livenessは依存先を見ない。プロセスが生きていてHTTPを返せるなら200、が正解です。デッドロックやイベントループの停止を検知したい場合も、「一定時間内にリクエスト処理が1件も進んでいない」といったプロセス内部の指標で判断します。
Readinessは「今は受けられない」を伝えるための手段
一方Readinessの失敗は「トラフィックを止める」だけで、コンテナは生き続けます。起動直後でDBコネクションプールがまだ温まっていない、キャッシュの初期ロード中、グレースフルシャットダウンで新規リクエストを受け付けたくない、といった「一時的に受けられない」状態をロードバランサーに伝えるための仕組みです。
特にデプロイ時は重要で、新しいコンテナが「起動したがまだ準備できていない」間にリクエストを受ければ、エラーが出ます。Readinessが成功するまで振り分けられない、という仕組みがあるからこそ、ゼロダウンタイムのローリング更新が成立します。デプロイ手法との関係はゼロダウンタイムデプロイの手法を、終了時の扱いはグレースフルシャットダウン入門を参照してください。
DB依存をチェックに含めるべきか:浅いチェックと深いチェック
浅いチェック(shallow)と深いチェック(deep)
浅いチェックは、プロセスが応答できることだけを確認します(固定の200を返す)。深いチェックは、DBへのSELECT 1、RedisへのPING、外部APIの疎通など、依存先まで確認します。深いチェックの方が「本当に動くか」に近い一方で、次の副作用があります。
- 依存先の障害で全台が同時に異常判定され、ロードバランサーが全台を外す(fail-openしない製品では完全停止)
- チェックのたびにDBクエリが走り、台数×頻度分の負荷がDBにかかる
- 依存先のタイムアウト待ちでチェック自体がタイムアウトし、実態より悪い判定になる
判断基準:「そのサーバーを外すと状況が改善するか」
Readinessに依存先を含めるかどうかは、「このサーバーをロードバランサーから外したら、ユーザーの体験が改善するか」で判断します。
- サーバー固有の問題(そのサーバーだけDBに繋がらない、コネクションプールが枯渇している):外せば他のサーバーが受けるので改善する。チェックに含める価値がある。
- 全台共通の問題(DB自体がダウン):外しても改善せず、全台が外れて完全停止になる。含めるべきではない。
この2つを区別できないため、実務では次の折衷案が多く採用されています。Readinessは「起動処理の完了」と「自分自身のコネクションプールに空きがあるか」程度の軽い確認に留め、DBのSELECT 1のような深いチェックは別のエンドポイント(例:/health/deep)として用意し、監視ツールからのアラート用にだけ使う、という分け方です。「自動で外す判断」と「人間に知らせる判断」を分けるのが要点です。
実装例:ExpressとFastAPI
Express(Node.js)
Liveness・Readiness・深いチェックの3エンドポイントを分け、Readinessはアプリ内のフラグで制御します。SIGTERM受信時にReadinessをfalseにすることで、ロードバランサーから外れてから終了できます。
import express from "express";
import { pool } from "./db.js";
const app = express();
let ready = false; // 起動処理完了で true
let shuttingDown = false;
// Liveness:プロセスが応答できれば常に200
app.get("/health/live", (req, res) => res.status(200).send("ok"));
// Readiness:起動完了かつシャットダウン中でなければ200
app.get("/health/ready", (req, res) => {
if (!ready || shuttingDown) return res.status(503).send("not ready");
res.status(200).send("ready");
});
// 深いチェック:監視ツール用(LBやk8sには使わない)
app.get("/health/deep", async (req, res) => {
const checks = {};
try {
await Promise.race([
pool.query("SELECT 1"),
new Promise((_, rej) => setTimeout(() => rej(new Error("db timeout")), 2000)),
]);
checks.db = "ok";
} catch (e) {
checks.db = e.message;
}
const healthy = Object.values(checks).every((v) => v === "ok");
res.status(healthy ? 200 : 503).json({ status: healthy ? "ok" : "degraded", checks });
});
async function start() {
await pool.query("SELECT 1"); // 起動時に一度だけDB到達を確認
ready = true;
app.listen(3000);
}
process.on("SIGTERM", () => {
shuttingDown = true; // まずReadinessを落として振り分けから外れる
setTimeout(() => process.exit(0), 10000); // LBが外すのを待ってから終了
});
start();
FastAPI(Python)
import asyncio
from fastapi import FastAPI, Response
from contextlib import asynccontextmanager
state = {"ready": False}
@asynccontextmanager
async def lifespan(app: FastAPI):
await db.connect() # 起動処理
state["ready"] = True
yield
state["ready"] = False # 終了時はまずReadinessを落とす
await asyncio.sleep(5)
await db.disconnect()
app = FastAPI(lifespan=lifespan)
@app.get("/health/live")
async def live():
return {"status": "ok"}
@app.get("/health/ready")
async def ready(response: Response):
if not state["ready"]:
response.status_code = 503
return {"status": "not ready"}
return {"status": "ready"}
@app.get("/health/deep")
async def deep(response: Response):
try:
await asyncio.wait_for(db.fetch_one("SELECT 1"), timeout=2.0)
return {"status": "ok", "db": "ok"}
except Exception as e:
response.status_code = 503
return {"status": "degraded", "db": str(e)}
Kubernetes側の設定例
閾値は「一時的な遅延で誤判定しないが、本当に壊れたら数十秒以内に検知する」バランスで決めます。failureThreshold×periodSecondsが「異常と判定するまでの時間」です。
containers:
- name: app
startupProbe:
httpGet: { path: /health/live, port: 3000 }
periodSeconds: 5
failureThreshold: 30 # 起動に最大150秒まで猶予
livenessProbe:
httpGet: { path: /health/live, port: 3000 }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3 # 30秒応答なしで再起動
readinessProbe:
httpGet: { path: /health/ready, port: 3000 }
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2 # 10秒で振り分けから外す
successThreshold: 1
トラブル事例:DBの短い遅延で全台が再起動ループに入った
症状
夜間バッチの実行中にDBの応答が数秒遅くなったタイミングで、Kubernetes上のAPIサーバー全Podが同時に再起動を始め、サービスが約20分間断続的に503を返しました。DBの遅延自体は2〜3分で収まっていたにもかかわらず、復旧に時間がかかりました。
原因
livenessProbeに/healthを指定しており、このエンドポイントはDBへSELECT 1を発行する深いチェックでした。timeoutSecondsは1秒、failureThresholdは3だったため、DBの応答が1秒を超える状態が30秒続いた時点で全Podが「死亡」と判定され、一斉に再起動されました。再起動したPodは起動時に一斉にDBへ接続し、それがDBの負荷を押し上げてさらに遅延を伸ばし、次のLivenessも失敗するというループに陥りました。
対処
- Livenessを、依存先を見ない
/health/liveに変更した。DBの遅延では再起動されなくなった。 - Readinessは
/health/ready(起動完了フラグのみ)にし、深いチェックは監視ツールからのアラート専用にした。 timeoutSecondsを2秒、failureThresholdを3に見直し、一時的な遅延で振り分けから外れにくくした。- 起動時のDB接続にジッター付きの待機を入れ、全Podが同時に接続を張らないようにした。
変更後、同様のDB遅延が起きた際は、深いチェックのアラートで人間が気づいただけで、Podの再起動もユーザー影響も発生しませんでした。
まとめ
ヘルスチェックは「200を返すエンドポイント」ではなく、「ロードバランサーとオーケストレーターに何を判断させるか」の設計です。Livenessは再起動の判断なので依存先を見ずプロセス自身の状態だけを返す。Readinessはトラフィックを流すかの判断なので、起動完了とシャットダウン中の状態を正確に返す。DBなどの深いチェックは「外して改善するか」で判断し、迷ったら別エンドポイントにして監視用に使う。この3点を守るだけで、「DBが少し遅くなっただけで全台ダウン」という事故はほぼ防げます。
ヘルスチェックの見直しや、コンテナ環境での自動復旧が期待どおり動かない問題の切り分けは、Harmonic Societyのシステム開発・インフラ支援にご相談ください。
Harmonic Society
この記事の内容、自社の業務でも活かせそうですか?
ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。
関連記事
Related / 9 articles
Notes & Insights
- プログラミング
DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用
DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。
- プログラミング
WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド
WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。
- プログラミング
セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説
CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。
- プログラミング
クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計
クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。
- プログラミング
秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除
APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。
- プログラミング
開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御
開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。
- プログラミング
ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方
NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。
- プログラミング
ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方
ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。
- プログラミング
localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決
localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。
Harmonic Society
「読んで終わり」にせず、自社の業務で試してみませんか?
AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。
無料・30分・オンラインOK|1営業日以内に返信します