メインコンテンツまでスキップ

マイグレーション失敗時の運用手順書

1. 概要

本手順書は、CI / 本番デプロイで Drizzle のマイグレーションが失敗したときの切り分けと復旧手順を定めたものである。 属人化を防ぎ、本番事故時の MTTR(平均復旧時間)を短縮することを目的とする。

対象者

  • subseek のオンコール担当(リポジトリ管理者)

スコープ

  • ci.yml および deploy-production.ymlRun DB migrations (...) ステップが失敗したケース
  • マイグレーション後に発覚した論理バグ(カラムは追加されたがデータが壊れた等)

関連ドキュメント


2. 全体フロー


3. 失敗パターン分類

Pattern症状主因復旧手段
A. Syntax / Logicdrizzle-kit migrateSqliteError: near "..." 等で停止SQL 構文ミス、想定外の予約語、型不一致rollback で 1 step 巻き戻し or migration を修正して再 push
B. Constraint violationNOT 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 lagmigration 自体は成功するが、API のリードレプリカが古いスキーマを返し API エラーTurso プライマリと replica の同期遅延数分待つ。継続するなら Turso ダッシュボードでレプリカ再構築

4. Step 1: 影響評価(共通)

4.1 GitHub Actions のログを開く

  1. 失敗した PR / commit から該当 workflow ログを開く
    • staging: ci.ymldeploy-apiRun DB migrations (staging)
    • production: deploy-production.ymldeploy-apiRun DB migrations (production)
  2. 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.sqlbar 列に 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 からの restoreTurso 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"
既存 17 migrations は down SQL が無い

リポジトリ初期に作成された 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 秒以内)待っても解消しない場合:

  1. Turso ダッシュボードで該当 replica を一度削除して再作成
  2. Cloudflare Workers の API Worker を再起動(wrangler deploy で空 deploy)
  3. それでも復帰しなければ Turso サポートにエスカレーション

6. Step 3: 移行修正 PR の出し方

復旧後、原因を取り除いた migration を再度 PR にする。

# 1. 失敗 migration を修正
git checkout -b fix/migration-NNNN
# packages/db/migrations/NNNN_xxx.sql を edit

# 2. 対応する down SQL も書く(新規 migration なら必須)
# packages/db/migrations/down/down_NNNN_xxx.sql

# 3. ローカルで :memory: テストを実行
pnpm --filter @subseek/db vitest

# 4. PR を Draft で作成
gh pr create --draft \
--title "fix(db): Repair NNNN migration after production failure" \
--body "..."

PR 説明には以下を必ず含める:

  • 失敗時のエラーメッセージ
  • 修正内容(何を変えたか)
  • 復旧手段(rollback / restore のどちらを使ったか)
  • 再発防止(単体テストの追加、データ補正の前提)

7. エスカレーション基準

以下のいずれかに該当する場合、即座にチーム(GitHub Discussions または Slack #incidents)に連絡する。

条件連絡先
production の API が 5 分以上 5xx を返しているチーム全員(Slack #incidents
dump が R2 に保存されていない(pre-migration backup ステップが失敗していた)チーム全員
Turso の primary DB が応答しないTurso サポート + チーム全員
restore が成功せず再試行も失敗するチーム全員
個人情報(users / payments)テーブルでデータ損失の可能性プロダクトオーナー + 法務確認

TODO(仕様仮置き): Slack の #incidents チャンネル / オンコール輪番は未整備(v0.5 リリース後の運用設計)。当面は GitHub Issue に incident ラベルを付けて記録する。


8. Step 4: ポストモーテム

復旧後 24 時間以内に以下を埋めて Issue 化する。

ポストモーテムテンプレート

## 概要
- 発生日時:
- 影響範囲: (staging / production)
- 影響時間:
- 影響を受けたユーザー数(推定):

## タイムライン (UTC)
- HH:MM CI で migration 失敗を検知
- HH:MM Slack 通知
- HH:MM オンコール対応開始
- HH:MM 復旧完了

## 根本原因(Why)
- 何故失敗したか
- 何故レビューで気付けなかったか

## 復旧手順
- どの選択肢を取ったか(rollback / restore / PITR)
- なぜその選択肢が最善だったか

## 再発防止
- [ ] migration の単体テストを追加
- [ ] レビュー観点を CLAUDE.md に追記
- [ ] CI の dump サイズ監視

ポストモーテム Issue は bug + documentation ラベルを付け、v0.x の次マイルストーンに紐づける。


9. 関連ドキュメント