FR-184 · 需求討論稿 v1 · 2026-11-07(D1~D8 已裁,定案在 design.md)

主系統上 GCP

急件:POC 階段先用一台 GCE VM 跑既有安裝版 stack,程式零改動;基礎設施用 Terraform 描述,換雲只換一個 module;K8s 延後到有 SLA 需求

決策者 11-07 定調:急著上雲端、自己營運、POC 階段、一套就好、GCP 台灣機房;規格本稿建議 4 核 16 GB,決策者裁先 8 GB(D3)。本稿把三件事講清楚:為什麼 VM 不是 K8s、每月要花多少(全部實查,不是記憶值)、十個服務在 VM 上能不能全起(全部能,已對照 189 POC 實測)。探脈發現一個會改設定方向的事:DEPLOYMENT_MODE=saas 現在不能直接用(三個設定頁會消失、更新中心不認照),雲端這台第一期要跑 host 模式,見 D2。

決策者已裁:VM 不是 K8s,POC 一套 決策者已裁:先 8 GB(本稿建議 16)、GCP 台灣 決策者已裁:Cloudflare 前門、Brevo 寄信、LC 鎖 IP D1~D8 已裁,定案見 design.md 程式零改動;Terraform repo 骨架已建 費用每月約 144 美元(實查)

0. 分工概述(30 秒版)

整案一句話:在 GCP 台灣機房開一台 VM,用 Terraform 建好網路、磁碟、固定 IP、備份、祕密容器,然後在上面跑主產品既有的 install.sh,等於多了一台住在雲端的 POC;出版管線的「直接部署」多一台目標主機,之後升級跟 189 一樣按一個鈕。

階段 做什麼 產出 完成怎麼判定(決策者檢查法)
批 1 環境 guidant-ai-infra repo 登記進工作坊;Terraform 平台 module 寫完,plan 看得到要建的全部資源 modules/platform-gcp/*.tf、envs/gcp-prod/main.tf 首腦跑 terraform plan,列出 VM、磁碟、IP、防火牆、bucket、secret 容器,沒有紅字
批 2 建機 首腦 terraform apply;Cloudflare 子網域指到固定 IP GCP 上一台 VM、一個對外 IP、DNS 一筆 瀏覽器開 https://<網域> 看到 nginx 的 502(服務還沒裝,但 TLS 與 DNS 通)
批 3 裝機 在 VM 上跑既有安裝包、填 .env、License 線上開通、SMTP 填 Brevo 可登入的系統 決策者登入、建專案、上傳證據、按 AI 分類、匯出 SSP pdf、收到一封系統信
批 3 管線 FR-173 直接部署加目標 gcp-prod .gitlab/ci/deploy.yml 多一個 job GitLab Run pipeline 選 gcp-prod,約 10 分鐘後雲端版本號變新
批 4 營運 每日快照、pg_dump 推 GCS、記憶體與磁碟告警 排程與告警規則 隔天 GCS 裡看到一個 dump 檔;手動把記憶體灌滿收到一封告警信
批 4 驗收 全功能跑一輪 驗收清單勾完 清單見 §7

依賴:批 1 前決策者要做完的事已全部完成(GCP 專案 guidant-ai-cloud-514012、帳單、API、state bucket,11-07 驗過)。批 2 要決策者給 Cloudflare 的 API 權杖與網域名稱。批 3 要 Brevo 帳號與 AI 金鑰。

1. 為什麼要做

主系統到今天只有落地版:客戶自己的主機裝安裝包。決策者 11-07 要自己營運一套雲端版給客戶試用,POC 階段,而且急。

現況對這件事有利的地方比想像多:

  • 安裝版 compose 是乾淨的服務契約,十個服務各自一個容器,FR-065 當初就為「客戶隨便一台 Ubuntu」設計,GCE VM 就是那種主機。
  • 出版管線(FR-173)已經能對任一台主機 ssh 送安裝包跑 install.sh --upgrade,雲端這台只是第四台。
  • 儲存已走 S3 相容介面(SeaweedFS),寄信、AI、License 全部是設定不是程式。

不利的只有一件:程式裡有「跑在一台機器上」的假設(機器指紋、主機撿升級單、排程跟著 api),這些在 VM 上完全不是問題,只在 K8s 才會撞到。所以 VM 是急件的正解,K8s 留到有 SLA 需求。

2. 架構

FR-184 架構:Cloudflare 前門、GCP 一台 VM 跑既有 stack、CI ssh 部署

圖 1 — 一台 VM 跑既有十個容器;Cloudflare 擋前面;CI 走既有直接部署那條路(點圖看大圖)

兩層切開,換雲的縫在中間:

層 內容 換雲時
平台層(Terraform,modules/platform-gcp/) VPC、防火牆、VM、資料碟、固定 IP、快照排程、GCS 備份 bucket、Secret Manager 容器、開機腳本(swap、docker、掛碟) 重寫這個 module
介面(module outputs,固定欄位) public_ip、instance_name、ssh_user、backup_bucket、data_mount_path 不變
應用層 主產品安裝包+install.sh,一行不改 不變

Terraform repo guidant-ai-infra 骨架已建(commit 7ec4f3a、78e8255):.gitignore、CLAUDE.md(runner 只做到 plan,apply 由決策者或首腦)、envs/gcp-prod/ 的 provider 與 GCS 後端、terraform init 與 validate 已在新專案上通過。

3. 費用(全部實查)

抓法:Google 定價頁是前端渲染,用無視窗瀏覽器切到台灣區抓;AWS 抓定價頁背後的官方 JSON;DigitalOcean 與 Hetzner 官方頁純文字。抓取日 2026-11-07,美元。

3.1 單價

項目 GCP 台灣 AWS 台北 AWS 東京
4 核 16 GB 每小時 e2-standard-4 0.1552 t3.xlarge 0.1958 t3.xlarge 0.2176
4 核 32 GB 每小時 e2-highmem-4 0.2093 — —
磁碟每 GB 月 pd-balanced 0.10 gp3 0.0864 gp3 0.096
快照每 GB 月 0.05 0.045 0.05
固定 IP 每小時 0.005 0.005 0.005
出站流量每 GB 亞洲 0.12 每月 100 GB 免費 同左
一年承諾 最多 63 折 t3.xlarge 0.137 同左

DigitalOcean 與 Hetzner 實查後退出候選:前者沒有 4 核 16 GB 的 Basic 方案(要 General Purpose 月費 126,不便宜);後者新加坡節點沒有 4 核 16 GB、多個方案標缺貨。

3.2 整套月費(4 核 16 GB、100 GB 碟、快照、IP、備份、50 GB 流量)

144GCP 台灣,隨需

100GCP 台灣,一年承諾後

166AWS 台北,隨需

184GCP 台灣 32 GB,隨需

16 GB 對 32 GB 差 40 美元(承諾後差 25)。決策者已選 16 GB。

3.3 K8s 對照(決策者看過,留紀錄)

VM 16 GB GKE Autopilot 正統(受管 DB/Redis) GKE 高可用
每月隨需 144 約 290(pod 申請量縮到實測峰值後) 約 770
程式改動 零 八處拿掉單機假設 同左
上線 3 到 5 天 3 到 4 週 3 到 4 週

K8s 貴在兩件結構性的事:Autopilot 每個 pod 按申請量預留、不能像單機那樣共用記憶體;DB、Redis、負載平衡器在單機裡是免費搭便車,K8s 各自一張帳單。決策者 11-07 裁「POC 走 VM」。

4. 十個服務在 VM 上能不能起(對照 189 POC 實測)

結論:全部能,不需要 GPU、不需要巢狀 docker、不需要特權容器。

服務 怎麼跑 189 現值 190 現值 compose 上限
api BE image,RUN_MODE=api 4.0 GB 1.9 GB 6 GB
worker 同 image 2.2 GB 1.6 GB 3 GB
socketio 同 image 1.8 GB 1.6 GB 3 GB
sandbox AI 分類器 evidence-classifier image,Prompt Guard ONNX 模型 1.1 GB 跑 CPU;真正的 LLM 分類打 api.anthropic.com 0.1 GB 閒置 同左 4 GB
弱點掃描、SBOM、CINC 在 api 容器內 含在 api
LibreOffice 轉檔、PDF 匯出 api 容器內建 含在 api
db、redis、seaweedfs、bugsink 各自容器 0.9+0.01+0.7+0.4 GB bugsink 1 GB
fe nginx 對外 80/443 0.01 GB
合計 約 10 GB 約 6 GB 硬上限加總 17 GB

189 是跑六天、有舊資料的狀態;4 核 Xeon 平均負載 0.68,CPU 很閒。磁碟方面 189 用 135 GB,其中 30 GB 是歷史 image、6.6 GB 升級包暫存、9 GB 證據檔,資料庫只有 631 MB;雲端從乾淨安裝起算,100 GB 夠 POC。

16 GB 的風險講清楚:compose 硬上限加總 17 GB,只有「AI 分類批次、弱點掃描、LibreOffice 匯出同時發生」才碰到。對策是 8 GB swap 當保險絲(189 也有 4 GB swap)加「記憶體超過 85% 或 swap 動用就告警」,告警響了 Terraform 改一個變數升 32 GB,關機重開兩分鐘。

5. 待決策

D1 — 雲端這台的部署形態:第一期跑 host,不跑 saas

探脈發現 DEPLOYMENT_MODE=saas 現在有兩個洞:①全公司共用設定(SMTP/LDAP/登入政策)讀寫都卡「總部」(common/authz/tenant_hq.py),saas 下沒有任何租戶算總部,三頁整個消失(DEV 2026-10-04 就是因此改回 host,見 memory project_dev_deployment_mode_host);②更新中心只認落地版的照(product_update_flow.py:193),saas 照會讓「立即檢查」回尚未開通。

建議:第一期 DEPLOYMENT_MODE=host、TURNSTILE_ENABLED=false,跟 189 一模一樣,等於多一台 POC 住在雲端。機器指紋綁這台 VM(Terraform 建的 VM 有穩定的 /etc/machine-id),License 走線上開通或離線開通都行。saas 模式的兩個洞另開 FR(總部判定、更新中心認 saas 照),做完再切。理由:急件不該被 saas 的既有缺口擋住;host 模式在 VM 上沒有任何技術問題。

D2 — Turnstile 人機驗證:第一期關,不開

FE image 是落地版 build(.env.onprem 烙 VITE_TURNSTILE_ENABLED=false),要開 Turnstile 得重 build 一顆 FE image 並拿正式的 site key。

建議:第一期關,與 D1 一致。雲端站有 Cloudflare WAF 擋在前面,POC 階段使用者是邀請制,沒有公開註冊入口,暴力登入的風險可接受。要開時另開一張卡:FE 多一個 build mode、Cloudflare 申請 site key。

D3 — 規格與 swap:e2-standard-4、8 GB swap、告警 85%

建議:照決策者已裁的 16 GB。swap 與告警寫進 Terraform 與開機腳本。升級路徑是改 machine_type 變數重 apply。

D4 — 祕密放哪:Secret Manager 只放容器,值由決策者手動放

Terraform 建 secret 的「殼」(名稱與權限),不寫值;值由決策者用 gcloud secrets versions add 放進去。VM 開機腳本或裝機步驟從 Secret Manager 讀出來填 .env。

建議:這樣 Terraform state 與 git 裡永遠沒有密碼。要放的有:DB 兩組密碼、Redis 四組、SMTP 帳密、AI 金鑰、License Center token、classifier token、bugsink secret。裝機時 install.sh 本來會自動產生 DB/Redis 密碼,只有 SMTP、AI、LC token 三組是外部給的,其餘讓 installer 產生後回寫 Secret Manager 備查。

D5 — 防火牆:80/443 只放行 Cloudflare 的 IP 範圍,不開給全世界

建議:Terraform 從 Cloudflare 公開的 IP 清單(https://www.cloudflare.com/ips-v4)產防火牆規則,繞過 Cloudflare 直打 VM IP 會被擋,WAF 才有意義。SSH 只放行 GitLab CI runner(188 的對外 IP)與決策者的 IP,寫在 ssh_allowed_cidrs 變數。

D6 — 寄信:Brevo SMTP 587

GCE 封鎖 25 port 出站,這是 GCP 鐵則。公司信箱不是 Google Workspace。

建議:Brevo 免費方案每天 300 封,用自家網域當寄件人,Cloudflare 加三筆 DNS 驗證。SMTP 設定在主系統後台「郵件設定」頁填(host 模式下總部看得到),不進 .env。

D7 — License Center 放行:Cloudflare Access 鎖 VM 固定 IP

建議:決策者已選。Terraform output 印出 public_ip,決策者貼到 Cloudflare Access 的 IP 規則。BE 程式不動。將來多台或換 K8s 再改 service token。

D8 — 出版管線:gcp-prod 比照 poc,部署前要按 ▶ 確認

FR-173 的 deploy.yml 一個 job 對應一台,poc 是 when: manual。

建議:deploy-gcp-prod 比照 poc:GUARDED=1、when: manual。deploy_e2e.sh 的 case 多一行 gcp-prod) E2E_HOST=jedi@<public_ip>。IP 從 Terraform output 來,寫進 GitLab CI 變數 GCP_PROD_HOST,不寫死在腳本。

6. 不做什麼

  • K8s:五條應用層前提(FR-065 §4.8)與 Helm chart 全部延後。觸發條件:有付費客戶要 SLA、或要零停機升級。
  • saas 模式:D1 說明的兩個洞另開 FR,本案不碰 tenant_hq.py 與 product_update_flow.py。
  • 多租戶自助註冊:POC 階段租戶由管理員在後台建。
  • STG 環境:決策者裁一套就好。envs/ 目錄結構保留 gcp-stg/ 的位置,要加是複製變數檔。
  • Agent 連線方向:FR-181 已在做「Agent 只出站」,本案不重複。雲端版接 Agent 等 FR-181 合回。
  • 高可用:單機,掛了從快照起新機。恢復步驟寫進營運手冊。

7. 端到端驗收

批 4 驗收卡的清單,決策者親手做:

  1. 瀏覽器開 https://<網域>,憑證是 Cloudflare 簽的、沒有警告。直打 VM IP 被擋。
  2. 用管理員登入,建一個租戶、一個使用者,用新使用者登入。
  3. 建專案、上傳一份證據檔(走 SeaweedFS)、按 AI 分類,分類結果回來。
  4. 匯出 SSP docx 與 pdf,中文沒有消失(LibreOffice 與字型在 image 內)。
  5. 觸發密碼重設,收到 Brevo 寄的信,寄件人是自家網域。
  6. 更新中心「立即檢查」回正常(host 照)。
  7. GitLab Run pipeline 選 gcp-prod,按 ▶,約 10 分鐘後系統版本號變新、資料都在。
  8. 隔天 GCS bucket 裡有 pg_dump;GCP 主控台看到資料碟快照。
  9. 從快照起一台新 VM、裝同版、restore dump,能登入看到同樣資料(災難復原演練,首腦做)。

8. 風險與陷阱

  • 帳單帳戶被風控關閉:11-07 第一個專案就撞到,重建帳單帳戶與專案後才過。Terraform apply 前先 gcloud billing projects describe 確認 billingEnabled: true。
  • GCE 的 /etc/machine-id:host 模式綁機器指紋。GCE 從映像開機會產生新的 machine-id,但從快照還原的 VM 會沿用舊的,災難復原後指紋不變,License 不用重開。從頭重建 VM 指紋會變,要重新開通,寫進營運手冊。
  • 25 port:主系統後台 SMTP 設定若填 25 會靜默寄不出去,手冊明寫 587。
  • Cloudflare 代理後的真實 IP:nginx 看到的來源 IP 是 Cloudflare 的,稽核紀錄要真實 IP 得讀 CF-Connecting-IP。現在 nginx.onprem.conf 沒處理,列為批 3 的小項:加 real_ip_header 設定。
  • swap 在 GCE 映像預設沒有:開機腳本要建,漏了 16 GB 就是硬上限。

9. 參考

  • FR-065 落地版 installer(compose 服務契約、§4.8 K8s 預留五條)
  • FR-173 出版管線 v2.0(直接部署那條路)
  • FR-167 線上更新通道(更新中心只認 host 照的那一段)
  • memory project_dev_deployment_mode_host(saas 模式三頁消失的根因)
  • Terraform repo:~/Projects/Billows/Audit-Manager/guidant-ai-infra(GitLab auditmanager/guidant-ai-infra)
  • 費用原始頁面文字:首腦工作暫存區 scratchpad/pricing/*.txt(不入版控)