FR-184 · 設計文件 v1.0 · 定稿 · 2026-11-07

設計 v1.0:一台 GCE VM 跑既有安裝版,Terraform 建機

e2-standard-2(2 核 8 GB)+8 GB swap、GCP 台灣、Cloudflare 前門、host 模式、Turnstile 開、升級走更新中心;六張卡五批

決策者 2026-11-07 裁完 D1~D8。本版是完整的設計文件,讀者不必回頭看討論稿;討論稿(discussion.md)保留費用實查與 K8s 對照供稽核。主產品只改一處:Turnstile 改成「有填 key 就開」,同一包落地版與雲端版通用;其餘改的是出版管線加一台目標主機與新 repo guidant-ai-infra 的 Terraform。

D1~D8 已裁 2026-11-07 六張卡五批:T1 → T2 → T3+P1 → O1 → V1 主產品只改 Turnstile 開關;apply 類兩張首腦親手做 每月約 82 美元(8 GB 隨需)

變更紀錄

日期 變更 對應
2026-11-07 v1.0 定稿:D1~D8 落定案表;拆六張卡四批;D3 規格由討論稿建議的 16 GB 改為決策者裁的 8 GB 討論稿 discussion.md
2026-11-07 D2 改判:Turnstile 列上線必要條件,加 F1(fe)、T4(infra)兩張卡,V1 移到最後一批;八張卡六批 決策者同日裁示
2026-11-08 D8 改為升級走更新中心線上更新,CI 直接部署(P1)留作救火 決策者裁示
2026-11-08 D2 做法改為「有填 key 就開」,主產品改動(BE a108ab505、FE f95a85b8);F1、T4 取消,六張卡五批;§5.2 裝機步驟改走 install.conf 決策者裁示;沿革見 handoff/FR-184-LOG.md

1. 這案在做什麼

在 GCP 台灣機房開一台 VM,用 Terraform 建好它需要的一切(網路、防火牆、磁碟、固定 IP、快照、備份 bucket、祕密容器、告警),然後在上面跑主產品既有的安裝包。做完等於多了一台住在雲端的 POC:決策者自己營運、多租戶、客戶用瀏覽器進來試用;升級跟 189 一樣在 GitLab 按一個鈕。

不做 K8s。理由在討論稿 §3.3:費用兩倍以上、程式要改八處、上線多三週,而 POC 階段用不到它買的那三樣東西(多副本、獨立擴縮、節點自動替換)。觸發條件:有付費客戶要 SLA、或要零停機升級。

2. 分工概述(30 秒版)

批 卡 做什麼 誰做 完成怎麼判定(決策者檢查法)
1 T1 Terraform 平台 module 寫完,plan 看得到全部資源 runner 首腦貼 plan 摘要:VM、磁碟、IP、3 條防火牆、bucket、6 個 secret 的 IAM 綁定、2 條告警,沒有紅字
2 T2 apply 建機;Cloudflare 子網域指到固定 IP 首腦親手 瀏覽器開 https://guidant-ai.jedicotech.com 看到 502(TLS 與 DNS 通,服務還沒裝);直打 IP 連不上
3 T3 VM 上跑既有安裝包、自帶密碼、License 開通、填 Brevo 與 AI 金鑰 首腦親手 決策者登入、建租戶、上傳證據、AI 分類、匯出 pdf、收到一封系統信
3 P1 出版管線直接部署多一台 gcp-prod(救火用,平常升級走更新中心) runner GitLab Run pipeline 選 gcp-prod、按 ▶,約 10 分鐘後雲端版本號變新
4 O1 每日 pg_dump 推 GCS、告警實測、災難復原演練 runner 寫腳本,首腦演練 隔天 GCS 有 dump;灌記憶體收到告警信;從快照起第二台能登入看到同樣資料
5 V1 §8 十項驗收、文件收口 決策者驗,runner 收文件 十項全勾

依賴:批 1 的前置(GCP 專案、帳單、API、state bucket、infra repo 骨架)已於 11-07 完成並驗過。T2 要決策者給 Cloudflare 權杖與網域;T3 要決策者先把自帶密碼、Brevo 帳密、AI 金鑰放進 Secret Manager。

3. 決策定案表

# 題目 定案 理由 被排除的
D1 部署形態 DEPLOYMENT_MODE=host,與 189 相同 saas 有兩個既有缺口:總部判定讓郵件/LDAP/安全政策三頁消失(common/authz/tenant_hq.py)、更新中心只認 host 照(product_update_flow.py:193)。host 在 VM 上沒有任何技術問題,機器指紋綁的是自己的 VM saas:另開 FR 補兩個缺口再切;順便把 DEPLOYMENT_MODE 改名成它真正控制的「信任模型」
D2 Turnstile 人機驗證 開,上線必要條件;裝機時 install.conf 填 GUIDANT_TURNSTILE_SITE_KEY/GUIDANT_TURNSTILE_SECRET_KEY 主產品的開關是「兩把都填真 key 才開」(common/util/turnstile_state.py),後端驗 token、登入頁(讀 /setup/status)、前門 CSP(放行 challenges.cloudflare.com)三處同一判定。key 在 compose .env,升級保留,同一顆 FE image 落地版與雲端版通用 關(討論稿建議):POC 也要有人機驗證;另出雲端版 FE image:升級會被蓋回關閉版,且前門 CSP 會擋 Turnstile
D3 規格 e2-standard-2(2 核 8 GB)+8 GB swap;告警門檻記憶體 80%、swap 用掉一半 決策者裁先 8 GB。首腦已告知風險:190 乾淨狀態 6 GB、api 容器上限 6 GB、installer 最低值 16 GB,AI 分類或匯出同時跑可能 OOM。swap 當緩衝、告警響了改一個變數升級 16 GB(討論稿建議):費用每月多 57
D4 祕密 Secret Manager 只建殼,值由決策者手動放;DB/Redis 六組密碼決策者自帶 Terraform state 與 git 永遠沒有密碼。installer 偵測到既有 .env 會沿用不重產(install.sh 第 1211 行),所以裝機前先把自帶密碼寫進 .env 即可。JWT、加密鑰、S3、classifier 四組 installer 強制產生(內部憑證不開放自帶),產完回寫 Secret Manager 備查 全部自動產生:決策者要自己管
D5 防火牆 80/443 只放行 Cloudflare IP 範圍;22 只放行 188 runner 與管理者 IP 繞過 Cloudflare 直打 VM 會被擋,WAF 才有意義。IP 清單從 https://www.cloudflare.com/ips-v4 抓 開給全世界
D6 寄信 Brevo SMTP 587 GCE 封 25 port;公司信箱不是 Google Workspace;Brevo 免費每天 300 封、自家網域當寄件人。在後台郵件設定頁填,不進 .env Gmail 當 SMTP:每天 500 封上限、易被鎖、寄件人是 gmail.com
D7 License Center 放行 Cloudflare Access 鎖 VM 固定 IP 一台、IP 固定、BE 程式不動。Terraform output 印 IP,決策者貼到 Access 規則 service token:多台或 K8s 再改
D8 升級路徑 平常走更新中心線上更新,只收正式發版;CI 直接部署 deploy-gcp-prod(比照 poc,GUARDED=1、when: manual)留作救火 雲端版有外部客戶試用,等同正式環境;直接部署可推任何 branch,線上更新只拿得到簽正式章的版本。自己天天用也等於持續驗客戶會走的那條路(下載、驗章、八步升級、失敗退回)。安裝包預設寫入 UPDATE_SITE_URL,裝完就接更新站。救火時(線上更新卡住、急修還沒正式發布)才用直接部署 只用直接部署:半成品容易誤進正式環境,也測不到更新中心那條路

4. 架構

FR-184 架構

圖 1 — 一台 VM 跑既有十個容器;Cloudflare 擋前面;升級走更新中心,CI 直接部署留作救火(點圖看大圖)

FR-184 雲端架構(GCP 官方圖示)

圖 2 — 雲端版架構,GCP 官方圖示,對齊 2026-11-08 實際建出來的資源(點圖看大圖);圖源 diagrams/cloud_architecture.py

兩層切開,換雲的縫在 module outputs:

層 內容 換雲時
平台層 modules/platform-gcp/ VPC、防火牆、VM、資料碟、固定 IP、快照政策、GCS 備份 bucket、Secret Manager 殼、告警、開機腳本 新增 modules/platform-<雲>/ 輸出同一組欄位
介面(outputs) public_ip、instance_name、ssh_user、backup_bucket、data_mount_path 不變
應用層 主產品安裝包+install.sh 不變

5. 詳細設計

5.1 Terraform 資源(T1)

資源 設定 為什麼
VPC+子網 10.10.0.0/24,asia-east1 自己一個網路,不用 default
防火牆 allow-cloudflare-web tcp 80/443,source=Cloudflare ips-v4+ips-v6(data "http" 抓) D5
防火牆 allow-ssh-admin tcp 22,source=var.ssh_allowed_cidrs 188 runner 與管理者
防火牆 deny-all-ingress 最低優先權兜底 GCP default 網路的 allow 規則不會帶進來,但明寫保險
固定 IP google_compute_address DNS、LC Access 規則、CI 變數都指它
資料碟 pd-balanced var.data_disk_gb(預設 100) 掛 /srv/guidant-ai,與系統碟分開,快照只拍它
快照政策 每日、保留 7 天 災難復原的基礎
VM var.machine_type(預設 e2-standard-2)、Ubuntu 24.04 LTS、系統碟 30 GB、ssh user jedi user 名對齊 deploy_e2e.sh 的 jedi@
VM service account 只給 Secret Manager accessor、logging writer、monitoring writer 最小權限;裝機腳本用它讀 secret
備份 bucket asia-east1、版本控管、30 天生命週期 pg_dump 落點
Secret Manager 不建:六個 secret 決策者 11-07 已親手建好(guidant-db-admin/guidant-db-app/guidant-redis/guidant-redis-api/guidant-redis-socketio/guidant-redis-worker,各一版);Terraform 用 data 讀名稱、把 accessor 綁給 VM service account,不讀值 D4;SMTP 與 AI 金鑰走後台填(guidant-smtp/guidant-anthropic 兩個 secret 是備查,裝機不用)
告警 × 2 記憶體 > 80%、磁碟 > 85%,通知 var.alert_email D3 的保險絲;記憶體指標要裝 Ops Agent

開機腳本 scripts/startup.sh:建 swap(GCE 映像預設沒有)、裝 docker-ce 與 compose plugin、空碟才格式化並掛資料碟、裝 Ops Agent。不做安裝流程,那是 install.sh 的事。

5.2 裝機(T3)

  1. 首腦在 Mac 從 Secret Manager 讀值組 install.conf(600)送上 VM:主機名稱、資料目錄、DB 管理/DB 應用/Redis 主密碼、root admin 密碼、Anthropic 金鑰、Turnstile 兩把 key。
  2. 從 188 scp 安裝包與 .sha256,驗完解開,sudo ./install.sh --config install.conf。host 模式是 installer 預設。
  3. 裝完換 Cloudflare Origin 憑證:覆蓋 certs/server.crt、server.key,chown 101:101 server.key,docker compose up -d guidant-fe。不能裝前先放:certs/server.key 存在時 installer 判定已有安裝而停下(install.sh:933)。
  4. 刪 VM 上的 install.conf;installer 自產的 JWT、加密鑰、S3、classifier、Redis 三組行程帳號回寫 Secret Manager 備查。Redis 那三組全新安裝一律自產,不讀 install.conf。
  5. 決策者登入、License 線上開通(貼序號)、後台填 Brevo 587。

機器指紋:host 模式綁 /etc/machine-id。GCE 從映像開機會產生新的;從快照還原的 VM 沿用舊的,災難復原後 License 不用重開。從頭重建 VM 指紋會變,要重新開通,寫進 runbook。

5.3 升級與出版管線(P1)

平常升級:管理員登入 → 系統設定 → 更新中心 →「立即檢查」→「更新到 X」。只看得到正式發版:雲端版的機器指紋不登記進更新站「內部主機清單」,guidant.env 不加 PRODUCT_UPDATE_ALLOW_SAME_VERSION,測試版發布的草稿就到不了這台。Turnstile 的 key 在 compose .env,升級保留,升完不必補任何步驟。

救火用直接部署(P1):照既有三台的樣子加第四台:.gitlab-ci.yml 的 target 選項加 gcp-prod;deploy.yml 加 deploy-gcp-prod job 比照 poc;deploy_e2e.sh 的 case 加一行,主機從 CI 變數讀。188 runner 的公鑰(~jedi/.ssh/id_ed25519.pub,註解 guidant-build-188,11-07 查過存在)進 T1 的 ssh_public_keys。

5.4 備份與復原(O1)

每日 cron:pg_dump -Fc → gcloud storage cp 到備份 bucket。資料碟每日快照。復原步驟:從快照起新 VM → 裝同版 → restore dump → License 不用重開(machine-id 沿用)。演練一次並 destroy 臨時 VM。

6. 現況接入點盤點

元件 現況 本案動作
docker/production/docker-compose.yml、install.sh 落地版安裝流程 不動
scripts/build/deploy_e2e.sh 三台 case 加 gcp-prod case(P1)
.gitlab/ci/deploy.yml、.gitlab-ci.yml 三個 deploy job 加 deploy-gcp-prod、target 選項(P1)
nginx.onprem.conf(FE repo) 只轉 X-Forwarded-For,沒處理 CF-Connecting-IP 不動;稽核紀錄要真實 IP 另開 FE 卡(§9)
common/authz/tenant_hq.py、product_update_flow.py saas 兩個缺口 不動;另開 FR(§9)
guidant-ai-infra(新 repo) 骨架:.gitignore、CLAUDE.md、envs/gcp-prod/{versions,variables}.tf、.terraform.lock.hcl T1 填 module 與 main.tf
.claude/workshop.toml [repos] 六個 repo 加 infra(開卡前由首腦登記)

7. 拆分表

由 handoff/cards.py 產,已驗:依賴都在更早的批、同批同 repo 零撞檔。

批 卡 repo 做什麼 reuse/extend/new 改哪些既有檔 新開檔 依賴 model/effort
1 T1 infra Terraform 平台 module 全部資源+開機腳本,做到 plan new:modules/platform-gcp/*.tf、scripts/startup.sh、envs/gcp-prod/{main,outputs}.tf;extend:variables、tfvars 範本、module README envs/gcp-prod/variables.tf、terraform.tfvars.example、modules/platform-gcp/README.md 六支 — opus/high
2 T2 infra apply 建機、Cloudflare DNS 與 Origin 憑證、LC Access 加 IP(首腦親手) new:docs/runbook.md 建機節 — docs/runbook.md T1 opus/medium
3 T3 infra 裝機與開通、密碼回寫(首腦親手) new:scripts/env_from_secrets.sh;extend:runbook 三節 docs/runbook.md scripts/env_from_secrets.sh T2 opus/medium
3 P1 main 直接部署多一台 gcp-prod extend:三支 CI 檔、FR-173 design 四台表 .gitlab-ci.yml、.gitlab/ci/deploy.yml、scripts/build/deploy_e2e.sh、FR-173 design-v2.0.md — T1 sonnet/medium
4 O1 infra 備份 cron、告警實測、災難復原演練 new:scripts/backup_pg.sh;extend:startup 裝 cron、runbook docs/runbook.md、scripts/startup.sh scripts/backup_pg.sh T3 sonnet/medium
5 V1 main §8 驗收、README/登記表/CLAUDE.md/memory 收口 extend FR-184 README、docs/features/README.md、CLAUDE.md memory 一支 T3、P1 sonnet/low

T2、T3 是 apply 類,依 infra repo CLAUDE.md「runner 只做到 plan」由首腦親手做;開卡是為了留紀錄與驗收落點,Notion 作業人員仍標「小弟」但 Model 欄留空。

8. 端到端驗收

決策者親手:

  1. 瀏覽器開 https://guidant-ai.jedicotech.com,憑證是 Cloudflare 簽的、沒有警告。直打 VM IP 連不上。
  2. 管理員登入,建一個租戶、一個使用者,用新使用者登入。
  3. 建專案、上傳證據檔、按 AI 分類,結果回來。
  4. 匯出 SSP docx 與 pdf,中文沒有消失。
  5. 觸發密碼重設,收到 Brevo 寄的信,寄件人是自家網域。
  6. 更新中心「立即檢查」回正常。
  7. 有新正式版時,更新中心「立即檢查」看到新版、按「更新到 X」,升完版本號變新、資料都在、Turnstile 仍開著。
  8. 隔天 GCS bucket 有 pg_dump;主控台看到資料碟快照。
  9. 從快照起一台新 VM、restore dump,能登入看到同樣資料(首腦做,決策者看)。
  10. 登入頁看得到 Turnstile 小工具(DevTools Network 有 challenges.cloudflare.com),正確帳密能登入;升級後不必做任何事,小工具仍在。

9. 不做什麼

  • K8s:FR-065 §4.8 五條前提與 Helm chart 全部延後。
  • saas 模式:另開 FR 補總部判定與更新中心認照,順便把 DEPLOYMENT_MODE 改名為它真正控制的事(信任模型)。
  • Cloudflare 真實 IP 進稽核紀錄:nginx 要加 set_real_ip_from Cloudflare 範圍+real_ip_header CF-Connecting-IP,另開 FE 卡。
  • STG 環境:envs/ 保留位置,要加是複製變數檔。
  • 多租戶自助註冊、高可用:POC 不做。
  • Agent 接雲端:等 FR-181 合回。

10. 風險與陷阱

  • 8 GB 的 OOM 風險:compose 上限加總 17 GB,api 一個容器上限 6 GB。AI 分類批次、弱點掃描、LibreOffice 匯出同時發生會踩到。對策:8 GB swap、告警 80%、升級只改 machine_type 重 apply(關機重開兩分鐘,資料碟不動)。
  • installer 印警告:最低值 16 GB,低於會印 ⚠ 但不擋裝(README 第 552 行實測 7 GB RC=0)。
  • 帳單帳戶風控:11-07 第一個專案撞過。apply 前先 gcloud billing projects describe guidant-ai-cloud-514012 看 billingEnabled: true。
  • 25 port:後台 SMTP 填 25 會靜默寄不出去,runbook 明寫 587。
  • Cloudflare IP 清單會變:Terraform 每次 plan 重抓,清單變了 plan 會顯示防火牆更新,屬正常。
  • swap 在 GCE 映像預設沒有:startup.sh 建,漏了 8 GB 就是硬上限。
  • 從頭重建 VM 指紋會變:License 要重開通;從快照還原不會。
  • Turnstile 只認 .env 的兩把 key:填在 guidant.env 不會生效(前門讀不到 site key)。guidant.env 若有 TURNSTILE_ENABLED=false 會強制關,要開時一併刪掉。