第 6 章:雲平台部署 — 把容器部署到 Cloud Run(free tier 優先)
這一章在做什麼? 我們把第 4 章推到 GHCR 的 image,真正部署到 Cloud Run(GCP 的 serverless 容器服務),並用 Terraform 一併佈建一個最小的 Cloud SQL(雲端 PostgreSQL),讓 app 連上它。做完這章,你的後端會有一個公開網址,任何人都能 curl 到——它真的上線了。
為什麼選 Cloud Run? 因為它對新手最友善、最省錢:你只給它一顆 image,它就幫你跑起來、自動配網址與 HTTPS、沒流量時自動縮到零(不計費)、免費額度也大。你完全不用管伺服器。
前置:第 5 章的 Terraform 基礎(API、服務帳號)已 apply、image 已在 GHCR 且設為公開。
💸 本章開始會產生費用! Cloud Run 在免費額度內+縮到零,幾乎是 $0;但 Cloud SQL 會按小時計費(最小規格約每月個位數美元),即使沒人用也照算。所以本章結尾有明確的
terraform destroy——學完就刪掉。
步驟 1:先搞懂 Cloud Run
Cloud Run
GCP 的「serverless 容器」服務。你給它一顆容器 image,它負責跑、自動擴縮、配 HTTPS 網址。有請求才啟動、沒請求縮到零,按實際用量計費。
🔁 前端類比:很像把後端容器丟上 Vercel/Netlify 的感覺——push 一個產物,它給你一個會自動擴縮的網址。差別是 Cloud Run 跑的是你的容器,什麼語言都行。
它和「資料庫」的分工:Cloud Run 是無狀態的(縮到零、隨時被重建),所以資料庫不能跑在它裡面,要用獨立的 Cloud SQL(代管的 PostgreSQL)。app 透過 GCP 的 Cloud SQL 連接器(一條安全通道)連到它。
步驟 2:補上要部署的 image 與 random provider
- 在
code/terraform/variables.tf追加一個變數:
# code/terraform/variables.tf(追加)
variable "image" {
description = "要部署的容器 image(GHCR,需為公開)"
type = string
}
- 在
code/terraform/terraform.tfvars填入你的 image(把<你的帳號>換掉):
# code/terraform/terraform.tfvars(追加)
image = "ghcr.io/<你的帳號>/no-logic-trade:latest"
- 在
code/terraform/versions.tf的required_providers裡加上random(我們會用它產生資料庫密碼):
# code/terraform/versions.tf(在 required_providers 內追加)
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
步驟 3:Terraform — 佈建 Cloud SQL(PostgreSQL)
建立 code/terraform/database.tf:
# code/terraform/database.tf
# 給 Cloud SQL 實例名稱加一段隨機後綴:
# 因為「刪除後的實例名,GCP 會保留約一週不讓你重用」,加後綴可避免重建撞名。
resource "random_id" "suffix" {
byte_length = 2
}
# 隨機產生資料庫密碼(存在 state 裡,不寫進版控)
resource "random_password" "db" {
length = 24
special = false
}
resource "google_sql_database_instance" "pg" {
name = "nlt-pg-${random_id.suffix.hex}"
database_version = "POSTGRES_16"
region = var.region
deletion_protection = false # 關鍵:讓 terraform destroy 能刪掉它
settings {
tier = "db-f1-micro" # 最小、最便宜的共享核心規格
ip_configuration {
ipv4_enabled = true # 給公開 IP(仍透過連接器安全連線)
}
}
}
resource "google_sql_database" "db" {
name = "nolologic"
instance = google_sql_database_instance.pg.name
}
resource "google_sql_user" "user" {
name = "nolologic"
instance = google_sql_database_instance.pg.name
password = random_password.db.result
}
逐段解釋:
random_id.suffix:產生隨機後綴,避免 Cloud SQL 實例撞名(GCP 對「剛刪掉的名字」會保留約一週,是新手很常踩的雷)。random_password.db:隨機產生 24 字密碼,存在 state(已 gitignore)。google_sql_database_instance.pg:Cloud SQL 實例本體。database_version = "POSTGRES_16"、tier = "db-f1-micro"(最便宜)。deletion_protection = false:很重要——預設是true,會讓terraform destroy失敗。設 false 才能順利刪除。ipv4_enabled = true:給它公開 IP(我們仍用連接器連,不直接暴露密碼)。
google_sql_database.db:在實例裡建立名為nolologic的資料庫。google_sql_user.user:建立帳號nolologic,密碼用上面隨機產生的。
步驟 4:Terraform — 佈建 Cloud Run 服務
建立 code/terraform/cloudrun.tf:
# code/terraform/cloudrun.tf
resource "random_password" "jwt" {
length = 48
special = false
}
# 讓 app 的服務帳號(第 5 章建的 nlt-app)能連 Cloud SQL
resource "google_project_iam_member" "sql_client" {
project = var.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.app.email}"
}
resource "google_cloud_run_v2_service" "app" {
name = "nlt-api"
location = var.region
deletion_protection = false
template {
service_account = google_service_account.app.email
scaling {
min_instance_count = 0 # 沒流量縮到零 → 不計費
max_instance_count = 2
}
containers {
image = var.image
ports {
container_port = 8080
}
# 環境變數:沿用你 app 既有的設定鍵(DB_HOST 等)
env {
name = "APP_PORT"
value = "8080"
}
env {
name = "DB_HOST"
# Cloud SQL 連接器會把 socket 掛在 /cloudsql/<連線名稱>
value = "/cloudsql/${google_sql_database_instance.pg.connection_name}"
}
env {
name = "DB_PORT"
value = "5432"
}
env {
name = "DB_NAME"
value = google_sql_database.db.name
}
env {
name = "DB_USER"
value = google_sql_user.user.name
}
env {
name = "DB_SSLMODE"
value = "disable"
}
env {
name = "DB_PASSWORD"
value = random_password.db.result
}
env {
name = "JWT_SECRET"
value = random_password.jwt.result
}
# 把 Cloud SQL 連接器掛進來
volume_mounts {
name = "cloudsql"
mount_path = "/cloudsql"
}
resources {
limits = {
cpu = "1"
memory = "512Mi"
}
}
}
volumes {
name = "cloudsql"
cloud_sql_instance {
instances = [google_sql_database_instance.pg.connection_name]
}
}
}
depends_on = [google_project_iam_member.sql_client]
}
# 允許「任何人」呼叫這個服務(這樣 curl 才打得到)
resource "google_cloud_run_v2_service_iam_member" "public" {
project = var.project_id
location = var.region
name = google_cloud_run_v2_service.app.name
role = "roles/run.invoker"
member = "allUsers"
}
逐段解釋:
random_password.jwt:替正式環境產一把夠長的 JWT 密鑰(取代開發用的dev-secret)。google_project_iam_member.sql_client:把roles/cloudsql.client授予 app 的服務帳號——少了它,app 連不上 Cloud SQL。google_cloud_run_v2_service.app:Cloud Run 服務本體。service_account:以第 5 章的nlt-app身分執行。scaling { min_instance_count = 0 }:縮到零,沒流量時 $0。containers.image = var.image:跑你 GHCR 上的 image。- 一堆
env { name value }:直接對應你 app 既有的設定鍵(第 1 份教學的config.go讀這些),所以 app 一行程式都不用改。關鍵是DB_HOST = /cloudsql/<連線名稱>——你的lib/pq會把開頭是/的 host 當成 unix socket 連線,正好接上連接器。 volume_mounts+volumes.cloud_sql_instance:把 Cloud SQL 連接器掛在/cloudsql。depends_on:確保先有 IAM 權限,app 啟動才連得上 DB。
google_cloud_run_v2_service_iam_member.public:把roles/run.invoker給allUsers,服務才公開可呼叫(否則curl會 403)。
💡 關於密碼放在
env:為了讓教學好懂,我們把 DB 密碼/JWT 直接放進 env(它們在 state 裡、已 gitignore)。正式環境應改用 Secret Manager(值不落在 env/state)——第 10 章會提到 Secrets 管理原則。本章的 DB 反正用完即刪,先求跑通。
app 會自己建表嗎? 會。你的 Go app 啟動時會自動跑 migration(第 1 份教學的 migrator),所以 Cloud Run 一啟動、連上 Cloud SQL 就把 5 張表建好,不需要額外步驟。
步驟 5:apply,取得公開網址
cd code/terraform
terraform init # 重新 init 以下載 random provider
terraform apply # 輸入 yes 確認
- Cloud SQL 建立要等幾分鐘(這是正常的,雲端在替你開一台資料庫)。
- 完成後,輸出會有 Cloud Run 網址。也可單獨取出:
在 code/terraform/outputs.tf 追加:
# code/terraform/outputs.tf(追加)
output "cloud_run_url" {
description = "Cloud Run 服務網址"
value = google_cloud_run_v2_service.app.uri
}
terraform output -raw cloud_run_url
# 例如 https://nlt-api-xxxxx-de.a.run.app
✅ 如何驗證這一章成功
把網址存起來,打打看:
URL=$(terraform output -raw cloud_run_url)
# 1) 健康檢查
curl $URL/health
# 預期:{"status":"ok"}
第一次請求可能慢個幾秒(冷啟動 + 跑 migration),屬正常。
跑一輪完整流程(全部打到雲端):
# 2) 註冊
curl -s -X POST $URL/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"cloud@example.com","password":"secret123"}'
# 3) 登入拿 token
TOKEN=$(curl -s -X POST $URL/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"cloud@example.com","password":"secret123"}' | sed 's/.*"token":"//;s/".*//')
# 4) 入金
curl -s -X POST $URL/api/account/deposit \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"amount":5000}'
# 5) 下單
curl -s -X POST $URL/api/orders \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"symbol":"AAPL","side":"buy","quantity":3}'
- [ ]
curl $URL/health回{"status":"ok"}。 - [ ] 註冊→登入→入金→下單都正常回應(代表 app + Cloud SQL 連線都通)。
查看雲端日誌(debug 用):
gcloud run services logs read nlt-api --region asia-east1 --limit 50
💸 用完即刪(非常重要)
學完就把會計費的東西刪掉。Cloud SQL 只要還在就一直算錢。
cd code/terraform
terraform destroy # 輸入 yes
destroy 會移除 Cloud Run、Cloud SQL、IAM 綁定等(Cloud SQL 刪除要等幾分鐘)。完成後確認真的沒了:
gcloud sql instances list --project 你的-project-id # 預期:空的 / Listed 0 items
gcloud run services list --region asia-east1 # 預期:看不到 nlt-api
最後到 GCP Console → 帳單 → 報表,確認花費停止累積(可能隔一點時間更新)。
下次要再做時:重新
terraform apply即可(因為 Cloud SQL 名稱有隨機後綴,不會撞到剛刪的名字)。 想保留 app 但只刪 DB? 進階用法可用terraform destroy -target=...,但新手建議整份 destroy 最單純、最不會漏掉計費資源。
🧯 常見錯誤 / 踩雷點
| 症狀 | 原因 | 解法 |
|---|---|---|
curl 回 403 / 需要登入 |
沒給 allUsers invoker,或被組織政策「網域限制共用」擋住 |
確認有 google_cloud_run_v2_service_iam_member.public;個人帳號通常可行,公司帳號可能被政策擋(改用 gcloud run services proxy 本機測試) |
| 服務一直啟動失敗 / Revision 失敗 | app 連不上 DB、或沒監聽 8080 | gcloud run services logs read;確認 SA 有 cloudsql.client、DB_HOST 是 /cloudsql/<連線名稱>、container_port 為 8080 |
| 拉不到 image(image pull 失敗) | GHCR package 還是 private | 回第 4 章把 package 改 Public |
terraform destroy 卡住刪不掉 Cloud SQL |
deletion_protection = true |
設為 false 後再 apply 一次、再 destroy |
| 重建時報「instance name 不可用」 | 重用了剛刪除的 Cloud SQL 名稱(GCP 保留約一週) | 已用 random_id 後綴避免;若手動命名請換名 |
| app 連 DB 逾時 | Cloud Run 與 Cloud SQL 不同 region、或連接器沒掛 | 兩者同 var.region;確認 volumes.cloud_sql_instance 與 volume_mounts 都有 |
| 帳單持續增加 | 忘了 destroy,Cloud SQL 整天在跑 | 立刻 terraform destroy;並確認第 1 章的預算警示有效 |
下一步 → 第 7 章:Kubernetes 入門 — 先在本機用 kind 跑起來 Cloud Run 幫我們把「單一服務」跑起來很省事,但當你要管理「很多服務、要自我修復、要更細的控制」時,就需要容器編排。我們會在本機用 kind 建一個 Kubernetes 叢集,理解 Pod / Service / Deployment,把 app 和一個 in-cluster 的 Postgres 部署上去——而且完全免費、跑在你自己電腦。