マイグレーション失敗時の運用手順書
1. 概要
本手順書は、CI / 本番デプロイで Drizzle のマイグレーションが失敗したときの切り分けと復旧手順を定めたものである。 属人化を防ぎ、本番事故時の MTTR(平均復旧時間)を短縮することを目的とする。
対象者
- subseek のオンコール担当(リポジトリ管理者)
スコープ
ci.ymlおよびdeploy-production.ymlのRun DB migrations (...)ステップが失敗したケース- マイグレーション後に発覚した論理バグ(カラムは追加されたがデータが壊れた等)
関連ドキュメント
packages/db/README.md—dump/rollback/restoreの使い方apps/docs/docs/specification/deploy-architecture.md— CI / デプロイのフロー- #318 — ロールバック / ダンプ機能の Issue
2. 全体フロー
3. 失敗パターン分類
| Pattern | 症状 | 主因 | 復旧手段 |
|---|---|---|---|
| A. Syntax / Logic | drizzle-kit migrate が SqliteError: near "..." 等で停止 | SQL 構文ミス、想定外の予約語、型不一致 | rollback で 1 step 巻き戻し or migration を修正して再 push |
| B. Constraint violation | NOT NULL constraint failed / UNIQUE constraint failed / FOREIGN KEY constraint failed | 既存データが新制約に違反 | データ補正 → migration を修正 or ダンプから restore |
| C. Timeout / Lock | ステップが 5 分以上応答せず GitHub Actions の hard timeout に達する | 大量データへの ALTER、長時間ロック | Turso 側でクエリ強制終了 → ダンプから restore(部分実行された変更を巻き戻し) |
| D. Replica lag | migration 自体は成功するが、API のリードレプリカが古いスキーマを返し API エラー | Turso プライマリと replica の同期遅延 | 数分待つ。継続するなら Turso ダッシュボードでレプリカ再構築 |
4. Step 1: 影響評価(共通)
4.1 GitHub Actions のログを開く
- 失敗した PR / commit から該当 workflow ログを開く
- staging:
ci.yml→deploy-api→Run DB migrations (staging) - production:
deploy-production.yml→deploy-api→Run DB migrations (production)
- staging:
Run DB migrationsの直前にDump ... DB to R2 (pre-migration backup)ステップが成功しているかを確認する- 成功していれば R2 にバックアップがある(後述の復旧で使う)
- 失敗していればロールバック前提(dump が無いため restore できない)
4.2 失敗位置を特定する
drizzle-kit のログから「どの migration ファイル」「どのステートメント」で失敗したかを読む。
[i] Migration 0017_add_xxx applied!
[i] Migration 0018_add_yyy applied!
[!] Migration 0019_change_zzz failed
SqliteError: NOT NULL constraint failed: foo.bar
at Statement.run ...
→ 0019_change_zzz.sql の bar 列に NOT NULL を付けたが既存データが NULL を持っていた、と判断できる。
4.3 本番影響の判定
| 状況 | 判断 |
|---|---|
| staging 失敗のみ・production は未デプロイ | 落ち着いて migration を修正、PR を再 push |
| production 失敗・API がデプロイ済みで DB スキーマと不一致 | 緊急。Step 2 復旧へ |
| production 失敗・API はまだ旧バージョン | API は旧スキーマで動作継続中。Step 2 復旧をやるか、roll forward で migration 修正を急ぐかを判断 |
5. Step 2: 復旧手順
5.1 復旧手段の選び方
| 失敗パターン | 第 1 選択 | 第 2 選択 |
|---|---|---|
| Pattern A(最新 migration のみで止まった) | rollback で 1 step 巻き戻し | dump からの restore |
| Pattern B(データ違反) | データ補正 SQL を実行 → migration を修正して再実行 | dump からの restore |
| Pattern C(部分実行されている可能性) | dump からの restore | Turso PITR |
| Pattern D(スキーマは正しいが反映遅延) | 待機 | レプリカ再構築 |
5.2 Pattern A: rollback で 1 step 巻き戻し
最新の migration が forward only に書かれていて、対応する down_<tag>.sql がある場合。
# 1. 復旧ブランチを切る
git checkout -b hotfix/rollback-NNNN
# 2. 環境変数をセット(production の例)
export DATABASE_URL='libsql://...'
export DATABASE_AUTH_TOKEN='...'
# 3. ドライランで実行 SQL を確認
pnpm --filter @subseek/db rollback -- --dry-run
# 4. 1 step 巻き戻す
pnpm --filter @subseek/db rollback
# 5. journal が更新されたことをコミット
git add packages/db/migrations/meta/_journal.json
git commit -m "chore(db): Rollback migration NNNN_xxx"
リポジトリ初期に作成された 0001〜0017 の migration は down_<tag>.sql を持たない。これらの巻き戻しは rollback では失敗する(down SQL が見つかりません で停止)。dump からの restore に切り替えること。
5.3 Pattern B: データ補正 + migration 修正
constraint violation が出た場合、既存データが新制約と矛盾している。
# 1. 違反データを特定(例: NOT NULL 違反)
turso db shell <DB_NAME> "SELECT id FROM foo WHERE bar IS NULL"
# 2. データ補正 SQL を実行
turso db shell <DB_NAME> "UPDATE foo SET bar = 'default' WHERE bar IS NULL"
# 3. migration を修正(例: DEFAULT 値を加える)
# packages/db/migrations/0019_xxx.sql を edit
# 4. drizzle-kit に再適用させる
pnpm --filter @subseek/db exec drizzle-kit migrate
migration 修正で済む場合は rollback も restore も不要(journal は次回 PR で前進する)。
5.4 Pattern C: dump から restore
部分実行で DB が中途半端な状態になった、または rollback できない場合。
# 1. R2 から最新ダンプを取得(CI で自動保存されている)
pnpm dlx wrangler@latest r2 object get \
"subseek-db-dumps/production/2026-04-24T03-15-00Z.sql" \
--file ./recovered.sql
# 2. ダンプ内容のスポットチェック(テーブル一覧・行数)
head -20 ./recovered.sql
grep -c "INSERT INTO" ./recovered.sql
# 3. 既存テーブルを drop してから restore(既存テーブルが残っていると CREATE で衝突する)
# Turso CLI で全テーブル drop するか、空 DB を新規作成してそこに当てる
# 4. restore
DATABASE_URL='libsql://...' \
DATABASE_AUTH_TOKEN='...' \
pnpm --filter @subseek/db restore -- ./recovered.sql
# 5. journal も巻き戻す(ダンプ作成時点に合わせる)
# ダンプには __drizzle_migrations が含まれているので、その中身に合わせて
# packages/db/migrations/meta/_journal.json の末尾 entry を削除する
ダンプは「論理ダンプ」で大量データ時に時間がかかる。本番で 1GB 超を扱う場合は Turso 公式の Point-in-Time Recovery(PITR) が推奨される(後述)。
5.5 Pattern C 補助: Turso PITR
Turso は DB ごとに直近 24 時間のポイントインタイム復元をサポートしている。
TODO(仕様仮置き): 本機能の本番運用フローは未確定(#319 後続)。 一般論として以下のコマンドで復元できる:
# 1. 失敗 1 分前の時刻に復元したコピー DB を作成
turso db create subseek-prod-recovery \
--from-db subseek-prod \
--timestamp '2026-04-24T03:14:00Z'
# 2. 復元先で動作確認
turso db shell subseek-prod-recovery "SELECT count(*) FROM users"
# 3. アプリの DATABASE_URL を切り替え(GitHub Secrets を更新して redeploy)注意: 切り替え後は元 DB の名前を入れ替えるかアプリ側 URL を更新する必要があり、ダウンタイムが発生する。決定後にこの節を埋めること。
5.6 Pattern D: レプリカ反映遅延
migration 自体は完了したのに API がスキーマ不一致のエラー(no such column: ...)を返す場合。
# Turso のレプリカ状況を確認
turso db inspect <DB_NAME>
数分(通常 30 秒以内)待っても解消しない場合:
- Turso ダッシュボードで該当 replica を一度削除して再作成
- Cloudflare Workers の API Worker を再起動(
wrangler deployで空 deploy) - それでも復帰しなければ Turso サポートにエスカレーション