第26章:/ready を作る:依存サービスがOKかチェック 🧩🔌
① 今日のゴール 🎯
この章が終わると…👇
/readyが 「今、リクエストを受けていい状態か?」 を返せるようになります 🙆♂️- DBなど依存が死んだら 200 → 503 に切り替わるのを体験できます 💥
- 「軽い疎通」「タイムアウト」「キャッシュ(叩きすぎ防止)」のコツが分かります 🧠✨
イメージはお店🍜:
/health= 店員は店にいる(生存)/ready= 開店準備できた(仕込みOK、レジOK、電気OK)
② 図(1枚)🖼️
ブラウザ / LB / 監視ツール
|
| GET /ready
v
APIコンテナ(Node)
|
| 軽い疎通(SELECT 1)
v
DBコンテナ(Postgres)
ポイント:「APIが生きてる」だけじゃ足りないんです。依存(DBなど)が落ちてたら、受け付けると事故ります 😵💫
③ 手を動かす(手順 5〜10個)🛠️
0) まず知っておくこと(超重要)📌
Composeはデフォで「依存が“起動した”」までは並べてくれますが、「依存が“利用可能になった”」までは待ってくれません。なので依存チェックは自分で作るのが基本です。(Docker Documentation)
(起動順をちゃんと制御したい場合は depends_on の condition: service_healthy を使う手もあります。これもDocker Docsに例が載ってます。(Docker Documentation))
1) ComposeにDB(Postgres)を追加する 🐘🧩
compose.yml(または docker-compose.yml)に db を足します👇
(Docker Docsにも pg_isready を使った例が出てきます。(Docker Documentation))
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgres://app:app@db:5432/appdb"
depends_on:
db:
condition: service_healthy
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
✅ ここで大事:db はコンテナ内のホスト名です。localhost じゃないです 🙅♂️💦
2) DBクライアント(pg)を追加する 📦
npm i pg
npm i -D @types/pg
3) Readinessチェック本体を作る(キャッシュ+タイムアウト付き)⏱️🧠
src/readiness.ts を作ります👇
import { Pool } from "pg";
type CheckOk = { ok: true };
type CheckNg = { ok: false; reason: string };
type CheckResult = CheckOk | CheckNg;
type Cache = { at: number; result: CheckResult };
const TTL_MS = 3_000; // /ready が連打されてもDBを叩きすぎない
const DB_TIMEOUT_MS = 1_000; // “軽い疎通” は短く失敗してほしい
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
return Promise.race([
p,
new Promise<T>((_, rej) => setTimeout(() => rej(new Error("db check timeout")), ms)),
]);
}
export function createReadinessChecker(pool: Pool) {
let cache: Cache | null = null;
async function checkDb(): Promise<CheckResult> {
try {
// “軽い疎通”の王道:SELECT 1
await withTimeout(pool.query("SELECT 1"), DB_TIMEOUT_MS);
return { ok: true };
} catch (e) {
const reason = e instanceof Error ? e.message : String(e);
return { ok: false, reason };
}
}
return async function checkReady(): Promise<CheckResult> {
const now = Date.now();
if (cache && now - cache.at < TTL_MS) return cache.result;
const result = await checkDb();
cache = { at: now, result };
return result;
};
}
✨ コツ:
- 毎回DBを叩かない(TTLキャッシュ)🧊
- 長く待たない(タイムアウト)⏱️
- 重いSQLは絶対やらない(
SELECT 1で十分)🪶
4) /ready エンドポイントを生やす 🌱
たとえば src/server.ts(あなたのExpress起動ファイル)に追加👇
import express from "express";
import { Pool } from "pg";
import { performance } from "node:perf_hooks";
import { createReadinessChecker } from "./readiness";
const app = express();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
connectionTimeoutMillis: 1000, // 接続自体もダラダラ待たない
max: 5,
});
const checkReady = createReadinessChecker(pool);
// 状態変化のときだけログを出す(ログ爆発を防ぐ)🧯
let lastReady: boolean | null = null;
app.get("/ready", async (_req, res) => {
const t0 = performance.now();
const r = await checkReady();
const ms = Math.round(performance.now() - t0);
if (lastReady !== r.ok) {
console.log(`[ready] state changed -> ${r.ok ? "READY ✅" : "NOT READY ❌"}`);
lastReady = r.ok;
}
if (r.ok) {
return res.status(200).json({
ok: true,
checks: { db: { ok: true, ms } },
});
}
return res.status(503).json({
ok: false,
checks: { db: { ok: false, ms, reason: r.reason } },
});
});
export default app;
5) 起動して動作確認 🚀
docker compose up -d --build
curl http://localhost:3000/ready
期待する感じ(例)👇
- OK時:
200と{ ok: true, ... } - NG時:
503と{ ok: false, ... }
ステータスコードも見たいとき👇
curl -i http://localhost:3000/ready
6) 依存を止める → /ready が失敗に変わる 😈➡️💥
docker compose stop db
curl -i http://localhost:3000/ready
✅ ここで 503 が返れば勝ちです 🏆✨
(理由が reason に入ってるのも最高👍)
7) 依存復帰 → /ready がOKに戻る 🩹➡️✅
docker compose start db
curl -i http://localhost:3000/ready
④ つまづきポイント(3つ)🪤
-
DBホストを
localhostにしてしまう 🥲 コンテナ内から見たlocalhostは「APIコンテナ自身」です。DBはdb(サービス名)にします 👍 -
/ready が重くて遅い 🐢
SELECT * FROM big_tableとかやると逆に障害を作ります 😇 “軽い疎通”だけにするのが鉄則です 🪶(依存チェックは軽く!) -
監視が連打してDBが死ぬ 🔁💥 ロードバランサやオーケストレータは高頻度で叩きます。 だから TTLキャッシュが地味に超重要です 🧊✨
⑤ ミニ課題(15分)⏳
できそうなのを1つやってみてね 😊
-
課題A(おすすめ):
/readyのレスポンスにsince(最終チェック時刻)を入れる ⏱️ -
課題B:DBが落ちた時、
reasonを「人間向けメッセージ」に変換して返す(例:DBに接続できません)🗣️ -
課題C:依存チェックを増やす(例:Redisや外部HTTP)➕🔌
- ただし「軽く」「短く」「叩きすぎない」ルールは守ること 🙏
⑥ AIに投げるプロンプト例(コピペOK)🤖📋
Copilot / Codex に投げる用👇
Express + TypeScriptで /ready エンドポイントを実装したい。
要件:
- DB(PostgreSQL)への軽い疎通として SELECT 1 を使う
- タイムアウト(1秒)で失敗扱いにする
- /ready が連打されてもDBを叩きすぎないように TTLキャッシュ(3秒)を入れる
- readyなら200 + JSON、readyでなければ503 + JSON(reason付き)
- 状態が変わった時だけログを出す(READY/NOT READY)
コード例を src/readiness.ts と src/server.ts の形で出して。
おまけ:この章の“設計のキモ”だけ一言で言うと 🧠✨
「受け付けていいか」を外に宣言するのが /ready です。
Composeも含めて、起動順やルーティング判断は “ready情報” があると超ラクになります。(Docker Documentation)
次章(第27章)で、この考え方を DockerのHEALTHCHECK に接続して「コンテナ自身の健康状態」にしていきます 🧪📦(start_period などの意味も、Docker Docsに定義があります)(Docker Documentation)