ヘルスチェックOKなのに500エラー?マイグレーション記録漏れが引き起こした「静かな障害」
はじめに
「デプロイ完了、ヘルスチェックOK」
その後動作確認すると「画面が見れない」……
調べてみると、アプリケーションは正常に起動している。でも特定のAPIだけが500エラー。原因は、マイグレーションツールが「未適用」と誤判断したことでした。
普段は何も考えずに動いてくれるマイグレーションツール。その「信頼」が裏切られた瞬間の記録です。
インシデント概要
| 項目 | 内容 |
|---|---|
| 発生日時 | 2026-01-17 07:31 JST |
| 解決日時 | 2026-01-17 08:10 JST |
| 影響時間 | 約40分 |
| 重大度 | High(本番APIが500エラー) |
| 影響範囲 | シフト枠一覧画面が表示不可 |
タイムライン
07:22 v1.7.0デプロイ開始
07:26 デプロイ完了、ヘルスチェックOK ← この時点では「成功」に見えた
07:31 ユーザーから500エラー報告
07:35 原因特定(DBにinstance_idカラムが存在しない)
07:42 v1.6.0へロールバック開始
07:46 ロールバック完了(しかし別の問題が発生)
07:47 Viteエラー発生 ← 焦って開発用docker-composeを使ってしまった
07:55 本番用docker-compose.prod.ymlで再起動、v1.6.0で復旧
08:01 マイグレーション修正作業開始
08:03 マイグレーション037-039適用完了
08:10 v1.7.0再デプロイ完了、正常稼働確認
なぜ「静かな障害」が起きたのか
問題の連鎖
今回の障害は、単純なエラーではなく5段階の連鎖で発生しました。
① schema_migrationsに記録漏れがあった
↓
② マイグレーションツールが「026は未適用」と誤判断
↓
③ 026を実行しようとするが、テーブルは既に存在 → エラーで中断
↓
④ 037-039(新機能に必要なカラム追加)が適用されないまま終了
↓
⑤ アプリケーションは起動成功、しかしコードが参照するカラムがDBにない → 500エラー
なぜ気づけなかったのか
ヘルスチェックは通っていたからです。
/healthエンドポイントは「アプリケーションが起動しているか」を確認するだけ。マイグレーションが正常に完了したか、主要なAPIが動作するかまでは見ていませんでした。
これが「静かな障害」の怖さです。監視が緑でも、ユーザーには赤という状況が起きうる。
schema_migrationsの仕組み
マイグレーションツールはschema_migrationsテーブルを見て、どのマイグレーションが適用済みかを判断します。
schema_migrationsに記録: version 1-25(25件)
実際にDBに適用済み: version 1-36(36件相当)
v1.7.0が必要とする: version 1-39(39件)
過去のどこかで、マイグレーションは実行されたのに記録だけが漏れていた。ツールから見れば「026以降が未適用」に見える状態でした。
この不整合がいつ発生したのかは特定できていません。おそらく手動でのDB操作や、過去のトラブル対応時に記録を更新し忘れたのだと思います。
復旧手順
Step 1: 緊急バックアップ
何よりもまずバックアップ。これは正解でした。
docker exec db pg_dump -U user database > /path/to/backup_emergency.sql
Step 2: 安定版へロールバック
本番用のcomposeファイルを使う。
docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d
実はここで一度ミスをしています。焦ってdocker-compose.yml(開発用)を使ってしまい、Vite開発サーバーが起動。本番ドメインからのアクセスがブロックされました。
Blocked request. This host ("example.com") is not allowed.
焦っているときこそ基本に忠実に。
Step 3: マイグレーション記録の修正
不整合を解消するため、実際に適用済みのマイグレーションを記録に追加しました。
INSERT INTO schema_migrations (version) VALUES
(26), (27), (28), (29), (30), (31), (32), (33), (34), (35), (36);
この手動INSERTによる修正は、調べてみると複数の技術記事で推奨されている一般的なアプローチでした。
Step 4: 残りのマイグレーション適用
docker exec backend /app/migrate -action=up
Step 5: 再デプロイ
v1.7.0を再度デプロイし、正常稼働を確認。
教訓
うまくいったこと
- 最初にバックアップを取った:これがなければ、どんな修正も怖くてできなかった
-
原因特定が早かった(4分):エラーログに
instance_idが明記されていた - 手動INSERTの判断:調査の結果、これは業界標準のアプローチだった
うまくいかなかったこと
- ロールバック時に開発用composeを使ってしまった:焦りによる二次障害
- マイグレーション記録の不整合に事前に気づけなかった:定期的なチェックの仕組みがなかった
幸運だったこと
- 影響範囲が限定的だった:シフト枠画面だけで、他の機能は正常動作
- 問題発見が早期であった:監視で検知できなかったが、5分で発見できた
再発防止策
実施済み
- [x] デプロイ前チェックリストにマイグレーションstatus確認を追加
- [x] schema_migrationsの件数とマイグレーションファイル数の一致確認を追加
- [x] 本番サーバーから開発用docker-compose.ymlを削除
実施予定
高優先度
| 対策 | 目的 |
|---|---|
| マイグレーション失敗時はデプロイを中止する | 「アプリは起動したがDBが不整合」を防ぐ |
| ヘルスチェックに主要APIエンドポイントを追加 | 「静かな障害」の検知 |
| マイグレーション実行前の自動バックアップ | 復旧の保険 |
中優先度
| 対策 | 目的 |
|---|---|
| ステージング環境での事前テスト | 本番デプロイ前の検証 |
| マイグレーションのDry run(プレビュー)機能 | 実行前に何が起きるか確認 |
| マイグレーションファイル変更時のレビュー必須化 | ヒューマンエラーの低減 |
まとめ
マイグレーションツールは普段、何も考えずに動いてくれます。だからこそ、その「信頼」が裏切られたときに気づきにくい。
今回の障害で学んだことは3つ:
- ヘルスチェックOKは「完全に正常」を意味しない
- マイグレーション記録の整合性は定期的に確認すべき
- 焦っているときこそ、基本に忠実に
schema_migrationsのレコード数とマイグレーションファイル数を比較する。たったこれだけのチェックで、今回の障害は防げました。
SELECT COUNT(*) FROM schema_migrations;
ls -1 migrations/*.sql | wc -l
同じ問題で悩む方の参考になれば幸いです。
Discussion