diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3faf8201..8f029e41 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -6,7 +6,31 @@ --- -## 📍 當前狀態 (2026-04-10 深夜 — 全站真實數據驗收 + AI 回覆修復 + Backlog 清零 ✅) +## 📍 當前狀態 (2026-04-11 — 基礎設施重建計畫 ADR-069 規格完成) + +### 本 Session 完成項目 (2026-04-11 深夜) + +| 項目 | 內容 | +|------|------| +| 規格文件 v1.3 | `docs/superpowers/specs/2026-04-10-infra-rebuild-sprint-abc-design.md` | +| 涵蓋範圍 | Sprint A(清廢棄修錯誤)+ Sprint B(Ansible+ArgoCD GitOps)+ Sprint C(Velero+rsync DR) | +| 整合確認 | B2/B3/D1 Backlog 不受影響;K8s pods 全程不觸碰 | +| 文件更新時間表 | 每個里程碑明確指定需更新的 memory/LOGBOOK/ADR/Skills | + +**重要技術發現**: +- Sentry snuba crash loop 根因:Docker DNS 無法解析 `kafka:9092` hostname,非 worker 數量問題 +- K8s `SENTRY_DSN=CHANGE_ME` — Phase 6 未完成,需在 A-0 確認 Sentry 運作後修正 +- 188 `ollama.wooo.work` nginx 指向 :3000 (wooo-aiops frontend),應改為 :3010 (open-webui) +- Harbor nginx upstream :5050 錯誤,應為 :5000 +- bitan pharmacy 是完整 Next.js source code,Docker 化需含 build step + +**Backlog 狀態**: +- 新增 ADR-069 基礎設施重建計畫(Sprint A/B/C) +- B2/B3/D1 不動 + +--- + +## 📍 舊狀態 (2026-04-10 深夜 — 全站真實數據驗收 + AI 回覆修復 + Backlog 清零 ✅) ### 本 Session 完成項目 (2026-04-10 深夜 第二輪) diff --git a/docs/superpowers/specs/2026-04-10-infra-rebuild-sprint-abc-design.md b/docs/superpowers/specs/2026-04-10-infra-rebuild-sprint-abc-design.md new file mode 100644 index 00000000..e8e95bcf --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-infra-rebuild-sprint-abc-design.md @@ -0,0 +1,1646 @@ +# AWOOOI 基礎設施完整重建計畫 +## Sprint A + B + C — 正式設計規格文件 + +> **文件版本**: v1.3 +> **建立日期**: 2026-04-10 +> **狀態**: 待實施 +> **ADR**: ADR-069(本文件為 ADR 核心規範) +> **作者**: Claude Sonnet 4.6 + 首席架構師審查通過 + +--- + +## 一、背景與目標 + +### 問題陳述 + +AWOOOI 私有雲目前存在三大結構性問題: + +1. **服務散落、設定漂移**:服務分散於 Docker、PM2、systemd 各層,nginx conf 在兩台主機各自演化,無版本控制 +2. **無變更管理**:所有主機設定為手動操作,無法追蹤、無法回滾 +3. **無災難備援**:無 K8s 備份、無 Host 層備份、無標準回復程序 + +### 目標 + +- Sprint A:讓所有主機進入**可預測的乾淨狀態**(清廢棄、修錯誤、統一入口) +- Sprint B:建立 **Git = 唯一真相來源**(Ansible 管 Host、ArgoCD 管 K8s) +- Sprint C:實現**任意單點失效 15 分鐘內可恢復** + +### 鐵律 + +- 🔴 **零影響現有商業流量**:awoooi、bitan、tsenyang、momo 全程不中斷 +- 🔴 **所有網站服務 = Docker**:bitan 遷移後統一,不上 K3s +- 🔴 **188 = 唯一 SSL 終止器**:110 退出 HTTPS 競爭 +- 🔴 **112 Kali = 預留安全接口**:本次不實作,文件記錄預留位置 + +--- + +## 二、現況真相地圖 + +### 主機角色 + +| 主機 | IP | 角色 | 關鍵服務 | +|------|----|------|---------| +| AI+Web 中心 | 192.168.0.188 | SSL 終止器、應用宿主 | Nginx, OpenClaw:8088, tsenyang:3000, momo:5003, signoz:3301, minio:9000, litellm:4000 | +| DevOps 金庫 | 192.168.0.110 | CI/CD、監控、DevOps 工具 | Harbor:5000, Sentry:9000, Gitea:3001, Langfuse:3100, 5×GitHub Runner, PM2(bitan:3003, wooo-frontend:3000), stockPlatform:31235, keepalived BACKUP(VIP:200) | +| K3s Control-Plane #1 | 192.168.0.120 | K8s Master | awoooi-prod namespace | +| K3s Control-Plane #2 | 192.168.0.121 | K8s Master HA | awoooi-prod namespace | +| K3s VIP | 192.168.0.125 | K8s API + NodePort | 32334(API), 32335(Web) | +| Nginx HA VIP | 192.168.0.200 | Nginx HA | 188 MASTER(未裝)/ 110 BACKUP | +| Kali Security | 192.168.0.112 | 安全掃描(預留) | Scanner API:8080 | + +### 完整問題清單(21 項) + +| # | 問題 | 主機 | Sprint | 根因 | +|---|------|------|--------|------| +| P1 | GitLab volumes 殘骸 | 188 | A | GitLab 廢棄後未清除 | +| P2 | GitLab volumes + 目錄殘骸 | 110 | A | 同上 | +| P3 | Nginx gitlab block 仍存在 | 188 | A | 同上 | +| P4 | all-sites-from-188.conf 漂移 | 110 | A | 複製 conf 未同步演化 | +| P5 | harbor nginx 指向 :5050(應為 :5000) | 110 | A | 手動設定錯誤 | +| P6 | ollama.wooo.work 獨立 conf 指向 :3000(tsenyang 佔用) | 188 | A | open-webui 未啟動 | +| P7 | n8n 容器未啟動(nginx conf 已存在) | 188 | A | 未啟動 | +| P8 | open-webui 容器未啟動 | 188 | A | 未啟動 | +| P9 | docker-registry :5000 與 Harbor 衝突 | 188 | A | Port 規劃衝突 | +| P10 | Swap 100% 滿(3.8GB) | 110 | A | Sentry snuba crash loop | +| P11 | 6 個 snuba 容器 crash loop | 110 | A | Docker 網路 DNS 無法解析 kafka:9092 | +| P12 | K8s Secrets SENTRY_DSN = "CHANGE_ME" | K3s | A | Phase 6 未完成 | +| P13 | Sentry Phase 6 Webhook→Telegram 告警未完成 | 全站 | A | 未實作 | +| P14 | 缺 sentry/gitea/langfuse/stock HTTPS | 188 | A | SSL 未申請 | +| P15 | signoz.wooo.work 只有 HTTP(all-sites.conf) | 188 | A | SSL 未設定 | +| P16 | keepalived MASTER 未安裝(VIP:200 單點) | 188 | A | 未裝 | +| P17 | bitan 跑 PM2,應遷移 Docker | 110 | A | 歷史遺留 | +| P18 | stock.wooo.work 無 HTTPS + 資料待確認 | 110 | A | SSL 未申請 | +| P19 | wooo-aiops 狀態未確認 | 110 | A | 未盤點 | +| P20 | ArgoCD 未安裝 | K3s | B | 未實作 | +| P21 | Ansible Host IaC 空骨架 | 全站 | B | 未實作 | +| P22 | Velero 未安裝 | K3s | C | 未實作 | +| P23 | Host 層無備份機制 | 188/110 | C | 未實作 | + +--- + +## 三、架構決策(ADR-069) + +### 3.1 Nginx HA 架構 + +``` +外部 DNS → 192.168.0.188(平時) + ↓ + keepalived MASTER(priority 100) + ↓ + VIP 192.168.0.200 + ↙ ↘ +188:443(主動) 110:443(備援) + keepalived BACKUP(priority 90) + 188 宕機時自動接管 +``` + +**決策**:188 必須安裝 keepalived MASTER,才能讓 VIP:200 真正 HA。 + +### 3.2 GitOps 架構 + +``` +git push gitea main +→ Gitea Actions: docker build + push Harbor(image:sha) +→ 更新 k8s/awoooi-prod/ 中的 image tag(git commit) +→ ArgoCD 偵測 YAML 變更 → 自動 apply 到 K3s +→ rollout 完成(取代現有 kubectl set image) +``` + +**回滾**:`argocd app rollback awoooi-prod `(30 秒)或 `git revert + push` + +### 3.3 備援架構 + +``` +K8s 層:Velero 每日快照 PV → MinIO(188:9000/velero-backups) → velero restore(<15 分鐘) +Host 層:重要 Docker volume → rsync → 對方主機 → 手動恢復(<30 分鐘) +Nginx 層:Ansible templates → git revert → ansible-playbook → 自動恢復(<5 分鐘) +``` + +### 3.4 112 Kali 預留接口 + +- **角色**:資訊安全掃描中心(所有安全機制集中) +- **本次**:不實作,僅在 Ansible inventory 保留 entry +- **預留**:Nginx 可預留 `security.wooo.work` 入口 +- **下一步**:獨立 ADR 討論安全架構整合 + +--- + +## 四、Sprint A — 整地(清廢、修錯、收斂) + +**原則**:每步驟完成後驗收,驗收通過才進下一步。 + +--- + +### A-0:安全防護(最先執行,獨立) + +> **目的**:在動任何 Nginx 或服務之前,確保監視器正常、記憶體安全。 + +#### A-0-1:擴充 110 Swap(4GB) + +```bash +# 在 192.168.0.110 執行 +sudo dd if=/dev/zero of=/swap2.img bs=1G count=4 status=progress +sudo chmod 600 /swap2.img +sudo mkswap /swap2.img +sudo swapon /swap2.img +echo '/swap2.img none swap sw 0 0' | sudo tee -a /etc/fstab + +# 驗證 +free -h && swapon --show +# 期望:Swap total ≈ 7.8GB +``` + +#### A-0-2:修復 snuba crash loop(根因:Kafka DNS 解析失敗) + +```bash +# 在 192.168.0.110 執行 +# 根因:snuba 容器無法解析 kafka:9092,需要整個 stack 重啟以重建 Docker 網路 +cd /opt/sentry + +# 先確認 kafka 容器健康 +docker ps | grep kafka +# 期望:sentry-self-hosted-kafka-1 Up (healthy) + +# 重啟整個 sentry stack(約 3-5 分鐘) +docker compose down && docker compose up -d + +# 等待 2 分鐘後驗證 +sleep 120 +docker ps -a | grep Restarting +# 期望:無任何 Restarting 容器 + +# 驗證 Sentry Web 正常 +curl -s -o /dev/null -w '%{http_code}' http://localhost:9000 +# 期望:200 或 302 +``` + +**注意**:`docker compose down` 不刪除 volume,資料安全。重啟期間 Sentry 短暫不可用(約 3 分鐘),不影響商業流量。 + +#### A-0-3:修復 K8s SENTRY_DSN(CHANGE_ME → 真實值) + +```bash +# 步驟 1:從 Sentry UI 取得 DSN +# 瀏覽器開啟 http://192.168.0.110:9000 +# Settings → Projects → awoooi-api → Client Keys (DSN) → 複製 DSN +# DSN 格式:https://@192.168.0.110:9000/ + +# 步驟 2:從 Sentry UI 取得 Auth Token +# Settings → Account → API → Auth Tokens → Create Token(scope: project:read, project:write) + +# 步驟 3:更新 K3s Secrets(在 MacBook 執行) +kubectl patch secret awoooi-secrets -n awoooi-prod \ + --type='json' \ + -p='[ + {"op":"replace","path":"/data/SENTRY_DSN","value":"'$(echo -n "YOUR_SENTRY_DSN" | base64)'"}, + {"op":"replace","path":"/data/SENTRY_AUTH_TOKEN","value":"'$(echo -n "YOUR_AUTH_TOKEN" | base64)'"} + ]' + +# 步驟 4:驗證 Secrets 已更新 +kubectl get secret awoooi-secrets -n awoooi-prod \ + -o jsonpath='{.data.SENTRY_DSN}' | base64 -d +# 期望:真實 DSN,不是 CHANGE_ME + +# 步驟 5:Rolling restart 讓 pod 讀取新 secrets +kubectl rollout restart deployment/awoooi-api -n awoooi-prod +kubectl rollout restart deployment/awoooi-web -n awoooi-prod +kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s +``` + +#### A-0-4:完成 Sentry Phase 6 — Webhook→Telegram 告警 + +```bash +# 步驟 1:在 Sentry UI 建立 Webhook Integration +# Settings → Integrations → WebHooks → Add Webhook +# URL:https://monitor.wooo.work/telegram/webhook(已有 nginx conf) +# Events:issue(新錯誤)、error(錯誤率突增) + +# 步驟 2:驗證 webhook 到達 OpenClaw +# 在 188 查看 openclaw logs +docker logs openclaw --tail 20 -f +# 觸發測試:Sentry UI → 點擊 Send Test + +# 步驟 3:確認 Telegram 收到告警 +# 期望:Telegram 群組收到格式化的錯誤通知 +``` + +#### A-0-5:更新 03-secrets.yaml(文件同步) + +```bash +# 在本地 repo 更新 secrets example,確保文件不再有 CHANGE_ME 誤導 +# 編輯 k8s/awoooi-prod/03-secrets.example.yaml +# 更新說明:如何取得 SENTRY_DSN 的步驟(不寫真實值) +``` + +**A-0 驗收標準**: +- [ ] `free -h` 顯示 Swap total ≈ 7.8GB +- [ ] `docker ps -a | grep Restarting` → 空結果 +- [ ] `curl http://192.168.0.110:9000` → 200/302 +- [ ] K8s pod 中 SENTRY_DSN 非 CHANGE_ME +- [ ] Telegram 收到 Sentry 測試告警 + +--- + +### A-1:GitLab 完整移除(188 + 110) + +> **目的**:清除所有 GitLab 殘骸,釋放磁碟空間。 + +#### A-1-1:188 GitLab 移除 + +```bash +# 在 192.168.0.188 執行 +cd /home/ollama/devops + +# 停止並刪除容器 +docker compose stop gitlab gitlab-runner +docker compose rm -f gitlab gitlab-runner + +# 刪除 volumes +docker volume rm wooo-gitlab-config wooo-gitlab-data wooo-gitlab-logs wooo-gitlab-runner-config + +# 刪除目錄 +sudo rm -rf /home/ollama/gitlab/ +sudo rm -rf /home/ollama/gitlab-runner/ + +# 驗證:確認 registry 容器不受影響 +docker ps | grep registry +# 期望:docker-registry 仍在運行(若已啟動) +``` + +#### A-1-2:188 Nginx 移除 GitLab Block + +```bash +# 編輯 /etc/nginx/sites-enabled/all-sites.conf +# 移除以下整個 server block(兩個,HTTP + HTTPS): +# server { server_name gitlab.wooo.work; ... } + +# 若有獨立 conf 檔(通常不存在,但確認) +sudo rm -f /etc/nginx/sites-available/gitlab.conf +sudo rm -f /etc/nginx/sites-enabled/gitlab.conf + +# 測試並重載 +sudo nginx -t && sudo systemctl reload nginx +``` + +#### A-1-3:188 刪除 GitLab SSL 憑證 + +```bash +# 確認 gitlab.wooo.work cert 無其他用途 +sudo certbot delete --cert-name gitlab.wooo.work +# 確認提示時輸入 y +``` + +#### A-1-4:110 GitLab 移除 + +```bash +# 在 192.168.0.110 執行 + +# 刪除 volumes +docker volume rm gitlab_gitlab-runner-config wooo-gitlab-config \ + wooo-gitlab-data wooo-gitlab-logs wooo-gitlab-runner-config + +# 刪除目錄(若存在) +sudo rm -rf /home/wooo/gitlab/ +sudo rm -rf /home/wooo/gitlab-migration/ +sudo rm -rf /home/wooo/gitlab-runner-config/ + +# 刪除 SSL 憑證 +sudo certbot delete --cert-name gitlab.wooo.work +``` + +#### A-1-5:清除 110 Nginx 殘留空白檔 + +```bash +# 在 192.168.0.110 執行 +# aiops.wooo.work.conf 是空白殘留檔,確認後刪除 +cat /etc/nginx/sites-enabled/aiops.wooo.work.conf +# 若確認為空或殘留 → 刪除 +sudo rm /etc/nginx/sites-enabled/aiops.wooo.work.conf +sudo nginx -t && sudo systemctl reload nginx +``` + +#### A-1-6:清除 188 舊版殘留 Volumes + +```bash +# 在 192.168.0.188 執行 +# clawbot 舊版 volumes(openclaw 不使用任何 named volume) +docker volume rm \ + clawbot_clawbot-redis-data \ + clawbot_clawbot-screenshots \ + clawbot-v5_clawbot-redis-data \ + clawbot-v5_clawbot-screenshots \ + clawbot-v5-updated_clawbot-redis-data \ + clawbot-v5-updated_clawbot-screenshots + +# n8n 舊版 named volumes(compose 使用 bind mount ./data,不用 named volume) +docker volume rm n8n_data n8n_n8n_data + +# open-webui 舊版 volume(正確的是 open-webui,open-webui-data 是殘留) +docker volume rm open-webui-data + +# grist SSL cert(無容器、無 nginx conf,完全廢棄) +sudo certbot delete --cert-name grist.wooo.work +``` + +#### A-1-7:清除 110 殘留 Runner 目錄 + +```bash +# 在 192.168.0.110 執行 +# actions-runner-awoooi-3:.runner 設定空白,無 systemd service,未完成的殘留 +ls /home/wooo/actions-runner-awoooi-3/ # 確認內容 +# 若確認無 .runner 設定且無 service → 刪除 +sudo rm -rf /home/wooo/actions-runner-awoooi-3/ +``` + +**A-1 驗收標準**: +- [ ] `docker volume ls | grep gitlab` → 空結果(兩台) +- [ ] `docker volume ls | grep clawbot` → 空結果(188) +- [ ] `docker volume ls | grep -E 'n8n_data|n8n_n8n|open-webui-data'` → 空結果(188) +- [ ] `ls /etc/letsencrypt/live/ | grep grist` → 空結果(188) +- [ ] `ls /home/ollama/gitlab` → No such file +- [ ] `ls /home/wooo/gitlab*` → No such file +- [ ] `ls /home/wooo/actions-runner-awoooi-3` → No such file +- [ ] `sudo nginx -t` 兩台都通過 + +--- + +### A-2:啟動休眠服務(188) + +#### A-2-1:修改 docker-registry Port(5000→5001) + +```bash +# 在 192.168.0.188 執行 +# 編輯 /home/ollama/devops/docker-compose.yml +# 將 registry 的 ports 從 '5000:5000' 改為 '5001:5000' + +# 若 registry 已在運行,需重啟 +cd /home/ollama/devops +docker compose up -d registry +``` + +#### A-2-2:啟動 n8n + +```bash +# 在 192.168.0.188 執行 +cd /home/ollama/n8n +docker compose up -d + +# 驗證 +docker ps | grep n8n +curl -s -o /dev/null -w '%{http_code}' http://localhost:5678 +# 期望:200 +``` + +#### A-2-3:啟動 open-webui(:3010) + +```bash +# 在 192.168.0.188 執行 +# volume open-webui 已存在,直接掛載 +docker run -d \ + --name open-webui \ + --restart always \ + -p 3010:8080 \ + -v open-webui:/app/backend/data \ + --add-host=host.docker.internal:host-gateway \ + ghcr.io/open-webui/open-webui:main + +# 驗證 +docker ps | grep open-webui +curl -s -o /dev/null -w '%{http_code}' http://localhost:3010 +# 期望:200 +``` + +**A-2 驗收標準**: +- [ ] `docker ps | grep -E 'n8n|open-webui|registry'` → 三個都 Up +- [ ] n8n: `http://localhost:5678` → 200 +- [ ] open-webui: `http://localhost:3010` → 200 +- [ ] registry: Port 5001 正常 + +--- + +### A-3:Bitan 藥局遷移 Docker + +> **目的**:統一網站服務為 Docker,納入版控與備份體系。 + +#### A-3-1:準備 Dockerfile + +```bash +# 在 192.168.0.110 執行 +# bitan 是 Next.js 源碼,需要 build +cat > /home/wooo/apps/bitan-pharmacy/Dockerfile << 'EOF' +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3003 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3003 +CMD ["node", "server.js"] +EOF +``` + +#### A-3-2:建立 docker-compose.yml + +```bash +cat > /home/wooo/apps/bitan-pharmacy/docker-compose.yml << 'EOF' +services: + bitan-pharmacy: + build: . + container_name: bitan-pharmacy + restart: always + ports: + - "3003:3003" + environment: + - NODE_ENV=production + - PORT=3003 + - TZ=Asia/Taipei + volumes: + - ./public:/app/public:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3003"] + interval: 30s + timeout: 10s + retries: 3 +EOF +``` + +#### A-3-3:建置並測試 + +```bash +# 在 192.168.0.110 執行 +cd /home/wooo/apps/bitan-pharmacy + +# Build(約 2-3 分鐘) +docker compose build + +# 先用不同 port 測試,不影響 PM2 現有服務 +docker run --rm -p 3004:3003 bitan-pharmacy-bitan-pharmacy & +sleep 10 +curl -s -o /dev/null -w '%{http_code}' http://localhost:3004 +# 期望:200 + +# 確認 bitan.wooo.work 透過 188 nginx 仍正常(PM2 仍在) +curl -s -o /dev/null -w '%{http_code}' https://bitan.wooo.work +# 期望:200 +``` + +#### A-3-4:切換(PM2 → Docker) + +```bash +# 在 192.168.0.110 執行 +# 停止 PM2 bitan(約 1 秒停機) +pm2 stop bitan-pharmacy + +# 立即啟動 Docker 版本(監聽同一個 port 3003) +cd /home/wooo/apps/bitan-pharmacy +docker compose up -d + +# 驗證切換成功(30 秒內) +sleep 5 +curl -s -o /dev/null -w '%{http_code}' http://localhost:3003 +# 期望:200 + +# 確認 bitan.wooo.work 正常 +curl -s -o /dev/null -w '%{http_code}' https://bitan.wooo.work +# 期望:200 +``` + +#### A-3-5:清理 PM2 + +```bash +# 確認 Docker 版本穩定後(觀察 10 分鐘) +pm2 delete bitan-pharmacy +pm2 save + +# 確認 PM2 清單 +pm2 list +# 期望:只剩 wooo-frontend +``` + +**A-3 驗收標準**: +- [ ] `docker ps | grep bitan` → bitan-pharmacy Up +- [ ] `curl https://bitan.wooo.work` → 200 +- [ ] `pm2 list` → 無 bitan-pharmacy +- [ ] 觀察 10 分鐘無異常 + +--- + +### A-4:DNS 確認 + SSL 申請 + +#### A-4-1:確認 DNS 指向 + +```bash +# 在本地 MacBook 執行 +# 確認以下域名都解析到 188(192.168.0.188) +for domain in sentry.wooo.work gitea.wooo.work langfuse.wooo.work stock.wooo.work signoz.wooo.work; do + echo -n "$domain → " && dig +short $domain | head -1 +done +# 期望:全部顯示 192.168.0.188 或對應的公網 IP +``` + +#### A-4-2:申請 SSL(webroot 模式) + +```bash +# 在 192.168.0.188 執行 +# webroot 模式不修改 nginx 設定,最安全 + +# 確保 webroot 目錄存在 +sudo mkdir -p /var/www/html + +# 申請(一次申請全部) +sudo certbot certonly --webroot -w /var/www/html \ + -d sentry.wooo.work \ + -d gitea.wooo.work \ + -d langfuse.wooo.work \ + -d stock.wooo.work \ + -d signoz.wooo.work \ + --email admin@wooo.work \ + --agree-tos \ + --no-eff-email + +# 驗證 +sudo ls /etc/letsencrypt/live/ +# 期望:出現 sentry.wooo.work 等新憑證 +``` + +**注意**:若 certbot 要求分開申請(每個域名一個 cert),則逐一執行。 + +**A-4 驗收標準**: +- [ ] DNS 確認全部指向正確 IP +- [ ] `/etc/letsencrypt/live/` 出現新憑證 +- [ ] `sudo certbot renew --dry-run` 通過 + +--- + +### A-5:重寫 188 Nginx 設定 + +> **重要**:這是最高風險步驟。必須先備份,修改後立即測試,失敗立即回滾。 + +#### A-5-1:備份現有設定 + +```bash +# 在 192.168.0.188 執行 +sudo cp /etc/nginx/sites-enabled/all-sites.conf /etc/nginx/all-sites.conf.bak.$(date +%Y%m%d%H%M) +sudo cp /etc/nginx/sites-enabled/ollama.wooo.work /etc/nginx/ollama.wooo.work.bak.$(date +%Y%m%d%H%M) +``` + +#### A-5-2:修改 all-sites.conf(移除 GitLab Block) + +在 `/etc/nginx/sites-enabled/all-sites.conf` 中: +- 移除 `gitlab.wooo.work` 的兩個 server block(HTTP + HTTPS) +- `signoz.wooo.work` 的 HTTP-only block **改寫為 HTTPS**(見下方完整 block) +- `stock.wooo.work` 的 HTTP-only block **改寫為 HTTPS** + +**signoz.wooo.work 完整 server block:** +```nginx +server { + listen 80; + server_name signoz.wooo.work; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name signoz.wooo.work; + + ssl_certificate /etc/letsencrypt/live/signoz.wooo.work/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/signoz.wooo.work/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:3301; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +**stock.wooo.work 完整 server block:** +```nginx +server { + listen 80; + server_name stock.wooo.work; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name stock.wooo.work; + + ssl_certificate /etc/letsencrypt/live/stock.wooo.work/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/stock.wooo.work/privkey.pem; + + location / { + proxy_pass http://192.168.0.110:31235; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +#### A-5-3:新增獨立 Conf 檔(sentry/gitea/langfuse) + +```bash +# 在 188 建立 /etc/nginx/sites-available/internal-tools.conf +# 包含 sentry、gitea、langfuse 三個 server block +``` + +**sentry.wooo.work:** +```nginx +server { + listen 80; + server_name sentry.wooo.work; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name sentry.wooo.work; + + ssl_certificate /etc/letsencrypt/live/sentry.wooo.work/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sentry.wooo.work/privkey.pem; + + client_max_body_size 50m; + + location / { + proxy_pass http://192.168.0.110:9000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } +} +``` + +**gitea.wooo.work:** +```nginx +server { + listen 80; + server_name gitea.wooo.work; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name gitea.wooo.work; + + ssl_certificate /etc/letsencrypt/live/gitea.wooo.work/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gitea.wooo.work/privkey.pem; + + client_max_body_size 512m; + + location / { + proxy_pass http://192.168.0.110:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } +} +``` + +**langfuse.wooo.work:** +```nginx +server { + listen 80; + server_name langfuse.wooo.work; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name langfuse.wooo.work; + + ssl_certificate /etc/letsencrypt/live/langfuse.wooo.work/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/langfuse.wooo.work/privkey.pem; + + location / { + proxy_pass http://192.168.0.110:3100; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +```bash +# symlink 啟用 +sudo ln -s /etc/nginx/sites-available/internal-tools.conf /etc/nginx/sites-enabled/internal-tools.conf +``` + +#### A-5-4:修改 ollama.wooo.work 獨立 Conf + +```bash +# 修改 /etc/nginx/sites-enabled/ollama.wooo.work +# 將 proxy_pass http://127.0.0.1:3000 改為 http://127.0.0.1:3010 +# (共兩處:HTTPS block + 內網 HTTP block) + +sudo sed -i 's|proxy_pass http://127.0.0.1:3000|proxy_pass http://127.0.0.1:3010|g' \ + /etc/nginx/sites-enabled/ollama.wooo.work +``` + +#### A-5-5:測試並重載 + +```bash +sudo nginx -t +# 必須通過才繼續 + +sudo systemctl reload nginx + +# 立即驗收全站 +for domain in aiops.wooo.work bitan.wooo.work n8n.wooo.work ollama.wooo.work \ + sentry.wooo.work gitea.wooo.work langfuse.wooo.work \ + stock.wooo.work signoz.wooo.work mo.wooo.work; do + code=$(curl -s -o /dev/null -w '%{http_code}' -L https://$domain --max-time 5) + echo "$domain → $code" +done +``` + +**若任何服務 502/404,立即回滾:** +```bash +sudo cp /etc/nginx/all-sites.conf.bak.$(date +%Y%m%d)* /etc/nginx/sites-enabled/all-sites.conf +sudo systemctl reload nginx +``` + +**A-5 驗收標準**: +- [ ] `sudo nginx -t` 通過 +- [ ] aiops.wooo.work → 200(商業流量不受影響) +- [ ] bitan.wooo.work → 200(商業流量不受影響) +- [ ] n8n.wooo.work → 200 +- [ ] ollama.wooo.work → 200(open-webui) +- [ ] sentry.wooo.work → 200 +- [ ] gitea.wooo.work → 200 +- [ ] langfuse.wooo.work → 200 +- [ ] stock.wooo.work → 200 +- [ ] signoz.wooo.work → 200 + +--- + +### A-6:修復 110 Harbor Nginx + 移除漂移 Conf + +> **依賴**:A-5 全部驗收通過後才執行。 + +#### A-6-1:修復 Harbor nginx conf + +```bash +# 在 192.168.0.110 執行 +# 備份 +sudo cp /etc/nginx/sites-enabled/harbor /etc/nginx/harbor.bak.$(date +%Y%m%d%H%M) + +# 修改 upstream(5050 → 5000) +sudo sed -i 's/server 127.0.0.1:5050/server 127.0.0.1:5000/' /etc/nginx/sites-enabled/harbor + +# 驗證修改 +grep 'server 127.0.0.1' /etc/nginx/sites-enabled/harbor +# 期望:server 127.0.0.1:5000 + +sudo nginx -t && sudo systemctl reload nginx +``` + +#### A-6-2:驗證 harbor.wooo.work + +```bash +curl -s -o /dev/null -w '%{http_code}' https://harbor.wooo.work +# 期望:200 +``` + +#### A-6-3:移除 all-sites-from-188.conf + +```bash +# 確認 188 已接手所有服務(A-5 驗收通過)後才執行 +sudo rm /etc/nginx/sites-enabled/all-sites-from-188.conf + +sudo nginx -t && sudo systemctl reload nginx + +# 再次確認 110 管理的服務不受影響 +curl -s -o /dev/null -w '%{http_code}' https://harbor.wooo.work +# 期望:200 +``` + +**A-6 驗收標準**: +- [ ] `grep 5050 /etc/nginx/sites-enabled/harbor` → 空結果 +- [ ] `https://harbor.wooo.work` → 200 +- [ ] `ls /etc/nginx/sites-enabled/all-sites-from-188.conf` → No such file +- [ ] `sudo nginx -t` 通過 + +--- + +### A-7:確認 wooo-aiops 狀態 + +```bash +# 在 192.168.0.110 執行 +echo '=== wooo-aiops docker containers ===' && \ + docker ps | grep -E 'wooo|aiops' + +echo '=== wooo-frontend PM2 ===' && \ + pm2 show wooo-frontend | grep -E 'status|uptime|restart' + +echo '=== wooo-aiops docker compose ===' && \ + cd /home/wooo/wooo-aiops && docker compose ps 2>/dev/null + +# 記錄發現的問題,不改架構 +``` + +**A-7 驗收標準**: +- [ ] wooo-frontend PM2 status = online +- [ ] 記錄所有異常(不修復,交由 wooo-aiops 獨立處理) + +--- + +### A-8:確認 Stock 平台資料完整性 + +```bash +# 在 192.168.0.110 執行 +echo '=== stock containers ===' && docker ps | grep stock + +# 確認 dashboard 正常 +curl -s -o /dev/null -w '%{http_code}' http://localhost:31235 +# 期望:200 + +# 確認資料庫健康 +docker exec stock-platform-dashboard python inspect_db_health.py 2>/dev/null || \ +docker exec stock-platform-dashboard python -c " +import sqlite3, os, glob +dbs = glob.glob('/app/data/db/*.db') +for db in dbs: + size = os.path.getsize(db) + print(f'{db}: {size/1024/1024:.1f}MB') +" + +# 確認 scheduler 運作 +docker logs stock-platform-scheduler --tail 10 +``` + +```bash +# 從外部驗收 +curl -s -o /dev/null -w '%{http_code}' https://stock.wooo.work +# 期望:200 +``` + +**A-8 驗收標準**: +- [ ] `https://stock.wooo.work` → 200 +- [ ] 資料庫檔案存在且有合理大小 +- [ ] scheduler logs 無 error + +--- + +### A-9:安裝 188 keepalived MASTER + +> **依賴**:A-5 完成(Nginx 已收斂到 188 單點)後執行。 + +```bash +# 在 192.168.0.188 執行 +sudo apt install keepalived -y + +sudo tee /etc/keepalived/keepalived.conf << 'EOF' +! Keepalived configuration for 188 (MASTER) +global_defs { + router_id NGINX_188 + script_user root + enable_script_security +} + +vrrp_script check_nginx { + script "/usr/bin/pgrep nginx" + interval 2 + weight -50 + fall 3 + rise 2 +} + +vrrp_instance VI_1 { + state MASTER + interface ens160 + virtual_router_id 51 + priority 100 + advert_int 1 + + authentication { + auth_type PASS + auth_pass 73c1b95048bc12eb + } + + virtual_ipaddress { + 192.168.0.200/24 + } + + track_script { + check_nginx + } +} +EOF + +sudo systemctl enable keepalived +sudo systemctl start keepalived + +# 驗證 VIP 在 188 +ip addr show | grep 192.168.0.200 +# 期望:顯示 VIP 在 ens160 +``` + +**HA 驗證(謹慎執行):** +```bash +# 步驟 1:停止 188 nginx +sudo systemctl stop nginx + +# 步驟 2:在另一台機器確認 VIP 漂移到 110 +ssh wooo@192.168.0.110 "ip addr show | grep 192.168.0.200" +# 期望:VIP 出現在 110 + +# 步驟 3:恢復 188 nginx +sudo systemctl start nginx + +# 步驟 4:確認 VIP 回到 188 +ip addr show | grep 192.168.0.200 +# 期望:VIP 回到 188 +``` + +**A-9 驗收標準**: +- [ ] VIP 192.168.0.200 在 188 上可見 +- [ ] 停 188 nginx → VIP 在 30 秒內漂移到 110 +- [ ] 恢復 188 nginx → VIP 回到 188 + +--- + +### Sprint A 全域驗收 + +| 服務 | 驗收指令 | 期望 | +|------|---------|------| +| harbor.wooo.work | `curl -I https://harbor.wooo.work` | 200 | +| n8n.wooo.work | `curl -I https://n8n.wooo.work` | 200 | +| ollama.wooo.work | `curl -I https://ollama.wooo.work` | 200(open-webui)| +| sentry.wooo.work | `curl -I https://sentry.wooo.work` | 200,告警到 Telegram | +| gitea.wooo.work | `curl -I https://gitea.wooo.work` | 200 | +| langfuse.wooo.work | `curl -I https://langfuse.wooo.work` | 200 | +| stock.wooo.work | `curl -I https://stock.wooo.work` | 200,資料完整 | +| signoz.wooo.work | `curl -I https://signoz.wooo.work` | 200 | +| bitan.wooo.work | `curl -I https://bitan.wooo.work` | 200(Docker 版)| +| aiops.wooo.work | `curl -I https://aiops.wooo.work` | 200(商業流量不受影響)| +| 110 Swap | `free -h` | Total ≈ 7.8GB | +| 110 Sentry | `docker ps -a \| grep Restarting` | 空結果 | +| VIP:200 HA | 停 188 nginx | 30 秒內漂移到 110 | + +--- + +## 五、Sprint B — 變更管理基礎建設 + +### B-1:Ansible Host IaC + +#### 目錄結構 + +``` +/Users/ogt/awoooi/infra/ansible/ +├── inventory/ +│ ├── hosts.yml # 五主機清單 +│ └── group_vars/ +│ ├── all.yml # 共用變數(時區、用戶等) +│ ├── host_110.yml # 110 專用變數 +│ └── host_188.yml # 188 專用變數 +├── playbooks/ +│ ├── site.yml # 入口(可跑全站) +│ ├── 110-devops.yml # 110 完整狀態描述 +│ ├── 188-ai-web.yml # 188 完整狀態描述 +│ └── nginx-sync.yml # Nginx 設定同步 +├── roles/ +│ ├── nginx/ # Nginx 設定管理 +│ │ ├── tasks/main.yml +│ │ └── templates/ # Jinja2 conf 模板 +│ ├── docker-compose-service/ # Docker Compose 服務管理 +│ ├── swap/ # Swap 設定管理 +│ └── pm2-service/ # PM2 服務管理(wooo-frontend) +└── templates/ + └── nginx/ # 全域 nginx conf 模板 +``` + +#### hosts.yml 結構 + +```yaml +all: + children: + devops: + hosts: + host_110: + ansible_host: 192.168.0.110 + ansible_user: wooo + ai_web: + hosts: + host_188: + ansible_host: 192.168.0.188 + ansible_user: ollama + k3s_masters: + hosts: + host_120: + ansible_host: 192.168.0.120 + ansible_user: wooo + host_121: + ansible_host: 192.168.0.121 + ansible_user: wooo + security: + hosts: + host_112: + ansible_host: 192.168.0.112 + ansible_user: wooo + ansible_skip_tags: "all" # 預留,本次不執行 +``` + +#### 110-devops.yml 管理範圍 + +```yaml +# 110 預期狀態: +# - swap: 8GB(/swap.img 3.8GB + /swap2.img 4GB) +# - harbor: running(docker compose in /opt/harbor) +# - sentry: running(docker compose in /opt/sentry, 0 restarting) +# - gitea: running(port 3001) +# - langfuse: running(port 3100) +# - stock-platform: running(port 31235) +# - bitan-pharmacy: running docker(port 3003) +# - wooo-frontend: running PM2(port 3000) +# - 5x GitHub Runners: running(systemd services) +# - nginx: harbor conf 指向 127.0.0.1:5000 +# - keepalived: BACKUP priority 90 VIP:200 +``` + +#### 188-ai-web.yml 管理範圍 + +```yaml +# 188 預期狀態: +# - openclaw: running(port 8088) +# - tsenyang-website: running(port 3000) +# - momo-app: running(port 5003) +# - signoz: running(port 3301) +# - minio: running(port 9000) +# - litellm: running(port 4000) +# - n8n: running(port 5678) +# - open-webui: running(port 3010) +# - docker-registry: running(port 5001) +# - nginx: all-sites.conf 無 gitlab block +# - keepalived: MASTER priority 100 VIP:200 +``` + +#### CI 整合 + +```yaml +# .gitea/workflows/ansible-lint.yml +on: + push: + paths: + - 'infra/ansible/**' +jobs: + lint: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + - name: ansible-lint + run: | + pip install ansible-lint + ansible-lint infra/ansible/playbooks/ +``` + +**手動批准後執行**(不設 auto-deploy,Host 層變更需人工確認) + +--- + +### B-2:ArgoCD on K3s + +#### 安裝 + +```bash +# 在 120 或 121 執行(需 sudo kubectl 權限) +kubectl create namespace argocd +kubectl apply -n argocd \ + -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +# 等待 pod 就緒 +kubectl wait --for=condition=available deployment/argocd-server \ + -n argocd --timeout=300s + +# 設定 NodePort 存取 +kubectl patch svc argocd-server -n argocd \ + -p '{"spec":{"type":"NodePort","ports":[{"name":"https","port":443,"targetPort":8080,"nodePort":32080}]}}' + +# 取得 admin 密碼 +kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath="{.data.password}" | base64 -d +``` + +#### 建立 Application + +```bash +# 使用 ArgoCD CLI 或 Web UI +argocd app create awoooi-prod \ + --repo https://gitea.wooo.work/owenhytsai/awoooi.git \ + --path k8s/awoooi-prod \ + --dest-server https://192.168.0.125:6443 \ + --dest-namespace awoooi-prod \ + --sync-policy automated \ + --auto-prune \ + --self-heal +``` + +#### 改寫 CD 流程 + +將 `.gitea/workflows/cd.yaml` 的 deploy 階段改為: + +```yaml +# 舊:kubectl set image(移除) +# 新:更新 YAML 中的 image tag +- name: Update image tag in K8s manifests + run: | + NEW_TAG="${{ github.sha }}" + sed -i "s|image: harbor.wooo.work/awoooi/api:.*|image: harbor.wooo.work/awoooi/api:${NEW_TAG}|" \ + k8s/awoooi-prod/06-deployment-api.yaml + sed -i "s|image: harbor.wooo.work/awoooi/web:.*|image: harbor.wooo.work/awoooi/web:${NEW_TAG}|" \ + k8s/awoooi-prod/05-deployment-web.yaml + git config user.email "gitea-actions@wooo.work" + git config user.name "Gitea Actions" + git commit -am "ci: update image tag to ${NEW_TAG}" + git push + # ArgoCD 自動偵測並 apply +``` + +--- + +### B-3:Nginx 版控(接 Ansible) + +**原則**:Nginx conf 不再直接在主機手改,所有修改透過 Ansible templates。 + +``` +修改 nginx conf +→ 編輯 infra/ansible/roles/nginx/templates/*.j2 +→ git commit + push +→ Gitea Actions: ansible-lint +→ 人工批准 +→ ansible-playbook nginx-sync.yml --limit host_188 +→ 自動 reload nginx +``` + +--- + +### Sprint B 驗收標準 + +| 能力 | 驗收方式 | +|------|---------| +| Host 層回滾 | git revert nginx conf → ansible-playbook → 主機設定恢復 | +| K8s 層回滾 | `argocd app rollback awoooi-prod` → pod 更新到前一版 | +| 設定漂移偵測 | `ansible-playbook site.yml --check` → 顯示 drift | +| CD 新流程 | git push → ArgoCD 自動同步(無需手動 kubectl set image)| + +--- + +## 六、Sprint C — 災難備援 + +### C-1:Velero K8s 備份 + +#### 準備 MinIO bucket + +```bash +# 在 188 使用 mc(MinIO client)建立 bucket +docker exec minio mc alias set local http://127.0.0.1:9000 \ + $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD +docker exec minio mc mb local/velero-backups +docker exec minio mc policy set private local/velero-backups +``` + +#### 安裝 Velero + +```bash +# 建立 credentials 檔 +cat > /tmp/velero-credentials << EOF +[default] +aws_access_key_id=$MINIO_ROOT_USER +aws_secret_access_key=$MINIO_ROOT_PASSWORD +EOF + +# 安裝 +velero install \ + --provider aws \ + --plugins velero/velero-plugin-for-aws:v1.9.0 \ + --bucket velero-backups \ + --secret-file /tmp/velero-credentials \ + --use-volume-snapshots=false \ + --backup-location-config \ + region=minio,s3ForcePathStyle=true,s3Url=http://192.168.0.188:9000 + +# 建立每日定時備份 +velero schedule create daily-backup \ + --schedule="0 2 * * *" \ + --include-namespaces awoooi-prod \ + --ttl 720h +``` + +#### 驗收(模擬 restore) + +```bash +# 觸發手動備份 +velero backup create test-backup --include-namespaces awoooi-prod + +# 等待完成 +velero backup describe test-backup + +# 模擬 restore(在 staging 環境測試) +velero restore create test-restore --from-backup test-backup \ + --namespace-mappings awoooi-prod:awoooi-staging +``` + +--- + +### C-2:Host 層 rsync 備份 + +#### 備份腳本(188 → 備份 110 資料) + +```bash +# /home/ollama/bin/backup-from-110.sh +#!/bin/bash +set -euo pipefail +LOG="/var/log/backup-from-110.log" +DATE=$(date +%Y%m%d-%H%M%S) + +echo "[$DATE] Starting backup from 110" >> $LOG + +# Harbor registry data(最高優先) +rsync -avz --delete \ + wooo@192.168.0.110:/var/lib/docker/volumes/harbor_harbor-data/_data/ \ + /backup/110/harbor/ >> $LOG 2>&1 || \ + (echo "[$DATE] ERROR: Harbor backup failed" >> $LOG; exit 1) + +# Gitea repos +rsync -avz --delete \ + wooo@192.168.0.110:/var/lib/docker/volumes/gitea_gitea-data/_data/ \ + /backup/110/gitea/ >> $LOG 2>&1 || \ + (echo "[$DATE] ERROR: Gitea backup failed" >> $LOG; exit 1) + +# bitan git bare repo +rsync -avz \ + wooo@192.168.0.110:/home/wooo/bitan-pharmacy.git/ \ + /backup/110/bitan-pharmacy.git/ >> $LOG 2>&1 + +echo "[$DATE] Backup completed" >> $LOG +``` + +```bash +# crontab -e(ollama on 188) +0 1 * * * /home/ollama/bin/backup-from-110.sh +``` + +#### Alertmanager 整合(備份失敗告警) + +在 Prometheus alerting rules 中新增: +```yaml +- alert: BackupFailed + expr: time() - backup_last_success_timestamp > 86400 + annotations: + summary: "Host backup has not run in 24 hours" +``` + +--- + +### C-3:DR SOP 文件 + +存放於 `docs/runbooks/disaster-recovery/`: + +| 文件 | 場景 | 目標時間 | +|------|------|---------| +| DR-K8s-awoooi.md | awoooi pod 全掛 → Velero restore | < 15 分鐘 | +| DR-Harbor.md | Harbor 資料毀損 → rsync 備份還原 | < 30 分鐘 | +| DR-Nginx.md | Nginx 設定錯誤 → Ansible 回滾 | < 5 分鐘 | +| DR-Bitan.md | bitan 容器崩潰 → docker compose up | < 5 分鐘 | +| DR-Stock.md | stock 容器崩潰 → docker compose up | < 5 分鐘 | + +--- + +### Sprint C 驗收標準 + +| 場景 | 驗收方式 | 目標時間 | +|------|---------|---------| +| K8s Pod 全掛 | `velero restore` → pod 正常 | < 15 分鐘 | +| 188 Nginx 失效 | keepalived 切到 110 | < 30 秒 | +| Harbor 資料毀損 | rsync 備份還原 | < 30 分鐘 | +| K3s 設定被改 | `git revert + push` → ArgoCD 同步 | < 5 分鐘 | +| Nginx conf 錯誤 | `ansible-playbook nginx-sync.yml` | < 5 分鐘 | + +--- + +## 七、完整執行順序(無邏輯衝突) + +``` +A-0-1: 110 擴充 Swap ← 獨立,最先,防 OOM + ↓ +A-0-2: 修復 snuba crash loop ← 依賴 Swap 先擴充 + ↓ +A-0-3: 修復 K8s SENTRY_DSN ← 獨立,可與 A-0-2 平行 +A-0-4: Sentry Webhook→Telegram ← 依賴 A-0-2(Sentry 正常) + ↓ +[A-0 全部驗收] + ↓ +A-1: GitLab 完整移除 ← 零風險 + ↓ +A-2: 啟動 n8n/registry/open-webui ← 依賴 A-1(registry port 空出) +A-3: Bitan → Docker ← 獨立,可與 A-2 平行 + ↓ +A-4: DNS 確認 + SSL 申請 ← 依賴 A-2(open-webui 已啟動) + ↓ +A-5: 重寫 188 nginx 設定 ← 依賴 A-4(SSL 憑證到位) + ↓ +A-6: 修復 110 harbor + 移除漂移 ← 依賴 A-5(188 接手驗證通過) +A-7: wooo-aiops 狀態確認 ← 獨立,可與 A-6 平行 +A-8: Stock 資料完整性確認 ← 依賴 A-5(HTTPS 到位後測試) + ↓ +A-9: 安裝 188 keepalived MASTER ← 依賴 A-5(nginx 收斂後) + ↓ +[Sprint A 全部驗收] + ↓ +B-1: Ansible Playbook 撰寫 ← B 的基礎,必須先完成 + ↓ +B-2: ArgoCD on K3s 安裝 ← 依賴 B-1(Host 層先管起來) + ↓ +B-3: CD 流程改接 ArgoCD ← 依賴 B-2 + ↓ +[Sprint B 全部驗收] + ↓ +C-1: Velero 安裝 ← 依賴 B-2(ArgoCD 穩定後) +C-2: rsync 備份腳本 ← 獨立,可與 C-1 平行 + ↓ +C-3: DR SOP 文件 ← 依賴 C-1 + C-2 + ↓ +[全部完成] → 變更管理 + 災難備援目標達成 +``` + +--- + +## 八、衝突與依賴完整檢查表 + +| 依賴關係 | 說明 | 若順序錯誤的後果 | +|---------|------|----------------| +| A-0-2 依賴 A-0-1 | Sentry 重啟需要足夠記憶體 | Swap 不足可能觸發 OOM | +| A-0-4 依賴 A-0-2 | Sentry 必須正常才能測試 Webhook | 測試結果無效 | +| A-2 依賴 A-1 | registry Port 5001 需要確認 gitlab 的 Port 5000 已釋放 | Port 衝突 | +| A-4 依賴 A-2 | webroot SSL 申請需要 nginx 正在服務 acme-challenge | SSL 申請失敗 | +| A-5 依賴 A-4 | HTTPS server block 需要 SSL 憑證存在 | nginx -t 失敗 | +| A-6 依賴 A-5 | 需確認 188 已完整接手後才能移除 110 備份 conf | 移除後 110 nginx 仍試圖代理不存在的服務 | +| A-9 依賴 A-5 | nginx 收斂後才裝 keepalived,避免 110 意外成為 master(因為 110 的 conf 不完整)| VIP 漂移到不完整的 110 → 服務中斷 | +| B-1 依賴 Sprint A | Ansible 要描述「乾淨的狀態」,不能在廢棄服務存在時撰寫 | Playbook 包含已移除的服務 | +| B-2 依賴 B-1 | ArgoCD 接手 CD 前,Host 層需先有 IaC 管理 | CD 問題時無法快速回滾 Host 層 | +| C-1 依賴 B-2 | ArgoCD 與 Velero 可能對同一個 resource 操作,需確認無競爭 | restore 時 ArgoCD 可能立即 revert | +| C-2 獨立 | rsync 備份與其他步驟無相依 | 可隨時執行 | + +--- + +## 九、廢棄資源處置清單 + +### 處置原則(Archive 優先,不急著刪) + +遵循 `feedback_archive_not_delete.md` 鐵律:**先停用、記錄、隔離,確認無依賴後再清除**。 + +分三個等級: +- **等級 1(直接清除)**:已確認無任何容器引用、無任何服務依賴 +- **等級 2(Archive 觀察)**:有可能有資料或隱性依賴,mv 到 `_archive/` 目錄後觀察 14 天 +- **等級 3(永久保留)**:確認運作中,記錄保留原因 + +--- + +### 等級 1:直接清除(納入 Sprint A-1) + +> 依據:已 SSH 逐一驗證,無任何容器掛載、無任何 nginx conf 引用、無任何 systemd service 依賴。 + +| 項目 | 位置 | 查核事實 | +|------|------|---------| +| GitLab volumes(各5個)| 188 + 110 Docker | gitlab 容器已停止,`docker inspect` 確認無其他容器掛載 | +| `wooo-gitlab-*` 目錄 | 188 `/home/ollama/`、110 `/home/wooo/` | compose bind mount 遺留,服務已停 | +| `gitlab.wooo.work` nginx block | 188 all-sites.conf | 指向 110:8929,服務已停 | +| `gitlab.wooo.work` SSL cert | 188 + 110 | certbot `--cert-name gitlab.wooo.work`,無其他引用 | +| `grist.wooo.work` SSL cert | 188 | 無 nginx conf、無 Docker 容器、Grist 完全廢棄 | +| `aiops.wooo.work.conf`(空白檔)| 110 `/etc/nginx/sites-enabled/` | `cat` 確認為空,nginx 不讀空白 conf | +| `open-webui-data` volume | 188 Docker | `docker volume inspect` 確認從未被掛載(`du` 顯示空),`open-webui` 才是正確 volume | +| `n8n_data` + `n8n_n8n_data` volumes | 188 Docker | n8n compose 使用 bind mount `./data`,這兩個 named volume 從未被任何容器掛載 | +| `actions-runner-awoooi-3` 目錄 | 110 `/home/wooo/` | `.runner` 設定空白,`systemctl list-units` 確認無對應 service | + +--- + +### 等級 2:Archive 觀察(mv 到 `_archive/`,下次 Sprint 清) + +| 項目 | 位置 | 查核事實 | Archive 方式 | +|------|------|---------|-------------| +| clawbot 6個 volumes(`clawbot_*`、`clawbot-v5_*`、`clawbot-v5-updated_*`)| 188 Docker | `docker inspect openclaw` 確認 openclaw 完全不掛載任何 named volume(只用 bind mount);但 volume 內可能有舊對話資料,不急著刪 | 保留原地,在 INVENTORY.md 標記為「待清除」,下次 Sprint 前再確認 | +| `apt-cacher-ng` | 110 `/home/wooo/apt-cacher-ng/` | 容器未運行(`docker ps -a` 無)、port 3142 無 listener、所有主機 `/etc/apt/` 均無設定使用它 | `sudo mv /home/wooo/apt-cacher-ng/ /home/wooo/_archive/apt-cacher-ng/` | + +--- + +### 等級 3:永久保留 + +| 項目 | 位置 | 保留原因 | +|------|------|---------| +| `harbor-watchdog.service` | 110 systemd | 主動修復 Harbor 崩潰(Exited 128 死鎖),2026-04-05 建立,運作中 | +| `pm2-wooo.service` + `wooo-frontend` | 110 PM2 | 管理 wooo-aiops Next.js 前端(port 3000),屬 wooo-aiops 專案範疇 | +| `monitoring/` docker compose | 110 `/home/wooo/monitoring/` | Prometheus + Grafana + Alertmanager,正在運作 | +| `design-preview` container | 188 Docker | 設計稿預覽 port 8765,日常使用 | +| `clawdbot` nginx conf | 188 | 指向 OpenClaw,正在使用 | +| 所有 `actions-runner*` 目錄(除 awoooi-3)| 110 | 各自對應 running systemd service,詳見 A-7 | + +--- + +### 關於 wooo-frontend(PM2) + +`wooo-frontend`(PM2,port 3000)是 wooo-aiops 專案的前端,**本計畫不動**。是否遷移 Docker 由 wooo-aiops 獨立決定,不在此範疇。 + +--- + +## 十、基礎設施庫存記錄(防止下次再猜) + +### 問題根源 + +本次盤點花費大量時間是因為缺乏一份「當前狀態快照」文件。每次都要 SSH 進去逐一查,且沒有記錄,下次又要重新查。 + +### 解決方案:INFRASTRUCTURE-INVENTORY.md(Sprint A 結束後建立) + +**位置**:`docs/reference/INFRASTRUCTURE-INVENTORY.md` + +**內容結構**: + +```markdown +# 基礎設施庫存清單 +> 最後更新:{日期},由 ansible-playbook inventory-report.yml 自動生成 + +## 192.168.0.188 Docker Volumes +| volume 名稱 | 建立時間 | 掛載的容器 | 資料大小 | 狀態 | +|------------|---------|-----------|---------|------| +| open-webui | 2026-01-21 | open-webui | 120MB | 活躍 | +| n8n_n8n_data | 2026-03-07 | 無 | 0MB | 待清除 | +... + +## 192.168.0.110 systemd Services +| service 名稱 | WorkingDirectory | 所屬 repo | 狀態 | +|-------------|-----------------|---------|------| +| actions.runner.owenhytsai-awoooi.awoooi-110 | /home/wooo/actions-runner-awoooi | owenhytsai/awoooi | running | +... + +## 188 Nginx Sites +| conf 檔案 | 域名 | 後端 | SSL | 狀態 | +|----------|------|------|-----|------| +| all-sites.conf | aiops.wooo.work | 120:31234/31235 | ✅ | 活躍 | +... + +## SSL 憑證清單 +| 域名 | 到期日 | 對應 nginx conf | 狀態 | +|------|--------|---------------|------| +| gitlab.wooo.work | 2026-06-01 | 無 | 待刪除 | +... +``` + +**自動生成**:Sprint B 的 Ansible 中加入 `inventory-report.yml` playbook,執行後自動產生此文件並 git commit。從此每次 Sprint 後都有最新快照,永遠不需要重新猜測。 + +--- + +## 十一、112 Kali 預留接口文件 + +```yaml +# infra/ansible/inventory/hosts.yml 中: +security: + hosts: + host_112: + ansible_host: 192.168.0.112 + ansible_user: wooo + # 預留:本次不執行任何 playbook + # 未來規劃: + # - 角色:資訊安全掃描中心 + # - Scanner API::8080 + # - 預留 Nginx 入口:security.wooo.work + # - 獨立 ADR 討論安全架構整合 +``` + +--- + +--- + +## 十二、與現有專案整合 + +### 現有 Backlog 衝突分析 + +| 現有工作項目 | 衝突評估 | 結論 | +|------------|---------|------| +| B1 Sprint 5.2 全面監控 | 部分重疊:B1 監控 K8s metrics,Sprint A-0 修 Sentry,兩者**互補不衝突** | ✅ 並行安全 | +| B3 Trace Context UI | 純前端工作,與 infra sprint 無交集 | ✅ 並行安全 | +| D1 models.json 更新 | 後端設定檔,與 infra sprint 無交集 | ✅ 並行安全 | +| ADR-067 Ollama 應用 (Phase 30-34) | 依賴 188 Ollama:11434,Sprint A 清理不動 Ollama | ✅ 並行安全 | + +### 執行優先順序原則 + +``` +Sprint A(基礎清理)可立即開始,順序規則: + 1. A-0(Sentry 修復)→ 獨立,最高優先 + 2. A-1(188 GitLab 清除)→ 僅 188 Docker layer + 3. A-2(188 nginx 整合)→ 依賴 A-1 完成(GitLab conf 先移除) + 4. A-3(110 nginx 清理)→ 依賴 A-2 完成(188 接管 SSL 後) + 5. A-4(Harbor 修正)→ 獨立,可與 A-0 並行 + 6. A-5(open-webui 整理)→ 獨立 + 7. A-6(n8n 啟動)→ 獨立 + 8. A-7(bitan Docker 化)→ 依賴 A-3(110 nginx 清理後 proxy 設定確認) + 9. A-8(Stock HTTPS)→ 依賴 A-2(188 nginx 設定完成後加 SSL block) + 10. A-9(SENTRY_DSN 修正)→ 依賴 A-0(Sentry 確認運作後取 DSN) + +Sprint B(GitOps)→ 等 Sprint A 全完成後開始 +Sprint C(DR)→ 等 Sprint B Ansible 完成後開始(需要 Ansible inventory 正確) + +awoooi K8s pods(32334/32335)在 Sprint A/B/C 全程不觸碰。 +``` + +### 現有服務不中斷保證 + +| 服務 | 影響評估 | 保護措施 | +|------|---------|---------| +| awoooi API (K8s) | A-3 僅清 110 nginx,K3s 流量經 VIP:125 不受影響 | ✅ 零影響 | +| bitan.wooo.work | A-7 前保留 PM2:3003,Docker 驗證後才切換 nginx | ✅ 切換視窗 < 30秒 | +| tsenyang / momo | A-2 僅加 HTTPS redirect,HTTP 流量繼續 | ✅ 零影響 | +| stock.wooo.work | A-8 加 HTTPS,HTTP 繼續服務直到 SSL 確認 | ✅ 零影響 | +| Sentry | A-0 執行 `docker compose down && up`,預計 < 5 分鐘恢復 | ⚠️ 5分鐘維護視窗 | +| Harbor | A-4 只改 nginx upstream port,Harbor 本體不重啟 | ✅ 零影響 | + +--- + +## 十三、文件更新時間表 + +> 每個里程碑結束後,必須按以下清單更新文件。不得推遲到 Sprint 結束後才補。 + +### Sprint A 里程碑更新 + +| 里程碑 | 必須更新的文件 | 更新內容 | +|--------|-------------|---------| +| **A-0 完成** (Sentry 修復) | `project_current_status.md` | Sentry 狀態 → ✅ 運作中 | +| **A-0 完成** | `reference_four_hosts.md` | 110 Sentry:9000 狀態標記為 ✅ active | +| **A-0 完成** | LOGBOOK.md | 記錄 snuba 修復方法(DNS 問題根因) | +| **A-1 完成** (GitLab 清除) | `reference_four_hosts.md` | 移除 110 GitLab 相關記錄 | +| **A-4 完成** (Harbor 修正) | `reference_four_hosts.md` | Harbor upstream 更新為 :5000 | +| **A-7 完成** (Bitan Docker) | `reference_four_hosts.md` | 110 PM2 bitan:3003 → Docker bitan:3003 | +| **A-7 完成** | `project_master_workplan.md` | bitan Docker 化打勾 | +| **A-9 完成** (SENTRY_DSN) | LOGBOOK.md | 記錄 Phase 6 Sentry 整合修復完成 | +| **Sprint A 全完成** | `reference_four_hosts.md` | 完整更新所有服務/Port 正確狀態 | +| **Sprint A 全完成** | LOGBOOK.md | Sprint A 完整驗收紀錄 | +| **Sprint A 全完成** | `project_current_status.md` | 當前狀態更新 | + +### Sprint B 里程碑更新 + +| 里程碑 | 必須更新的文件 | 更新內容 | +|--------|-------------|---------| +| **B-Ansible 完成** | `.agents/skills/04-awoooi-devops-commander.md` | 加入 Ansible playbook 使用規範 | +| **B-Ansible 完成** | `reference_four_hosts.md` | 加入 Ansible inventory 路徑 | +| **B-ArgoCD 完成** | `docs/adr/ADR-069-infra-rebuild-gitops-dr.md` | 正式建立 ADR-069(從本 spec 摘要) | +| **B-ArgoCD 完成** | `project_master_workplan.md` | Sprint B 打勾 | +| **B-Nginx HA 完成** | `reference_four_hosts.md` | 更新 VIP:200 = 188 MASTER / 110 BACKUP | +| **Sprint B 全完成** | LOGBOOK.md | Sprint B 完整驗收 + GitOps 架構圖 | +| **Sprint B 全完成** | `INFRASTRUCTURE-INVENTORY.md` | Ansible 自動生成(`inventory-report.yml`) | + +### Sprint C 里程碑更新 + +| 里程碑 | 必須更新的文件 | 更新內容 | +|--------|-------------|---------| +| **C-Velero 完成** | `docs/runbooks/K3S-OPTIMIZATION-RUNBOOK.md` | 加入 Velero 備份/還原 SOP | +| **C-rsync 完成** | 新建 `docs/runbooks/HOST-BACKUP-RESTORE-SOP.md` | rsync 排程 + 手動還原步驟 | +| **Sprint C 全完成** | `project_master_workplan.md` | Sprint C 打勾,infra rebuild 正式關閉 | +| **Sprint C 全完成** | LOGBOOK.md | 最終驗收 + DR 演練結果 | +| **Sprint C 全完成** | `project_current_status.md` | 系統健康欄位更新(備份狀態) | + +### ADR-069 建立規範 + +``` +路徑:docs/adr/ADR-069-infra-rebuild-gitops-dr.md +建立時機:Sprint B ArgoCD 完成後(有實作依據才寫決策) +內容框架: + - 問題:手動設定 → 漂移、無法回滾 + - 決策:Ansible (Host IaC) + ArgoCD (K8s GitOps) + Velero (DR) + - 替代方案:Puppet/Chef(複雜度過高)、Terraform(非 Docker-native) + - 後果:需要 Ansible controller 節點(暫用 DevOps 金庫 110) +``` + +### 112 Kali 預留 ADR 說明 + +``` +未來討論安全架構整合時,建立獨立 ADR(序號 TBD): + - 112 Kali → security.wooo.work 或專用 VPN 接口 + - Scanner API :8080 整合到 AWOOOI 主動防禦流程(Phase 25) + - 不在本 ADR-069 範圍內 +``` + +--- + +*文件版本 v1.3(2026-04-10)— 整合專案工作計畫、文件更新時間表、ADR-069 建立規範*