🔥

ヘルスチェック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つ:

  1. ヘルスチェックOKは「完全に正常」を意味しない
  2. マイグレーション記録の整合性は定期的に確認すべき
  3. 焦っているときこそ、基本に忠実に

schema_migrationsのレコード数とマイグレーションファイル数を比較する。たったこれだけのチェックで、今回の障害は防げました。

SELECT COUNT(*) FROM schema_migrations;
ls -1 migrations/*.sql | wc -l

同じ問題で悩む方の参考になれば幸いです。

Discussion