如何確認 Azure Container Apps 上跑的是最新程式碼?

標籤:Azure、Container Apps、Go、React、DevOps、部署驗證


前言

你有沒有遇過這種狀況:

  1. 修了一個 bug,git push,手動觸發部署。
  2. 等了三分鐘,部署顯示「成功」。
  3. 開瀏覽器一看…… bug 還在
  4. 狐疑地 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 buildnpm 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 builddocker builddocker 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/backenddist/ 的內容,讓 COPY 指令的 layer hash 改變,Docker 就不會沿用舊 cache。所以只要確保每次 docker build 前都有重新 build,cache 問題自然消除。

Q:前後端版本不一致怎麼辦?

A:目前前後端各自有獨立的 SHA,若同時部署則 SHA 相同;若分開部署則可能不同。可以考慮:

  1. 永遠同時部署前後端(最簡單)
  2. /api/version 加入 frontend_commit 欄位(由前端在打 API 時傳入)

小結

問題解法
不知道 Azure 上跑的是哪個版本/api/version endpoint 回傳 commit SHA
Go binary 沒有重新編譯go builddocker build 之前執行
React dist/ 沒有重新 buildnpm run builddocker 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 }

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *