All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 56s
CD Pipeline / build-and-deploy (push) Successful in 4m16s
CD Pipeline / post-deploy-checks (push) Successful in 1m39s
134 lines
7.8 KiB
Markdown
134 lines
7.8 KiB
Markdown
# Public Maintenance Fallback Runbook
|
||
|
||
## 目標
|
||
|
||
主機或 VM 重啟時,public 網站不得直接露出空白 502。正式做法分兩層:
|
||
|
||
- L0 local edge:110 / 188 / 120 / 121 的 Nginx 仍可回應時,用 `ops/maintenance/maintenance.html` 攔截 `502` / `503` / `504`,顯示靜態維護頁。
|
||
- L1 external cloud/CDN:如果 99 / VMware / 家用路由 / 對外 IP 整段不可達,由外部雲端主機或 CDN health check 切到同一份靜態維護頁。
|
||
|
||
## 必要特性
|
||
|
||
- 維護頁不得讀取 `.env`、cookie、session、DB、K3s、Redis、NFS 或內部 API。
|
||
- 維護頁必須是靜態檔,能被 Nginx、Cloudflare Pages、S3/R2 static hosting、GCS static site 或任一低成本外部 VM 直接服務。
|
||
- L0 與 L1 使用同一份內容,避免使用者在不同故障層看到不同訊息。
|
||
- 502 fallback 只能處理 upstream failure;若整個 public edge unreachable,必須由 L1 接手。
|
||
|
||
## L0 受控套用
|
||
|
||
1. 將 `ops/maintenance/maintenance.html` 部署到 edge host 的 `/var/www/maintenance/maintenance.html`。
|
||
2. 在每個 public `server {}` 內 include `ops/maintenance/nginx-502-maintenance-snippet.conf` 的等價內容;AWOOOI source conf `ops/nginx/awoooi.wooo.work.conf` 與 `k8s/nginx/awoooi-prod.conf` 已固定 `proxy_intercept_errors on`、`error_page 502 503 504 =503 /__awoooi-maintenance.html` 與 `X-AWOOOI-Fallback: local-maintenance`。所有會 `proxy_pass` 到 AWOOOI API / Web 的 `location` 也必須明確帶同一組 `proxy_intercept_errors` / `error_page`,不得只依賴 server 層繼承。
|
||
3. `nginx -t` 通過後才 reload Nginx。
|
||
4. 用壞 upstream staging vhost 或暫存 upstream 驗證 `X-AWOOOI-Fallback: local-maintenance`,不得用 production DB / app restart 當測試手段。
|
||
|
||
## L1 外部雲端/CDN 建議
|
||
|
||
優先順序:
|
||
|
||
1. Cloudflare / CDN health check + fallback origin:最快切換,對使用者體驗最好。
|
||
2. 低成本外部 VM:可控性高,能同時跑 blackbox probe;需要 VM patching 與監控。
|
||
3. Object storage static hosting:成本最低,適合維護頁;DNS/CDN health check 仍要另外配置。
|
||
|
||
L1 active 條件:
|
||
|
||
- `probe_success` 對主要 public routes 連續 2 分鐘失敗,或
|
||
- `awoooi_reboot_event_detected == 1` 且 `awoooi_reboot_auto_recovery_slo_ready == 0` 超過 10 分鐘,或
|
||
- 99 / 110 / 188 / 120 / 121 / 112 任一 P0 host down 且 public route 同時 502 / timeout。
|
||
|
||
## 驗證
|
||
|
||
- L0:對測試 vhost 讀回 `503` 的維護頁 HTML,且 header `X-AWOOOI-Fallback=local-maintenance` 與 `Retry-After` 存在;probe 若讀到 `502` / `503` / `504` 但同時有 fallback header/body,必須分類為 maintenance fallback,不得算 raw 5xx。
|
||
- L1:從外部網路讀回靜態維護頁,且不經過 99 / 110 / 188 / 120 / 121 / 112。
|
||
- Recovery:主要 upstream 連續 2 分鐘健康後切回正式 origin;Telegram 發送 recovery 通知,並在 cold-start scorecard 留下 readback。
|
||
|
||
## 2026-07-02 scorecard gate
|
||
|
||
2026-07-02 晚間 Gitea CD `#4446` rollout 期間曾讀回 public `502`,後續正式 health 回到 `200`。這證明「source 有維護頁」不足以代表使用者不會看到 raw 502;驗收必須要求 runtime readback。
|
||
|
||
固定驗證命令:
|
||
|
||
```bash
|
||
python3 scripts/reboot-recovery/public-maintenance-fallback-probe.py \
|
||
--output /tmp/public-maintenance-fallback.json
|
||
python3 scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py \
|
||
--public-maintenance-file /tmp/public-maintenance-fallback.json
|
||
```
|
||
|
||
判斷規則:
|
||
|
||
- `public_route_raw_5xx_without_maintenance_fallback`:L0 Nginx intercept 未生效,下一步只能做 staging vhost / `nginx -t` / header verifier,不得用 production app restart 當測試。
|
||
- `public_route_unreachable_without_external_l1_fallback`:edge 或外部路徑整段不可達,下一步是 L1 external static origin / CDN health check decision record 與 rollback,不直接切 DNS。
|
||
- `public_maintenance_fallback_runtime_readback_missing`:沒有 runtime probe 證據,不得宣稱 public maintenance fallback 已完成。
|
||
|
||
## 2026-07-02 L0 source apply
|
||
|
||
已將 AWOOOI edge Nginx source 從 legacy `/50x.html` 改為 L0 maintenance fallback:
|
||
|
||
- `ops/nginx/awoooi.wooo.work.conf`
|
||
- `k8s/nginx/awoooi-prod.conf`
|
||
- `ops/maintenance/nginx-502-maintenance-snippet.conf`
|
||
|
||
驗收仍必須分兩層:
|
||
|
||
- Source verifier:`scripts/reboot-recovery/tests/test_reboot_p0_operational_contract.py` 必須確認上述 source 不可退回 `/50x.html`。
|
||
- Runtime verifier:在 edge host 用 staging vhost / `nginx -t` / header readback 證明 `X-AWOOOI-Fallback=local-maintenance`;通過前不得宣稱 production runtime 已接管 raw 502。
|
||
|
||
## 2026-07-03 API / SLO cutover raw 502 hardening
|
||
|
||
Gitea CD `#4517` 在 deploy marker 切換期間再次讀到 production API / SLO route 短暫 `502`,且沒有 maintenance body / fallback header。修正口徑:
|
||
|
||
- `error_page` 必須使用 `=503`,讓維護頁帶明確 `503` / `Retry-After`,而不是回傳裸 `502`。
|
||
- AWOOOI API、SSE、WebSocket、front-end 與 static proxy location 必須各自明確宣告 `proxy_intercept_errors on` 與同一條 `error_page`。
|
||
- `public-maintenance-fallback-probe.py` 必須把帶 `X-AWOOOI-Fallback=local-maintenance` 與維護頁 body 的 `502` / `503` / `504` 判成 fallback serving;只有缺 header/body 的 5xx 才是 `raw_5xx_without_maintenance_fallback`。
|
||
- 驗收仍以 marker-time public route watch 為準;source verifier 通過不等於 live Nginx runtime 已 reload。
|
||
|
||
## 2026-07-03 #4519 live edge drift
|
||
|
||
`#4519` 部署 source `a94ddd5` 後,deploy marker `3aca484 chore(cd): deploy a94ddd5 [skip ci]` 已推上 Gitea main,build log 也讀到 production deploy readback matched;但 marker 後 public probe `/tmp/awoooi-public-maintenance-fallback-after-4519-20260703-0433.json` 仍回:
|
||
|
||
- `https://awoooi.wooo.work/api/v1/health`:HTTP `502`
|
||
- `X-AWOOOI-Fallback`:空
|
||
- maintenance body:false
|
||
- `raw_5xx_without_fallback_count=1`
|
||
|
||
live edge readback:
|
||
|
||
- 188 readable config:`/etc/nginx/sites-enabled/awoooi.wooo.work.conf`
|
||
- config 仍走 VIP `192.168.0.125:32334/32335`
|
||
- config 缺 `proxy_intercept_errors` / `error_page 502 503 504 =503` / fallback header
|
||
- `/var/www/maintenance/maintenance.html` 缺失
|
||
- `ollama@192.168.0.188` 無 passwordless sudo,不能由一般 SSH channel apply
|
||
|
||
因此 P0-2 目前 blocker 不是 app deploy,而是 live edge source-to-runtime drift。下一步固定:
|
||
|
||
```bash
|
||
scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh --check
|
||
```
|
||
|
||
若 `SUDO_N_READY=1`,才能執行:
|
||
|
||
```bash
|
||
scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh --apply
|
||
python3.11 scripts/reboot-recovery/public-maintenance-fallback-probe.py --timeout 8 \
|
||
--output /tmp/public-maintenance-fallback-after-edge-apply.json
|
||
```
|
||
|
||
若 `SUDO_N_READY=0`,不得讀密碼、不得重啟 Nginx、不得用 app restart 當測試;需走 console/root-controlled apply 或維護窗口。
|
||
|
||
## 2026-07-02 API runtime artifact overlay
|
||
|
||
`/api/v1/agents/reboot-auto-recovery-slo-scorecard` 不得只依賴 committed snapshot 判斷 public maintenance fallback;它必須能讀取 `reboot-auto-recovery-slo-exporter.sh` 產出的 runtime artifact。
|
||
|
||
讀取順序:
|
||
|
||
1. `AWOOOI_PUBLIC_MAINTENANCE_FALLBACK_READBACK_FILE` 指定的 JSON。
|
||
2. `/home/wooo/reboot-recovery/reboot-auto-recovery-slo-*/public-maintenance-fallback.json` 中最新一份。
|
||
3. 找不到 artifact 時維持 fail-closed `public_maintenance_fallback_runtime_readback_missing`。
|
||
|
||
驗收:
|
||
|
||
- Production API 讀回 `readback.public_maintenance_runtime_readback_present=true`。
|
||
- Production API 讀回 `readback.public_maintenance_fallback_ready=true`。
|
||
- `active_blockers` 不包含 `public_maintenance_fallback_runtime_readback_missing`。
|
||
- 若 public routes 都正常,這只代表「沒有裸 5xx 且 runtime artifact 已接上 scorecard」;仍需 staging vhost / `nginx -t` / header readback 才能宣稱 L0 intercept 在 upstream failure 時生效。
|