# Disaster Recovery Drill Runbook

> **SSoT 参照**: バックアップ仕様は [supabase-branching.md](supabase-branching.md) §バックアップ
> および本 runbook で定義。Severity / 通知は
> [notification-strategy.md](notification-strategy.md) と
> [incident-response.md](incident-response.md) を参照。

Parky の本番障害 (DB 全損 / R2 全損 / Workers 設定全損) 想定で、復旧手順 (RTO/RPO 定義) と半年ごとの dry-run drill を定義する。

## 1. RTO / RPO 目標

| 対象 | RPO | RTO | 備考 |
|---|---|---|---|
| **Supabase Postgres (parky)** | **5 分** (PITR) | **30 分** | Supabase Pro plan の自動 PITR (7 日保持) |
| **R2 (parky-prod bucket)** | **24 時間** (logs lifecycle) | **2 時間** | バケット versioning 未有効化のため、誤削除復旧は不可。Phase 2 で versioning 検討 |
| **R2 (parky-instagram-assets)** | 同上 | 1 時間 | 公開 CDN だが書き込みは admin のみ |
| **Cloudflare Workers (admin/marketing/public)** | **0 分** (git管理) | **5 分** | `wrangler deploy` で即復旧。secrets は 1Password から再投入。store-sync queue consumer は admin Worker 内 (2026-05-11〜) |
| **Cloudflare KV (idempotency / rate limit)** | 失われても OK | n/a | ephemeral data。失われても運用継続可能 |
| **Stripe events** | **0 分** (Stripe 側保持) | n/a | Stripe Dashboard から webhook event を 30 日間 replay 可能 |

**Phase 1 SLA 公約**:
- 計画外停止 8 時間 / 月以下 (= 99.0% 月次 uptime, slo-error-budget.md と整合)
- 重大データ消失 0 件 / 半年

## 2. 障害シナリオ別復旧手順

### 2.1 Supabase Postgres 全損 / 誤 DROP

**症状**: `api.parky.co.jp/healthz/ready` が `db: false`、または admin が「テーブルがない」エラー。

**復旧フロー** (RTO 30 分目標):

```
1. (5 分) インシデント宣言
   - #p0-alerts に SEV1 発報 (incident-response.md)
   - Status page (Phase 2 で stub) に「DB 復旧中」表示
   - api.parky.co.jp を maintenance mode (Cloudflare Page Rule) に

2. (5 分) Supabase Dashboard で PITR ターゲット時刻決定
   - Supabase Dashboard → Project parky → Database → Backups → Point in Time Recovery
   - 障害発生 1 分前の timestamp (UTC) を選択
   - 復元先は 「Restore in place」(同 project に上書き) ※ 別 project に restore する option もあり、
     検証ありなら別 project → DNS 切替の方が安全 (RTO は 60 分に伸びる)

3. (15 分) PITR 実行を待つ
   - Supabase 側で自動。完了通知メールが `dev@parky.co.jp` (Supabase project owner) に届く

4. (3 分) 動作確認
   - api/scripts/healthcheck.sh dev で /healthz/ready を叩く
   - mcp__supabase__list_tables で主要テーブルが存在することを確認
   - mcp__supabase__execute_sql で row 件数 sanity check (parking_lots > 100 等)

5. (2 分) maintenance mode 解除
   - Cloudflare Page Rule 削除 → api.parky.co.jp 再開
   - #p0-alerts に復旧通知

6. ポストモーテム作成
   - docs/ops/postmortem-template.md をコピーして docs/ops/postmortems/<date>.md
   - 24 時間以内に root cause + 再発防止策を記入
```

**注意**:
- PITR は **同 project への in-place restore が破壊的** (現行データを上書き)。事故的に正常運用中に走らせないように、Supabase Dashboard のアクセスを admin role 限定に。
- Restore 後、`api-rls-tests` workflow を手動 dispatch して RLS が破損していないことを確認。

### 2.2 R2 bucket 全損 / 誤削除

**症状**: 画像 URL (`cdn.parky.co.jp/...`) が 404、admin の画像アップロードが失敗。

**復旧フロー** (RTO 2 時間目標):

```
1. (5 分) インシデント宣言、CDN cache purge を停止
2. (10 分) 何が消えたか調査
   - Cloudflare Dashboard → R2 → parky-prod → metrics で objects 数の急減を確認
   - wrangler r2 object list parky-prod で残存 object を列挙
3. (60 分) ソース取り直し
   - Stripe / Mapbox 系 asset は外部から再取得可
   - ユーザー UGC (アップロード画像) は失われた可能性あり → ユーザー通知
   - Instagram assets は parky-instagram-assets (別 bucket) を確認、生きていれば不要
4. (30 分) 投入
   - admin の bulk upload UI 経由か、wrangler r2 object put でリストア
5. (5 分) CDN cache purge → 動作確認
```

**改善 TODO (Phase 2)**:
- R2 bucket versioning 有効化 (= 誤削除から自動復旧可能、ストレージコスト 2x)
- 主要 bucket の毎週 snapshot を別 bucket にコピー (cron worker)

### 2.3 Cloudflare Workers 設定全損 / 誤 deploy

**症状**: `api.parky.co.jp/*` が 500、または旧 secrets で起動して KV / R2 が 403。

**復旧フロー** (RTO 5 分目標):

```
1. (1 分) 直前の正常 commit を git log で特定
   git log -10 --oneline -- 'api/wrangler*.toml' 'api/src'

2. (1 分) いずれかの方法で前バージョンに戻す:
   (a) wrangler 直接 rollback (worker 単位、最速):
       cd parky/api
       npx wrangler rollback --config wrangler.public.toml --env prod
       npx wrangler rollback --config wrangler.admin.toml --env prod
       npx wrangler rollback --config wrangler.marketing.toml --env prod
       # 2026-05-11: wrangler.store-sync.toml は削除済 (queue consumer は admin Worker 内)
   (b) スクリプト経由 (Discord 通知 + canary 中断まで自動):
       bash scripts/deploy/rollback.sh api prod --confirm --reason="DR drill"
   (c) git revert で前 SHA に戻し push → GitHub Actions の deploy-api-{public,admin,marketing}-prod.yml が走る (production environment approval 要)

3. (1 分) secrets 不整合なら再投入
   bash api/scripts/set-secrets.sh prod   # 1Password 経由で投入 (worker × secret 単位の選択も可)

4. (2 分) 動作確認
   curl -fsS https://api.parky.co.jp/healthz/ready
```

**注意**: canary deploy (CF Workers Versions API) は dev / stg では ENABLED、prod は `if: false` で disable 中。詳細は `docs/ops/canary-deploy.html` 参照。緊急 rollback の自動化は `scripts/deploy/rollback.sh`、平常 rollback は `docs/ops/deployment-rollback.md` 参照。

### 2.4 Stripe webhook 再生 (整合性回復)

**症状**: subscription_events テーブルに event が来ていない時間帯がある (Stripe Dashboard で配信失敗確認)。

**復旧フロー**:

```
1. Stripe Dashboard → Developers → Webhooks → Endpoint → Failed events
2. 個別 retry または bulk replay (Stripe 側 30 日保持)
3. webhook 受信側 (bff/webhooks/stripe.ts) は二段冪等
   (event_id PK + payment_intent partial UNIQUE) なので重複投入も安全
4. subscription_events 投入後の整合性確認:
   SELECT count(*) FROM subscription_events WHERE received_at > '<window_start>';
```

## 3. 半年ごとの DR Drill (実施テンプレ)

**目的**: 上記手順が机上で書かれているだけでなく、実際に復旧できることを担保する。

**頻度**: 毎年 1 月と 7 月の第 2 月曜 14:00 JST (40 分枠)。

**実施手順** (drill 当日):

```
1. (準備) 専用 Supabase project parky-drill を作成 (本番 schema apply 済)
2. (5 分) drill scenario 選択 (#2.1, #2.2, #2.3 から 1 つランダム)
3. (30 分) 本 runbook の手順を実機で実行 (本番には触らない)
4. (5 分) 経過時間 vs 目標 RTO の差分を計測、本 runbook に追記
```

**Drill log テンプレ** (実施毎に追記):

```markdown
### YYYY-MM-DD Drill #N (担当: <name>)

- シナリオ: #2.X (Supabase / R2 / Workers)
- 目標 RTO: 30 分
- 実 RTO: ?? 分
- 詰まったポイント:
  - (例) Supabase Dashboard の PITR メニュー位置を覚えていなかった (3 分ロス)
  - (例) wrangler secret put の 1Password fetch が rate limit (5 分ロス)
- 改善 action:
  - [ ] (#1) 本 runbook の §2.1 step 2 にスクリーンショット追記
  - [ ] (#2) 1Password rate limit 対策として op-cache を pre-warm するスクリプト追加
- 手順本体の更新差分: (commit SHA)
```

## 4. 連絡先 / エスカレーション

| Role | 連絡先 | 役割 |
|---|---|---|
| Primary on-call | dev@parky.co.jp / Discord (oncall.md 参照) | 初動 + ポストモーテム |
| Supabase Support | https://supabase.com/dashboard/support/new | Pro plan で 24h SLA |
| Cloudflare Support | dashboard chat (Pro plan) | Workers / R2 / DNS |
| Stripe Support | dashboard chat | Payment / webhook |

## 5. 関連ドキュメント

- [incident-response.md](incident-response.md) — トリアージ手順
- [oncall.md](oncall.md) — on-call ローテーション
- [notification-strategy.md](notification-strategy.md) — 通知ルーティング
- [deployment-rollback.md](deployment-rollback.md) — Workers rollback 詳細
- [supabase-branching.md](supabase-branching.md) — Supabase 環境構成
- [postmortem-template.md](postmortem-template.md) — ポストモーテムテンプレート
- [secret-rotation.md](secret-rotation.md) — secrets 再投入手順
