標籤:Azure、Container Apps、Go、React、DevOps、部署驗證
前言
你有沒有遇過這種狀況:
- 修了一個 bug,
git push,手動觸發部署。 - 等了三分鐘,部署顯示「成功」。
- 開瀏覽器一看…… bug 還在。
- 狐疑地
docker logs看一下,發現 container 跑的根本是上個版本的 image。
這就是我們踩過的坑。本文記錄我們如何系統性地解決這個問題——在 build 時把 git commit SHA 嵌入程式,透過 HTTP endpoint 暴露出來,讓部署後的驗證變成一行 curl 指令。
問題根源:三個隱形的快取陷阱
陷阱 1:Go binary 沒有重新編譯
我們的 Dockerfile 長這樣:
# backend/Dockerfile
FROM alpine:3.19
COPY bin/backend /app/backend
ENTRYPOINT ["/app/backend"]
Dockerfile 直接把 預先編譯好的 binary 複製進 image。
如果 go build 沒有在 docker build 之前執行,那麼:
修改 Go 原始碼 → docker build → 推送 image → 部署
↑ bin/backend 是舊的!
image 裡永遠是舊的 binary,source code 的修改完全無效。
陷阱 2:React dist/ 沒有重新 build
前端的 Dockerfile 同理:
# frontend/Dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
如果 npm run build 沒有在 docker build 之前執行,dist/ 是舊的,React 改動不生效。
陷阱 3:Docker layer cache
即使你真的重新執行了 go build 和 npm run build,Docker build cache 也可能讓你以為 image 更新了,實際上卻是沿用舊 layer。
解法:嵌入 Git Commit SHA
概念
build 時 ──► git rev-parse --short HEAD ──► 嵌入 binary
└─► /api/version 回傳
│
部署後 ─────────────────────────────────────────────► curl /api/version
└─► 比對 commit SHA ✅ 或 ⚠️
只要 /api/version 回傳的 commit hash 和 git rev-parse --short HEAD 一致,就能 100% 確認 Azure 上跑的是最新程式碼。
實作步驟
Step 1:在 Go 主程式宣告版本變數
cmd/main.go:
package main
// 這些變數在 go build 時由 -ldflags 注入
// 預設值為 "dev",方便本地開發
var (
AppName = "my-backend"
Version = "no-version"
BuildTime = "no-build-time"
CommitHash = "no-commit-hash"
)
為什麼用
var不用const?-ldflags "-X package.VarName=value"只能注入var,無法注入const。
Step 2:註冊 /api/version endpoint
在 cmd/main.go 的路由設定區塊加入:
import (
"encoding/json"
"net/http"
"time"
)
// 放在其他路由旁邊,不需要 auth middleware
mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"app": AppName,
"version": Version,
"commit": CommitHash,
"built_at": BuildTime,
})
})
本地測試:
go run ./cmd/main.go &
curl http://localhost:8080/api/version
# {"app":"my-backend","built_at":"no-build-time","commit":"no-commit-hash","version":"no-version"}
Step 3:在 deploy 腳本加入 go build -ldflags
deploy 腳本(PowerShell 範例)裡,Build-Backend 函式改成:
function Build-Backend {
# 1. 取得目前 commit SHA(若不在 git repo 則 fallback)
$GIT_SHA = git rev-parse --short HEAD 2>$null
if (-not $GIT_SHA) { $GIT_SHA = "unknown" }
$BUILD_TIME = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"
Write-Host " [Backend] 編譯 Go binary (commit=$GIT_SHA)..."
# 2. 交叉編譯 for Linux/amd64(Azure Container Apps 執行環境)
$env:GOOS = "linux"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "0"
Push-Location $BACKEND_DIR
go build `
-ldflags "-X main.CommitHash=$GIT_SHA -X main.BuildTime=$BUILD_TIME -X main.Version=$IMAGE_TAG" `
-o "bin\backend" `
"./cmd/main.go"
Pop-Location
Remove-Item Env:\GOOS, Env:\GOARCH, Env:\CGO_ENABLED -ErrorAction SilentlyContinue
# 3. 接著才 docker build(此時 bin/backend 已是最新)
Write-Host " [Backend] 建立 Docker Image ($IMAGE_TAG)..."
docker build -t "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG" $BACKEND_DIR
docker push "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"
}
關鍵順序:go build → docker build → docker push → ACA update,缺一不可。
Step 4:部署後自動驗證版本
同樣在 deploy 腳本,更新 ACA 之後加入:
# 更新 Azure Container Apps
az containerapp update `
--name $BACKEND_APP_NAME `
--resource-group $RESOURCE_GROUP `
--image "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"
# 等候新 revision 啟動
Write-Host " ⏳ 等待後端啟動後驗證版本..."
Start-Sleep -Seconds 30
# 驗證
try {
$deployed = (Invoke-RestMethod "https://your-app.example.com/api/version").commit
if ($deployed -eq $GIT_SHA) {
Write-Host " ✅ 版本驗證通過: commit=$deployed" -ForegroundColor Green
} else {
Write-Host " ⚠️ 版本不符: deployed=$deployed, local=$GIT_SHA" -ForegroundColor Yellow
Write-Host " 可能原因:ACA revision 還沒切換完成,稍後再手動確認" -ForegroundColor Yellow
}
} catch {
Write-Host " ⚠️ 無法連線驗證 /api/version: $_" -ForegroundColor Yellow
}
Step 5:在前端 footer 顯示 commit SHA(選用)
前端也可以在 build 時嵌入 SHA,讓使用者(或開發者)能目視確認前端版本。
deploy.ps1 Build-Frontend 函式:
function Build-Frontend {
$GIT_SHA = git rev-parse --short HEAD 2>$null
if (-not $GIT_SHA) { $GIT_SHA = "unknown" }
# 寫入 .env.production(只在 build 時使用,不會 commit 進 git)
Set-Content -Path "$FRONTEND_DIR\.env.production" -Value @"
VITE_API_BASE_URL=https://your-app.example.com
VITE_APP_GIT_SHA=$GIT_SHA
VITE_APP_BUILD_TIME=$(Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
"@
Push-Location $FRONTEND_DIR
npm run build
Pop-Location
docker build -t "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG" $FRONTEND_DIR
docker push "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"
}
vite-env.d.ts(型別宣告):
interface ImportMeta {
readonly env: ImportMetaEnv
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_APP_GIT_SHA?: string
readonly VITE_APP_BUILD_TIME?: string
}
React footer 元件:
const gitSha = import.meta.env.VITE_APP_GIT_SHA
// 在 footer 顯示
{gitSha && (
<Text size="xs" c="dimmed">
v{gitSha}
</Text>
)}
驗證流程總結
部署完成後,只需一行:
curl https://your-app.example.com/api/version
回傳範例:
{
"app": "my-backend",
"version": "dev-20250519153000",
"commit": "a1b2c3d",
"built_at": "2025-05-19T15:30:00Z"
}
再對照本地:
git rev-parse --short HEAD
# a1b2c3d ← 一致 ✅
常見問題
Q:commit=unknown 是什麼意思?
A:git rev-parse --short HEAD 找不到 git repo(例如 CI/CD 環境沒有 clone,或是在 zip 解壓的目錄執行),fallback 值為 "unknown"。
解法:確保 deploy 腳本執行時,工作目錄是 git repository 根目錄,或在 CI pipeline 裡先執行 git fetch --unshallow。
Q:-ldflags 可以注入任何值嗎?
A:只能注入 string 型別的套件層級 var。格式為 -X 完整套件路徑.變數名稱=值。
例如 main.go 裡的 var CommitHash → -X main.CommitHash=abc1234。
如果變數在子套件(如 internal/version),則為 -X github.com/your-org/your-app/internal/version.GitCommit=abc1234。
Q:Docker layer cache 怎麼辦?
A:每次 go build/npm run build 都會改變 bin/backend 或 dist/ 的內容,讓 COPY 指令的 layer hash 改變,Docker 就不會沿用舊 cache。所以只要確保每次 docker build 前都有重新 build,cache 問題自然消除。
Q:前後端版本不一致怎麼辦?
A:目前前後端各自有獨立的 SHA,若同時部署則 SHA 相同;若分開部署則可能不同。可以考慮:
- 永遠同時部署前後端(最簡單)
- 在
/api/version加入frontend_commit欄位(由前端在打 API 時傳入)
小結
| 問題 | 解法 |
|---|---|
| 不知道 Azure 上跑的是哪個版本 | /api/version endpoint 回傳 commit SHA |
| Go binary 沒有重新編譯 | go build 在 docker build 之前執行 |
React dist/ 沒有重新 build | npm run build 在 docker build 之前執行 |
| 部署後不知道有沒有成功 | 腳本自動 curl /api/version 比對 SHA |
| 前端版本無從確認 | VITE_APP_GIT_SHA 嵌入 build,footer 顯示 |
整套機制只需要三個地方的修改:Go 主程式加幾行、deploy 腳本加 go build -ldflags 和驗證邏輯、前端加一個環境變數。成本很低,但讓部署的可信度大幅提升。
附錄:完整 deploy.ps1 骨架
param(
[switch]$UpdateOnly,
[string]$Target = "All" # Backend | Frontend | All
)
# ── 設定區 ────────────────────────────────────────────────────────────
$RESOURCE_GROUP = "your-resource-group"
$BACKEND_APP_NAME = "your-backend-container-app"
$FRONTEND_APP_NAME = "your-frontend-container-app"
$ACR_LOGIN_SERVER = "your-acr.azurecr.io"
$PUBLIC_URL = "https://your-app.example.com"
$BACKEND_DIR = ".\your-backend"
$FRONTEND_DIR = ".\your-frontend"
$IMAGE_TAG = "dev-$(Get-Date -Format 'yyyyMMddHHmmss')"
$GIT_SHA = git rev-parse --short HEAD 2>$null; if (-not $GIT_SHA) { $GIT_SHA = "unknown" }
# ── 函式區 ────────────────────────────────────────────────────────────
function Build-Backend {
Write-Host "--- 重建並部署 Backend ---"
Write-Host " [Backend] 編譯 Go binary (commit=$GIT_SHA)..."
$env:GOOS = "linux"; $env:GOARCH = "amd64"; $env:CGO_ENABLED = "0"
Push-Location $BACKEND_DIR
go build -ldflags "-X main.CommitHash=$GIT_SHA -X main.Version=$IMAGE_TAG" `
-o "bin\backend" "./cmd/main.go"
Pop-Location
Remove-Item Env:\GOOS, Env:\GOARCH, Env:\CGO_ENABLED -ErrorAction SilentlyContinue
Write-Host " [Backend] 建立 Docker Image..."
docker build -t "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG" $BACKEND_DIR
docker push "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"
Write-Host " [Backend] 更新 Container App..."
az containerapp update --name $BACKEND_APP_NAME `
--resource-group $RESOURCE_GROUP `
--image "$ACR_LOGIN_SERVER/backend:$IMAGE_TAG"
# 驗證
Start-Sleep -Seconds 30
try {
$deployed = (Invoke-RestMethod "$PUBLIC_URL/api/version").commit
if ($deployed -eq $GIT_SHA) {
Write-Host " ✅ 版本驗證通過: commit=$deployed" -ForegroundColor Green
} else {
Write-Host " ⚠️ 版本不符: deployed=$deployed, local=$GIT_SHA" -ForegroundColor Yellow
}
} catch {
Write-Host " ⚠️ 無法連線驗證: $_" -ForegroundColor Yellow
}
}
function Build-Frontend {
Write-Host "--- 重建並部署 Frontend ---"
Set-Content "$FRONTEND_DIR\.env.production" @"
VITE_API_BASE_URL=$PUBLIC_URL
VITE_APP_GIT_SHA=$GIT_SHA
"@
Push-Location $FRONTEND_DIR
npm run build
Pop-Location
docker build -t "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG" $FRONTEND_DIR
docker push "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"
az containerapp update --name $FRONTEND_APP_NAME `
--resource-group $RESOURCE_GROUP `
--image "$ACR_LOGIN_SERVER/frontend:$IMAGE_TAG"
Write-Host " ✅ Frontend 部署完成" -ForegroundColor Green
}
# ── 執行 ─────────────────────────────────────────────────────────────
az acr login --name ($ACR_LOGIN_SERVER -replace "\.azurecr\.io","")
if ($Target -in @("Backend","All")) { Build-Backend }
if ($Target -in @("Frontend","All")) { Build-Frontend }