# Spec: 設定驅動的多主機正式部署

> Status: `draft-for-review`  
> Source: `requirements.md`  
> Last updated: `2026-07-18`  
> Implementation: not started

## 1. 目標拓撲

```text
Internet
  |
  v
Edge Nginx
  |-- /                 -> frontend-01, frontend-02, ...
  |-- /api              -> backend-01, backend-02, ...
  `-- /haixun-assets    -> MinIO

backend-* ----+---- MongoDB Atlas (fixed egress IP)
worker-*  ----+---- Redis over WireGuard
              `---- MinIO over WireGuard

worker-N <---- private Docker network ----> crawler-N

control machine -- SSH/Ansible --> all hosts
control machine -- push --------> private Registry
all Docker hosts -- pull -------> private Registry
```

## 2. 部署控制檔

### 2.1 `deployment.yml`

非機密設定的單一入口：

```yaml
environment: production

bootstrap:
  operatingSystem: ubuntu-24.04
  installDocker: true
  installWireGuard: true
  configureFirewall: true
  configureTimeSync: true
  installSecurityUpdates: true
  enableAutomaticUpdates: true
  acceptNewSSHHostKeys: true

ssh:
  user: ubuntu
  port: 22
  privateKey: ~/.ssh/id_ed25519

nodes:
  edge:
    - ip: 203.0.113.10
  registry:
    - ip: 203.0.113.11
  frontend:
    - ip: 203.0.113.20
  backend:
    - ip: 203.0.113.30
  worker:
    - ip: 203.0.113.40
  redis:
    - ip: 203.0.113.50
  minio:
    - ip: 203.0.113.60

network:
  wireguardCIDR: 10.80.0.0/16
  atlasEgressNode: edge

domains:
  web: app.example.com
  registry: registry.example.com
  letsEncryptEmail: admin@example.com

images:
  registry: registry.example.com/haixun
  tag: auto

database:
  provider: mongodb-atlas
  name: haixun
  runMigrations: true
  runInit: true
  seedAdmin: true

redis:
  namespace: haixun-prod
  persistencePath: /var/lib/haixun/redis
  appendOnly: true

minio:
  persistencePath: /var/lib/haixun/minio
  bucket: haixun-assets
  initializeBucket: true
```

每個 node 可選擇覆寫 `sshUser`、`sshPort`、`sshPrivateKey`、`wireguardIP` 與 `architecture`。preflight 必須拒絕重複 IP、重複 WireGuard IP、空角色、Redis/MinIO 多於一台（第一版）與缺少必要角色。

### 2.2 `secrets.vault.yml`

至少包含：

```yaml
mongoAtlasURI: mongodb+srv://...
registryUser: ...
registryPassword: ...
redisPassword: ...
minioRootUser: ...
minioRootPassword: ...
authAccessSecret: ...
authRefreshSecret: ...
memberSettingsEncryptionKeyID: v1
memberSettingsEncryptionKey: ...
aiModelCacheFingerprintSecret: ...
scoutSessionSecret: ...
crawlerToken: ...
initialAdmin:
  email: admin@example.com
  displayName: System Admin
  password: ...
```

Provider、SMTP、OAuth、Stripe 等既有 runtime secret 也放同一 Vault。Vault password 不進 repo；遠端 role 只取得執行該角色必要的 secret。

## 3. Image 與執行單位

| Image | 內容 | 持久資料 |
|------|------|----------|
| `frontend` | Vite build + static Nginx | 無 |
| `backend` | Go gateway + extension ZIP | 無 |
| `worker` | Go worker + Node scraper + Chromium | 無；僅 ephemeral temp |
| `crawler` | Node + Playwright crawler | 無 |
| `ops` | migrate/init/seeder binaries 與 migration files | 無 |
| `registry:2` | 私有 image registry | Registry volume |
| `redis:7` | cache/lock + AOF | Redis volume |
| `minio/minio` | object storage | MinIO volume |

應用 image 使用不可變 git SHA tag。`latest` 不可作為 production deploy 真相；inventory 另保存各角色目前與上一個成功 tag。

## 4. First deploy 狀態機

`make first-deploy` 僅是本機 orchestrator，按下列 checkpoint 執行：

1. **control doctor**：檢查 Ansible、Docker Buildx、SSH、Vault、Git worktree tag 能力。
2. **preflight**：解析設定、檢查 DNS、SSH/sudo、Ubuntu 版本、CPU architecture、磁碟/RAM、Atlas 固定出口前置條件。
3. **raw bootstrap**：在無 Python VM 先安裝 `python3`。
4. **common role**：套件、安全更新、chrony、fail2ban、目錄、Docker、log rotation。
5. **WireGuard**：配置 peer、防火牆與連通測試；所有私網測試成功才繼續。
6. **Registry first**：DNS 驗證、Let's Encrypt、認證、持久 volume、所有 Docker host login。
7. **local build/push**：測試並 build frontend/backend/worker/crawler/ops，push immutable tag。
8. **data services**：啟動 Redis/MinIO，檢查 persistence 與私網存取；建立 bucket/policy。
9. **Atlas prepare**：由單一受控 backend host 執行 ops container，依序 migration -> init -> optional first seed。
10. **applications**：backend rolling start、worker+crawler start、frontend rolling start。
11. **Edge**：產生 upstream、申請網站 TLS、驗證後 reload。
12. **acceptance**：HTTP、job claim、crawler health、Redis/MinIO persistence、公網封鎖與版本報告。

任一步驟失敗即停止後續 checkpoint；重跑從 idempotent state 繼續，不執行 wipe。

## 5. 一般部署狀態機

```text
test -> local build -> push -> remote pull while old runs
     -> optional migration(run_once)
     -> atomic config sync
     -> rolling recreate
     -> readiness
     -> edge upstream update
     -> record successful tag
```

- backend migration 失敗：不 recreate backend。
- backend/frontend readiness 失敗：不加入 upstream，保留其他健康節點。
- worker 失敗：回報 host，其他 worker 不回滾。
- config-only deploy：跳過 build/push，但仍做 render validation、atomic sync 與 recreate。
- rollback：切回上一個成功 image tag；資料庫只允許 forward-compatible migration，不自動執行 down migration。

## 6. 初始資料庫契約

- `ops migrate`：使用 Atlas URI 執行所有 forward migrations。
- `ops init`：執行 `cmd/init`，只建立可重複建立的 operational indexes。
- `ops seed-admin`：只在 `database.seedAdmin=true` 且目標管理員不存在時建立初始資料。
- seeder 需把目前寫死的 email/display name 改成設定值；password 至少 12 字元。
- 既有管理員存在時，seed 必須成功 no-op，不可更新 password hash。
- 管理員改密碼另設 `make admin-reset-password`，要求明確 host/environment 與確認。

## 7. 網路規則

| 來源 | 目的 | Port | 公開 |
|------|------|------|------|
| Internet | Edge | 80/443 | 是 |
| Control/Docker hosts | Registry | 443 | 認證 + TLS |
| Edge WG IP | Frontend | container HTTP | 否 |
| Edge WG IP | Backend | gateway port | 否 |
| Backend/Worker WG IP | Redis | 6379 | 否 |
| Backend/Worker/Edge WG IP | MinIO | 9000 | 否 |
| Worker container | crawler sidecar | crawler port | 否，不 publish |
| Backend/Worker | Atlas | 27017/SRV | 經固定出口 |

防火牆啟用前先 allow SSH；所有 role 完成後做反向驗證，確認 private port 從公網不可達。

## 8. 操作介面

```bash
make setup-control
make doctor
make preflight
make bootstrap
make bootstrap ROLE=worker
make bootstrap HOST=worker-02
make first-deploy
make deploy-all
make deploy-frontend
make deploy-backend
make deploy-worker
make deploy-config ROLE=worker
make db-check
make db-migrate
make db-init
make db-seed
make status
make logs ROLE=worker HOST=worker-01
make restart ROLE=backend
make rollback ROLE=backend
make vault-edit
```

具破壞性的資料 reset 不提供無參數捷徑，必須指定 environment、role 並互動確認。

## 9. Readiness 與擴縮

- gateway readiness 至少驗證程序、Redis、MongoDB；S3 狀態另外呈現，不能再只靠目前 Redis-only health。
- frontend readiness 驗證靜態首頁與指定 build version。
- crawler 增加 health endpoint；crawler token 不得記錄。
- worker 第一版以 container running + Mongo/Redis connectivity + worker identity 啟動檢查為準；後續補 durable heartbeat/lag。
- worker ID 由 hostname/role 派生，不得在多主機都使用 `worker-1`。
- Outbox 全域 lock 是吞吐限制，部署完成不代表 Outbox 已水平擴充；須另立應用改造 task。
