visionA/docs/autoflow/07-delivery/deployment-guide.md
jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。

OIDC / pairing FK 修復(接 DB 上線必要):
- 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback
  驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID)
- pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token;
  自建 device 空 serial 寫 NULL(避免撞 partial unique)
- device.SaveTx / session.CreateTx 新增 tx-aware 版本

B4 model metadata:
- 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model:
  converter_client → flow → adapter → model.Model → PG → ModelResponse DTO
- input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性
- 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md

nginx healthz(部署層):
- 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB
- 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因)

storage error 統一映射(不洩漏 storage 後端細節)。

測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、
internal/db(redis)、relay/session 弱處,含 testcontainers integration。
DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:36:35 +08:00

222 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA 部署指南
> 本檔聚焦「DB 接入版 backend 部署到 stage 並接真 PG/Redis」的端到端流程與煙測結果。
> stage 基礎設定host nginx、HTTPS termination、docker daemon 連線)另見同目錄 `stage-deployment-setup.md`
> Phase 0.6 交接見 `phase-0.6-handover.md`。
## 1. 架構總覽DB 接入後)
```
公司 host nginx (HTTPS termination, LE 證書)
│ stage-9527.innovedus.com:9527
visiona container (image visiona:stage)
├─ 內層 nginx :80 ── 反代 ──► api-server :3721 (/api/*)
│ ── 反代 ──► frontend standalone (Next.js)
│ ── /healthz → return 200淺層 liveness不打 backend
│ ── /healthz/deep → 反代 api-server /healthz深層 readinessping PG+Redis見 §5/§9
├─ api-server :3721
│ ├─ Postgres pool ──► 192.168.0.130:5432/visiona真 PG
│ └─ Redis client ──► visiona-redis:6379/0真 Redis
└─ remote-proxy :3800/:3801tunnel
docker network: visiona-stage_default
├─ visiona 172.19.0.2
└─ visiona-redis 同網段hostname 解析靠這個 network
```
DB-on 啟用條件(`.env.stage` 已設):
- Postgres`VISIONA_DB_HOST` + `VISIONA_DB_USER` + `VISIONA_DB_NAME` 非空 → 自動建池 + auto-migrate + repository 切 Postgres。
- Redis`VISIONA_REDIS_HOST` 非空(無密碼也算啟用)→ userSession 切 Redis、cookie session 持久化。
- `VISIONA_DB_AUTO_MIGRATE` 預設 **true**`internal/config/load.go:109`)→ 啟動自動跑 `migrate up`
## 2. 前置需求
| 項目 | 內容 |
|------|------|
| docker daemon | stage host 130 開 `tcp://192.168.0.130:2375`公司內網直連VPN 大流量會卡,見 deploy-stage-v2.sh 註解) |
| `.env.stage` | 含 DB + Redis + OIDC + storage 等 env**git-ignored不進 repo**(已驗證 `git check-ignore` 命中) |
| 真 PG | 192.168.0.130:5432 db=`visiona` user=`vsausr` sslmode=`disable`schema 已 migrate 到 version 3 |
| 真 Redis | container `visiona-redis`network `visiona-stage_default`無密碼6379 |
| 部署腳本 | `scripts/deploy-stage-v2.sh`remote build 模式build 全在 stage daemon 跑) |
## 3. 部署步驟DB 接入版)
### 3.1 network 接 visiona-redis — 怎麼解決的(關鍵)
backend 用 hostname `visiona-redis` 連 Redis**必須跟 visiona-redis 同 docker network**否則解析失敗。
解決方式:**靠 compose project name 自動對上**,不需改 compose。
- `deploy-stage-v2.sh``-p visiona-stage` 起 compose → 預設 network 名為 `visiona-stage_default`
- `visiona-redis` 本來就在 `visiona-stage_default`Uplabel 顯示屬於同 stack
- 因此新 `visiona` container 起來自動落在同網段,`visiona-redis` hostname 直接可解。
驗證(部署前):
```bash
export DOCKER_HOST=tcp://192.168.0.130:2375
docker run --rm --network visiona-stage_default redis:alpine redis-cli -h visiona-redis ping # → PONG
docker run --rm --network visiona-stage_default postgres:16-alpine pg_isready -h 192.168.0.130 -p 5432 # → accepting connections
```
⚠️ 注意:`docker-compose.stage.yml` 沒有顯式宣告 external network。目前能對上是因為「compose 自建的 default network 名稱 = visiona-redis 所在的 network 名稱」這個巧合(同 project name。若未來 visiona-redis 改由別的 compose stack 管、或改 project name需在 compose 顯式 `networks:` 接 external `visiona-stage_default`。建議後續硬化(見 §10
### 3.2 部署指令
```bash
# 1. 先備份當前 image 供 rollback
export DOCKER_HOST=tcp://192.168.0.130:2375
CURID=$(docker inspect visiona --format '{{.Image}}' | cut -c8-19)
docker tag "$CURID" visiona:stage-rollback-pre-db
# 2. build + deploy main HEADremote build全在 stage daemon 跑)
DOCKER_HOST=tcp://192.168.0.130:2375 bash scripts/deploy-stage-v2.sh
```
部署會 `Recreate` visiona container覆蓋舊版。本次部署版本**main HEAD 4d0b870**DB 接入塊 0-5image tag `visiona:stage` + `visiona:stage-20260620-184814-4d0b870`
## 4. 啟動煙測本次實測2026-06-20
啟動 log 關鍵行(`.autoflow/07-delivery/logs/block6-container-startup-202606201848.log`
```
postgres pool initialized target=192.168.0.130:5432/visiona sslmode=disable max_conns=10 min_conns=2
migrate up: applied version=3
migrations applied target=192.168.0.130:5432/visiona
redis client initialized target=visiona-redis:6379/0 db=0
pairing/session token stores initialized backend=postgres
user session store initialized backend=redis
device repository initialized backend=postgres
device unpairer initialized backend=postgres-tx
model repository initialized backend=postgres
conversion service initialized converter_base_url=http://192.168.0.130:9501
file access (FAA download) initialized Phase 0.9 功能正常)
api-server listening addr=0.0.0.0:3721
```
無 fatal / panic。所有 6 個 store 都顯示 `backend=postgres` / `backend=redis`DB-on 模式生效)。
## 5. healthz 行為(重要)
> **2026-06-20 更新**§8 的 nginx /healthz 不反映 DB 健康問題**已修復並重部署**(見 §8、§9
> 現在 stage 有兩條對外健康路由:淺層 `/healthz`(存活)與深層 `/healthz/deep`readiness反映 PG+Redis
| 路徑 | 回應(健康時) | 說明 |
|------|------|------|
| `https://stage-9527.innovedus.com:9527/healthz`(公開,淺層) | `200 "ok"` | nginx 直接 return不打 backend、**不反映 DB**。用途liveness / 「nginx 活著」探測。 |
| `https://stage-9527.innovedus.com:9527/healthz/deep`(公開,深層)✅ 新增 | `200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}` | nginx `proxy_pass` 到 backend `/healthz`**會 ping PG + Redis、down 回 503**。用途:**LB readiness**。 |
| backend `:3721/healthz`(容器內) | `200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}` | 深層路由的後端來源,真正 ping PG + Redis。 |
backend healthz 實作(`internal/api/health.go`):每次呼叫 ping PG + Redis任一失敗回 **503**
### 5.1 503 fail-fast 實測(停 visiona-redis
log`.autoflow/07-delivery/logs/block6-healthz-503-202606201848.log`
```
docker stop visiona-redis
→ backend :3721/healthz HTTP 503 {"checks":{"postgres":"ok","redis":"down"},"status":"unavailable"}
docker start visiona-redis
→ backend :3721/healthz HTTP 200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}
```
- Redis down → 正確偵測 `redis:down` 回 503PG 仍 `ok`
- visiona container **沒有因 Redis 中斷而 crash**fail-fast 只在啟動runtime 降級僅反映在 healthz
- Redis 恢復後 healthz 自動回 200。
## 6. 重啟資料持久化實測
log`.autoflow/07-delivery/logs/block6-persistence-202606201848.log`
```
插入 model row (name=block6-persist-...) 進 stage PG → rows_before_restart = 1
docker restart visiona
→ 啟動 log: "migrate up: no change (already at latest version)"migration 冪等)
→ backend healthz 200
查詢 model row → rows_after_restart = 1同 marker資料還在
(測試資料已 cleanup
```
證明真部署環境下 DB-on 的資料跨 container 重啟持久化。
## 7. 既有功能回歸
| 檢查 | 結果 |
|------|------|
| `GET /api/models`公開auth-gated | `401`api-server 活、DB-backed handler 可達,非 500/502 |
| `GET /`frontend | `200` |
| conversion service | 啟動 log 顯示 initializedconverter base url 正確) |
| FAA downloadPhase 0.9 | 啟動 log 顯示 initialized |
## 8. ✅ 已修復nginx /healthz 不反映 DB 健康(+ docker healthcheck false 444
**原問題**backend 的 DB-aware healthz會 ping PG/Redis、down 回 503 的 fail-fast 邏輯)**實作了但 nginx 攔截不轉發**。
- `docker/nginx.stage.conf` 兩個 `location = /healthz``return 200 "ok"`,不 `proxy_pass` 到 api-server。
- 結果:對外 `/healthz` 永遠 200即使 PG/Redis 掛了。backend 的 503 邏輯形同 dead code。
**併發現的第二個 bug本次一併修**default_server block 結尾用 **server-level `return 444;`**,在 nginx rewrite phase 會「先於」location 匹配短路掉所有請求 —— 包含本該回 200 的 `location = /healthz`。導致 docker healthcheck從 container 內打 `localhost/healthz`、來源 127.0.0.1)也被打成 **444**container 長期顯示 `(unhealthy)`FailingStreak 已累積到 96。原 deploy-stage-v2.sh:196 把這當成「Host 白名單造成的 false negative」實際成因是 `return 444` 的 phase 順序,不是 Host 白名單。
**修復內容2026-06-20commit 於 `docker/nginx.stage.conf`**
1. default_server 的 catch-all `return 444` 從 server-level 收進 `location / { return 444; }` —— 讓 exact-match `location = /healthz` 依 nginx location 優先序勝出。docker healthcheck 現回 200、container `healthy`
2. 公開 server block 新增 `location = /healthz/deep``proxy_pass http://visiona_api/healthz`(深層 readiness反映 PG+Redis
3. 保留淺層 `/healthz`return 200給 docker healthcheck / liveness。
**部署方式**:因 nginx config 在 image build 時 `COPY` 進去(非 bind mount改 source 後以 `deploy-stage-v2.sh` **rebuild + recreate** 持久化(`nginx -s reload` 只是 runtime 暫補、container 重建即失效)。重部署後驗證 baked config 與 source 一致。
## 9. healthz 接 load balancer最終建議 — 已實作)
**選項 A**liveness 淺 / readiness 深 分離,對齊 K8s 慣例)。已實作於 nginx
```nginx
# 公開 server blockserver_name stage-9527.innovedus.com
location = /healthz/deep {
access_log off;
proxy_pass http://visiona_api/healthz; # api-server :3721ping PG+Redis、down 回 503
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_connect_timeout 2s; proxy_read_timeout 2s; proxy_send_timeout 2s; # 探針不該 hang
proxy_no_cache 1; proxy_cache_bypass 1;
add_header Cache-Control "no-store" always;
}
```
**LB 設定建議**
- **health check path`/healthz/deep`** ← LB readiness 打這條
- 期望狀態碼200**503 → 拉出輪替**DB/Redis 掛時自動踢除實例)
- interval 1015s、timeout 5s、unhealthy threshold 連續 3 次(對齊 backend health.go 的 2s ping 逾時 + nginx 2s proxy 逾時)
- 淺層 `/healthz` 保留給 docker healthcheck / livenessDB 抖動時不會誤把實例標死)
**實測2026-06-20log`.autoflow/07-delivery/logs/healthz-deep-verify-*.log`**
```
PG+Redis 健康:
公開 /healthz → 200 "ok"
公開 /healthz/deep → 200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}
docker healthcheck → healthystreak=0修復前 unhealthy streak=96
停 visiona-redis503 路徑):
公開 /healthz/deep → 503 {"checks":{"postgres":"ok","redis":"down"},"status":"unavailable"} ← LB 會踢
公開 /healthz → 200淺層不受影響liveness 不誤殺)
公開 /frontend → 200
起 visiona-redis恢復
公開 /healthz/deep → 200自動恢復
```
## 10. 後續硬化建議(非本次範圍)
1. ~~**§8 healthz 接 LB**:依 §9 選項 A 加 `/healthz/deep`。~~**已完成2026-06-20**,見 §8/§9。
2. **§3.1 network 顯式化**:在 `docker-compose.stage.yml` 顯式宣告 external network `visiona-stage_default`,去除「靠 project name 巧合對上」的隱性依賴。
3. **migration 與多副本**:目前單副本啟動跑 auto-migrate 沒問題;未來多副本需改 `VISIONA_DB_AUTO_MIGRATE=false` + 獨立 `cmd/migrate` 步驟,避免多實例同時 migrate。
## 11. Rollback
```bash
export DOCKER_HOST=tcp://192.168.0.130:2375
# 回到 DB 接入前的 Phase 0.9 image
docker tag visiona:stage-rollback-pre-db visiona:stage
bash scripts/deploy-stage-v2.sh --skip-build
```
注意rollback 到 DB 接入前版本後repository 回 in-memoryPG/Redis 資料保留但不被讀。)
## 12. 煙測 evidence log 清單
| log | 內容 |
|-----|------|
| `.autoflow/07-delivery/logs/block6-deploy-202606201848.log` | build + compose upexit 0 |
| `.autoflow/07-delivery/logs/block6-container-startup-202606201848.log` | 啟動 logDB/Redis/migration initialized |
| `.autoflow/07-delivery/logs/block6-healthz-503-202606201848.log` | 503 fail-fast 實測(停/起 redis |
| `.autoflow/07-delivery/logs/block6-persistence-202606201848.log` | 重啟資料持久化 + migration 冪等 |