FR-184 · 設計文件 v1.0 · 定稿 · 2026-11-07
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。
| 日期 | 變更 | 對應 |
|---|---|---|
| 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 |
在 GCP 台灣機房開一台 VM,用 Terraform 建好它需要的一切(網路、防火牆、磁碟、固定 IP、快照、備份 bucket、祕密容器、告警),然後在上面跑主產品既有的安裝包。做完等於多了一台住在雲端的 POC:決策者自己營運、多租戶、客戶用瀏覽器進來試用;升級跟 189 一樣在 GitLab 按一個鈕。
不做 K8s。理由在討論稿 §3.3:費用兩倍以上、程式要改八處、上線多三週,而 POC 階段用不到它買的那三樣東西(多副本、獨立擴縮、節點自動替換)。觸發條件:有付費客戶要 SLA、或要零停機升級。
| 批 | 卡 | 做什麼 | 誰做 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|---|
| 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。
| # | 題目 | 定案 | 理由 | 被排除的 |
|---|---|---|---|---|
| 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,裝完就接更新站。救火時(線上更新卡住、急修還沒正式發布)才用直接部署 |
只用直接部署:半成品容易誤進正式環境,也測不到更新中心那條路 |
兩層切開,換雲的縫在 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 |
不變 |
| 資源 | 設定 | 為什麼 |
|---|---|---|
| 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 的事。
install.conf(600)送上 VM:主機名稱、資料目錄、DB 管理/DB 應用/Redis 主密碼、root admin 密碼、Anthropic 金鑰、Turnstile 兩把 key。.sha256,驗完解開,sudo ./install.sh --config install.conf。host 模式是 installer 預設。certs/server.crt、server.key,chown 101:101 server.key,docker compose up -d guidant-fe。不能裝前先放:certs/server.key 存在時 installer 判定已有安裝而停下(install.sh:933)。install.conf;installer 自產的 JWT、加密鑰、S3、classifier、Redis 三組行程帳號回寫 Secret Manager 備查。Redis 那三組全新安裝一律自產,不讀 install.conf。機器指紋:host 模式綁 /etc/machine-id。GCE 從映像開機會產生新的;從快照還原的 VM 沿用舊的,災難復原後 License 不用重開。從頭重建 VM 指紋會變,要重新開通,寫進 runbook。
平常升級:管理員登入 → 系統設定 → 更新中心 →「立即檢查」→「更新到 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。
每日 cron:pg_dump -Fc → gcloud storage cp 到備份 bucket。資料碟每日快照。復原步驟:從快照起新 VM → 裝同版 → restore dump → License 不用重開(machine-id 沿用)。演練一次並 destroy 臨時 VM。
| 元件 | 現況 | 本案動作 |
|---|---|---|
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(開卡前由首腦登記) |
由 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 欄留空。
決策者親手:
https://guidant-ai.jedicotech.com,憑證是 Cloudflare 簽的、沒有警告。直打 VM IP 連不上。challenges.cloudflare.com),正確帳密能登入;升級後不必做任何事,小工具仍在。DEPLOYMENT_MODE 改名為它真正控制的事(信任模型)。set_real_ip_from Cloudflare 範圍+real_ip_header CF-Connecting-IP,另開 FE 卡。envs/ 保留位置,要加是複製變數檔。machine_type 重 apply(關機重開兩分鐘,資料碟不動)。gcloud billing projects describe guidant-ai-cloud-514012 看 billingEnabled: true。.env 的兩把 key:填在 guidant.env 不會生效(前門讀不到 site key)。guidant.env 若有 TURNSTILE_ENABLED=false 會強制關,要開時一併刪掉。