第 10 章:收尾 — 完整 pipeline 全貌 + 安全拆除
這一章在做什麼? 我們把前面所有環節串成一條 gated pipeline:push 到 main → 自動跑測試 → 通過才 build 並推 image → 再自動部署到 Cloud Run。重點是用 Workload Identity 讓 GitHub Actions 免金鑰安全地部署到 GCP。做完這章,你改一行 code、git push,它就自己一路跑到上線——這就是 DevOps 的終點風景。最後附上一份完整拆除清單,確保不留下任何偷偷計費的東西。
前置:第 3–9 章。本章新增
code/terraform/cicd.tf與code/.github/workflows/deploy.yml。💸 本章會短暫產生費用(要把 Cloud Run + Cloud SQL 開起來示範自動部署)。示範完立刻照章末清單拆除。
步驟 1:回顧全貌(更新後的架構)
我們要把這些零件串成一條線:
flowchart TB
dev["開發者"] -->|"git push main"| gh["GitHub"]
gh --> t
subgraph pipe["GitHub Actions:一條 gated pipeline"]
direction LR
t["① test<br/>go vet + go test"] --> b["② build-push<br/>image → GHCR"]
b --> d["③ deploy<br/>部署到 Cloud Run"]
end
b -->|"push image"| ghcr["GHCR"]
d -->|"gcloud run deploy"| run["Cloud Run"]
ghcr -->|"pull image"| run
run -->|"連線"| sql["Cloud SQL"]
run -->|"/metrics"| prom["Prometheus → Grafana"]
wif["Workload Identity<br/>(免金鑰信任)"] -.->|"認證"| d
tf["Terraform"] -.->|"佈建"| run
tf -.->|"佈建"| sql
tf -.->|"佈建"| wif
關鍵是「gated(有閘門)」:三個 job 用 needs 串成依賴——測試沒過,就不會 build;沒 build,就不會部署。壞 code 永遠到不了正式環境。
步驟 2:讓 GitHub Actions 能安全部署到 GCP(Workload Identity)
要讓 GitHub Actions 去 GCP 部署,它得「證明自己是誰」。最差的做法是產生一把服務帳號金鑰塞進 GitHub Secret——金鑰會外洩、難輪替。更好的是 Workload Identity Federation(WIF)。
WIF 是什麼?(白話) 讓 GCP 信任 GitHub 簽發的短期身分 token(OIDC),GitHub Actions 跑的時候拿這個 token 換取「臨時的 GCP 權限」——全程沒有長期金鑰。而且可限定「只有你這個 repo」能換。安全又免維護。
用 Terraform 建立這套信任。建立 code/terraform/cicd.tf:
# code/terraform/cicd.tf
variable "github_repo" {
description = "GitHub repo(owner/name),限制誰能部署"
type = string
# 例如 "yourname/no-logic-trade"
}
# 部署用的服務帳號(GitHub Actions 會「臨時假冒」它)
resource "google_service_account" "deployer" {
project = var.project_id
account_id = "nlt-deployer"
display_name = "no-logic-trade CI/CD deployer"
}
# 給它部署 Cloud Run 的權限
resource "google_project_iam_member" "deployer_run_admin" {
project = var.project_id
role = "roles/run.admin"
member = "serviceAccount:${google_service_account.deployer.email}"
}
# 允許 deployer 以「app 執行用 SA(第 6 章的 nlt-app)」身分部署
resource "google_service_account_iam_member" "deployer_act_as_app" {
service_account_id = google_service_account.app.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.deployer.email}"
}
# Workload Identity Pool:容納外部身分的「信任池」
resource "google_iam_workload_identity_pool" "github" {
project = var.project_id
workload_identity_pool_id = "github-pool"
display_name = "GitHub Actions pool"
}
# Provider:信任 GitHub 的 OIDC,且只接受你的 repo
resource "google_iam_workload_identity_pool_provider" "github" {
project = var.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
workload_identity_pool_provider_id = "github-provider"
display_name = "GitHub provider"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
}
# 只允許這個 repo 的 token(避免任何人都能換權限)
attribute_condition = "assertion.repository == '${var.github_repo}'"
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}
# 允許「來自該 repo 的 Actions」假冒 deployer SA
resource "google_service_account_iam_member" "wif_impersonate" {
service_account_id = google_service_account.deployer.name
role = "roles/iam.workloadIdentityUser"
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.github_repo}"
}
output "wif_provider" {
description = "填到 GitHub Actions 的 workload_identity_provider"
value = google_iam_workload_identity_pool_provider.github.name
}
output "deployer_sa_email" {
description = "填到 GitHub Actions 的部署 SA email"
value = google_service_account.deployer.email
}
逐段重點:
google_service_account.deployer:CI/CD 專用的部署身分,只給「部署 Cloud Run」需要的權限(最小權限原則)。deployer_act_as_app:部署 Cloud Run 服務時要「以 app 的執行 SA 身分」設定,需要serviceAccountUser。google_iam_workload_identity_pool+..._provider:建立對 GitHub OIDC 的信任,attribute_condition把信任限縮到你這個 repo(重要的安全護欄)。wif_impersonate:授權「來自該 repo 的 Actions」能臨時假冒 deployer。- 兩個
output:等下要填進 GitHub。
套用(會把第 6 章的 Cloud Run/Cloud SQL 與這裡的 WIF 一起建好):
cd code/terraform
# 在 terraform.tfvars 補上 github_repo = "yourname/no-logic-trade"
terraform apply
terraform output wif_provider # 複製這個值
terraform output deployer_sa_email # 複製這個值
💡 讓 Terraform 與 CD 不打架:CD 會用
gcloud run deploy更新 image,這會和 Terraform 記的 image 不一致(drift)。在第 6 章cloudrun.tf的google_cloud_run_v2_service.app裡加上lifecycle { ignore_changes = [template[0].containers[0].image] },讓 Terraform 不去管 image,交給 CD 更新。
把兩個 output 填到 GitHub repo 的 Settings → Secrets and variables → Actions → Variables(這些不是密碼、用 Variables 即可):
GCP_WIF_PROVIDER=terraform output wif_provider的值GCP_DEPLOYER_SA=terraform output deployer_sa_email的值GCP_PROJECT_ID= 你的專案 ID
步驟 3:把 CI/CD/Deploy 串成一條 gated pipeline
建立 code/.github/workflows/deploy.yml(這條取代先前 cd.yml 的角色;ci.yml 可留著做 PR 檢查):
# code/.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
GCP_REGION: asia-east1
SERVICE: nlt-api
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go vet ./...
- run: go test ./...
build-push:
needs: test # ★ 測試過才 build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: type=sha,format=long
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-push # ★ 推好 image 才部署
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # ★ 取得 OIDC token 給 WIF(少了會認證失敗)
steps:
- name: 用 Workload Identity 認證 GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: ${{ vars.GCP_DEPLOYER_SA }}
- name: 安裝 gcloud
uses: google-github-actions/setup-gcloud@v2
- name: 部署到 Cloud Run(更新成這次的 image)
run: |
gcloud run deploy ${{ env.SERVICE }} \
--image ${{ needs.build-push.outputs.image }} \
--region ${{ env.GCP_REGION }} \
--project ${{ vars.GCP_PROJECT_ID }}
逐段重點:
- 三個 job 用
needs串成閘門:build-push需要test、deploy需要build-push。任一前置失敗,後面不跑。 build-push的outputs.image:把這次 build 的 image 標籤(...:sha-xxx)往下傳給deploy。deploy的permissions: id-token: write:這是 WIF 的命脈——讓這個 job 能向 GitHub 索取 OIDC token。少了它,auth步驟會失敗。google-github-actions/auth@v2:用步驟 2 的GCP_WIF_PROVIDER/GCP_DEPLOYER_SA換到臨時 GCP 權限(全程無金鑰)。gcloud run deploy --image ...:sha-xxx:把 Cloud Run 服務更新成這次的 image——這就是「自動部署」。
✅ 如何驗證:一次 push 跑完整條線
確保 terraform apply(第 6 章 infra + 本章 WIF)已完成、GitHub Variables 已填。然後做一次有意義的 push:
cd code
# 隨便改個會反映在輸出的小地方,或就空提交觸發
git commit --allow-empty -m "deploy: 觸發完整 pipeline"
git push
到 GitHub Actions 分頁看 Deploy 這條 workflow:
- 你會看到 test → build-push → deploy 三個 job 依序亮綠。
deploy完成後,Cloud Run 會有一個新 revision。
確認線上真的更新了:
cd code/terraform
URL=$(terraform output -raw cloud_run_url)
curl $URL/health # {"status":"ok"}
gcloud run revisions list --service nlt-api --region asia-east1 # 看到剛部署的新 revision
- [ ] Actions 的 Deploy:test→build-push→deploy 三段依序綠勾。
- [ ] Cloud Run 出現新 revision。
- [ ]
curl $URL/health正常。 - [ ] (可選)故意弄壞測試 push,pipeline 在 test 就停住、不會部署——閘門生效。
步驟 4:兩個收尾概念(知道就好)
GitOps(ArgoCD / Flux)
我們的
deploy是「push 後主動gcloud run deploy」。另一種主流做法叫 GitOps:叢集裡有個工具(ArgoCD/Flux)持續盯著 Git repo,一旦你改了 manifest,它就自動把叢集調成跟 Git 一致。差別是「推(push)給叢集」vs「叢集自己拉(pull)」。K8s 環境很常用 GitOps。
Secrets 管理原則
整份教學為了好懂,把一些密碼放在 env / state。正式環境請守住:
- 永遠不要把密鑰 commit 進 Git(靠
.gitignore)。- CI 的密鑰放 GitHub Secrets;認證盡量用 WIF 免金鑰(如本章)。
- 雲端執行期密鑰用 Secret Manager(Cloud Run 可直接掛載),不要放在純 env。
💸 完整資源拆除清單(務必逐項做完)
DevOps 學完,最重要的一件事:把會花錢的東西全部關掉。逐項執行並複查。
A. 雲端 GCP(會計費,最優先)
cd code/terraform
# 若 GKE 還開著,先刪 K8s 裡的 LoadBalancer(釋放對外 IP/轉發規則)
kubectl delete namespace nlt 2>/dev/null || true
# 一次銷毀 Terraform 建的所有東西(Cloud Run / Cloud SQL / GKE / WIF / SA…)
terraform destroy
逐項複查沒有殘留(這些只要還在就可能計費):
PID=你的-project-id
gcloud run services list --region asia-east1 --project $PID
gcloud sql instances list --project $PID
gcloud container clusters list --project $PID
gcloud compute forwarding-rules list --project $PID
gcloud compute addresses list --project $PID # 殘留的靜態 IP 也算錢
gcloud compute disks list --project $PID # 殘留磁碟也算錢
# 以上理想都應是空的
B. 本機(免費,但清乾淨)
helm uninstall monitoring -n monitoring 2>/dev/null || true
kind delete cluster --name nlt
C. GHCR image(公開 image 免費,可留可刪)
到 repo → Packages → 不要的話可刪除。
D. 帳單與防護複查
- GCP Console → 帳單 → 報表:確認花費停止累積(可能延遲幾小時更新)。
- 確認第 1 章設的預算警示仍在。
E. 終極保險(最推薦)
如果你確定這個練習做完不再用,刪掉整個 GCP 專案——這是「保證一切停止計費」最乾脆的做法:
gcloud projects delete 你的-project-id
刪除專案會在 30 天後永久移除其中所有資源,期間不再計費。確定不用就刪,最安心。
🧯 常見錯誤 / 踩雷點
| 症狀 | 原因 | 解法 |
|---|---|---|
deploy 的 auth 步驟失敗(unable to get token) |
job 少了 permissions: id-token: write |
補上該權限(WIF 必需) |
auth 成功但部署 permission denied |
deployer SA 權限不足、或 act-as app SA 沒給 | 確認 roles/run.admin 與對 app SA 的 serviceAccountUser |
WIF failed to generate token / 條件不符 |
attribute_condition 的 repo 名不對、Variables 填錯 |
確認 github_repo 與實際 owner/repo 一致;Variables 三個值正確 |
每次 terraform apply 都想把 image 改回舊的 |
Terraform 與 CD 都在管 image(drift) | 在 Cloud Run 資源加 lifecycle { ignore_changes = [template[0].containers[0].image] } |
| test 失敗卻還是部署了 | 沒用 needs 串閘門 |
確認 build-push.needs: test、deploy.needs: build-push |
| 帳單一直沒歸零 | 有殘留資源(靜態 IP、磁碟、Cloud SQL) | 跑 A 段所有 list 複查並手動刪;或直接刪整個專案(E 段) |
🎓 DevOps 全系列完成!
從一個容器化的 Go 後端,你一路長出了完整的交付與運維能力:
| 章 | 你建立的能力 |
|---|---|
| 1 | 看懂 DevOps 全貌與目標 |
| 2 | 優化容器 image(distroless、瘦身) |
| 3 | CI:push 自動測試與 build(GitHub Actions) |
| 4 | CD:自動把 image 推上 GHCR |
| 5 | IaC:用 Terraform 宣告式管理雲端 |
| 6 | 部署上雲:Cloud Run + Cloud SQL(free tier 思維) |
| 7 | 容器編排:本機 kind 跑 K8s |
| 8 | Managed K8s:GKE 上雲 |
| 9 | 可觀測性:Prometheus + Grafana |
| 10 | 串成 gated pipeline + WIF 免金鑰部署 + 安全拆除 |
核心心法:一切自動化、一切宣告式、一切可重現、一切能銷毀。改一行 code、git push,剩下交給管線。
想再進階?
- GitOps:ArgoCD / Flux 做 pull 式部署。
- 多環境:dev / staging / prod 用 Terraform workspace 或目錄分離。
- 進階監控:加 Loki(日誌)、Tempo(追蹤)、Alertmanager(警報)、SLO。
- 安全:容器映像掃描(Trivy)、Secret 掃描、最小權限 IAM、Secret Manager。
- 更穩的部署:藍綠 / 金絲雀(canary)部署、自動 rollback。
恭喜你從「DevOps 新手」走到「能把一個後端從 code 一路自動化到上線並監控」!🚀