Azure Container Apps CI/CD 部署與權限配置

Demo 資料說明

這是一份可以單獨閱讀的教學用範例。所有專案名稱、公司名稱、GitHub repository、Azure 資源名稱、網域、帳號與 tenant 名稱都是虛構資料,不能直接拿來連線或部署。請依照實際環境替換成自己的設定。

這份文件要解決什麼問題

Example Company 的開發團隊希望使用 GitHub Actions 做 Azure CI/CD,流程如下:

  1. GitHub Actions 將程式編譯成 container image。
  2. 將 image push 到 Azure Container Registry(ACR)。
  3. 更新 Azure Container App,讓 App 使用新的 image。
  4. Backend 啟動後,透過 Microsoft Entra Managed Identity 連線到 Azure SQL Database。

目前遇到的問題通常會長這樣:

  • GitHub Actions 可以登入 Azure,但 Container App 啟動時無法從 ACR 拉 image。
  • Container App 的部署指令顯示成功,但新的 revision 變成 ActivationFailed
  • Backend image 可以啟動,卻因為沒有 SQL contained user 或 database role 而無法登入資料庫。
  • App 位於沒有 VNet 路徑的 Container Apps Environment,因此連不到 SQL Private Endpoint。
  • Frontend 還在使用舊的 Harbor image 或舊的 Backend hostname。
  • 舊的部署系統與 Azure GitHub Actions 同時執行,造成兩邊互相覆蓋部署結果。

這些現象看起來都像「權限問題」,但實際上分屬四個不同層次:

  1. GitHub Actions 的 CI 身分能不能登入 Azure、push image、更新 App。
  2. Container App 執行時的 Managed Identity 能不能從 ACR pull image。
  3. Backend 的 Managed Identity 能不能登入 SQL Database。
  4. Container Apps Environment 是否有通往 SQL Private Endpoint 的網路路徑。

Demo 情境與資源

以下名稱全部是示意資料:

  • 公司:Example Company
  • GitHub organization/repository:example-company/demo-backend
  • Subscription:Demo-Subscription
  • App/ACR Resource Group:rg-demo-stg-001
  • 共用 Container Apps Environment:cae-demo-stg-001
  • 共用 CAE 所在 Resource Group:rg-cae-demo-stg-001
  • ACR:acrdemostg001.azurecr.io
  • SQL Server:sql-demo-stg-001.database.windows.net
  • SQL Database:demoproject-dev
  • SQL Private Endpoint:pe-sql-demo-stg-001
  • VNet:vnet-demo-stg-001

預期使用的兩個 Container App 是:

App用途Environment
ca-demo-backend-dev-001Backend APIcae-demo-stg-001
ca-demo-frontend-dev-001Frontendcae-demo-stg-001

另外,情境中有一組不建議繼續使用的舊 App:

App舊 Environment舊 image registry問題
ca-example-backend-dev-001cae-example-dev-001harbor.example.invalid不在共用 CAE,也沒有正確接上 Azure ACR
舊 Frontend Appcae-example-dev-001舊 Harborimage 或 Nginx 設定可能仍指向舊 Backend

正確的目標架構

完整的部署與連線路徑應該是:

GitHub Actions
    │ OIDC,取得短期 token
    ▼
GitHub CI Service Principal
    │ AcrPush + Container App update
    ▼
Azure Container Registry
    │ image push
    ▼
Container App(共用 cae-demo-stg-001)
    │ App 自己的 system-assigned Managed Identity
    ├─ ACR AcrPull
    ├─ Key Vault secret get/list(如果 App 使用 Key Vault)
    └─ Azure SQL Entra token
          │
          ▼
    VNet + Private DNS + SQL Private Endpoint
          │
          ▼
    demoproject-dev

最重要的觀念是:

GitHub Actions 的 Service Principal 負責部署,不是執行中的 App。
真正需要 pull image 和連 SQL 的,是各個 Container App 自己的 Managed Identity。

CI/CD 的正確流程

1. GitHub Actions 使用 OIDC 登入 Azure

GitHub Actions 不應把長期的 Azure client secret 放在 repository。建議使用 OIDC:

- name: Login to Azure
  uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

示範用的 Federated Credential subject 是:

repo:example-company/demo-backend:ref:refs/heads/main

如果 workflow 使用 GitHub Environment,例如 staging,subject 和 variables 的 scope 都要跟 GitHub Environment 的設定一致,不能只改其中一邊。

2. CI 身分將 image push 到 ACR

示範用的 image repository 是:

acrdemostg001.azurecr.io/demo-backend:<github.sha>

GitHub CI Service Principal 需要 AcrPush,但不需要 runtime AcrPull

AcrPush = GitHub Actions 將 image 推進 ACR
AcrPull = Container App 啟動時從 ACR 拉出 image

demo-backend 在第一次成功 docker push 前可能還不會出現在 ACR repository list 中。ACR 通常會在第一次 push 時建立 repository,這不是權限錯誤。

3. CI 身分更新正確的 Container App

部署 Backend 時,target 應該是位於共用 CAE 的 App:

az containerapp update \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --image acrdemostg001.azurecr.io/demo-backend:${GITHUB_SHA}

這個指令成功只代表 Azure 接受了 App 設定更新,不代表新的 revision 一定能啟動。接下來還要檢查 runtime identity 是否能 pull image。

4. Container App 使用自己的 Managed Identity pull image

Backend 和 Frontend 都要各自完成以下設定:

  • 啟用 system-assigned Managed Identity。
  • 將該 App 的 identity 在 ACR scope 授予 AcrPull
  • 將 Container App registry authentication 設為 identity: system

可用以下指令設定 registry identity:

az containerapp registry set \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --server acrdemostg001.azurecr.io \
  --identity system

Frontend App 也要使用相同方式設定,只是 --name 改為 ca-demo-frontend-dev-001

檢查 App identity、Environment、registry 與 image:

az containerapp show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "{identity:identity,environment:properties.managedEnvironmentId,registries:properties.configuration.registries[].{server:server,identity:identity},images:properties.template.containers[].image}" \
  -o json

檢查 runtime identity 是否有 ACR AcrPull

az role assignment list \
  --assignee-object-id <BACKEND_APP_PRINCIPAL_ID> \
  --scope "$(az acr show \
    --resource-group rg-demo-stg-001 \
    --name acrdemostg001 \
    --query id -o tsv)" \
  --query "[].{role:roleDefinitionName,scope:scope}" \
  -o table

Frontend 使用自己的 <FRONTEND_APP_PRINCIPAL_ID> 查詢,不要共用 Backend 的 identity。

如果 App 沒有 AcrPull,常見結果是:

  • az containerapp update 指令本身成功。
  • revision 建立了,但狀態是 ActivationFailed
  • logs 或 revision events 出現 image pull、registry authentication 或 unauthorized 錯誤。

這時應該補在 Container App 的 Managed Identity,不能把 AcrPull 加到 GitHub OIDC Service Principal 代替。

Backend 存取 SQL Database

SQL 身分的分工

Backend 不應把 SQL username/password 寫進 GitHub Secrets 或 container environment。建議使用:

  1. Backend App 的 system-assigned Managed Identity 取得 Microsoft Entra token。
  2. SQL Entra administrator 在 demoproject-dev 建立對應的 contained user。
  3. 只授予 Backend 實際需要的 database roles。

示範用的 database URL 概念如下:

sqlserver://sql-demo-stg-001.database.windows.net:1433
  ?database=demoproject-dev
  &encrypt=true
  &trustservercertificate=false
  &fedauth=ActiveDirectoryManagedIdentity
  &useMsi=true

建立 contained user 與 database roles

以下 SQL 必須由 SQL Microsoft Entra administrator 或有足夠 database 權限的管理者,在 demoproject-dev database 執行:

CREATE USER [ca-demo-backend-dev-001] FROM EXTERNAL PROVIDER;

再依照應用程式需要授予角色:

ALTER ROLE [db_datareader]
ADD MEMBER [ca-demo-backend-dev-001];

ALTER ROLE [db_datawriter]
ADD MEMBER [ca-demo-backend-dev-001];

-- 只有需要由 App 執行 schema migration 時才授予
ALTER ROLE [db_ddladmin]
ADD MEMBER [ca-demo-backend-dev-001];

這些是 SQL Database 裡的 data-plane permissions,不是 Azure Resource Group RBAC。因此 az role assignment list 看不到 db_datareaderdb_datawriterdb_ddladmin membership。

可以在 SQL 中確認:

SELECT
    dp.name AS database_principal,
    rp.name AS database_role
FROM sys.database_role_members AS drm
JOIN sys.database_principals AS rp
    ON rp.principal_id = drm.role_principal_id
JOIN sys.database_principals AS dp
    ON dp.principal_id = drm.member_principal_id
WHERE dp.name = N'ca-demo-backend-dev-001';

Frontend 不需要 SQL 權限,也不應為 Frontend 建立 SQL contained user。

SQL 網路路徑

即使 SQL user/roles 都正確,網路不通時仍然無法連線。

Demo 情境中的網路設計是:

  • pe-sql-demo-stg-001 的狀態為 Approved。
  • Private DNS zone privatelink.database.windows.net 連結到 vnet-demo-stg-001
  • 共用 cae-demo-stg-001 有正確的 VNet/infrastructure subnet 路徑。
  • App 放在共用 CAE,才能透過 Private Endpoint 解析並連線到 SQL。

舊的 cae-example-dev-001 沒有這條 infrastructure subnet 路徑,因此不適合作為需要存取 Private SQL 的 target。

看到下面的錯誤時,優先檢查 CAE、VNet、Private DNS 和 Private Endpoint:

Client with IP address ... is not allowed to access the server

不要為了繞過問題,直接把 SQL firewall 開成 0.0.0.0。這會把網路問題變成不必要的公開暴露。

看到下面的錯誤時,優先檢查 Managed Identity、contained user 和 database roles:

Login failed

開發者帳號能做什麼

示範開發者帳號是 [email protected],在 demo tenant 中的 UPN 示意如下:

demo_user_example.com#EXT#@exampletenant.onmicrosoft.com

示範直接授予的 Azure roles:

ScopeRole可以做什麼
rg-demo-stg-001Contributor在 App RG 建立、更新、刪除 Container App
rg-demo-stg-001Reader讀取 RG 資源
acrdemostg001AcrPushPush image 到 ACR
cae-demo-stg-001Reader讀取共用 CAE 設定
cae-demo-stg-001Container Apps Environment Joiner建立 App 時加入共用 CAE

因此,這個帳號可以:

  • 建立或更新 App。
  • 將 App 放進共用 cae-demo-stg-001
  • Push image 到 ACR。

但這個帳號不能:

  • 建立 Azure RBAC role assignment。
  • 替新的 App 授予 ACR AcrPull
  • 修改 SQL Private Endpoint、VNet 或 SQL firewall。
  • demoproject-dev 建立 Entra contained user。
  • 授予或修改 SQL database roles。

這是刻意的權限邊界:開發者可以部署應用程式,但不能自行擴大 App 的 Azure 權限或資料庫權限。需要新增 AcrPull 或 SQL user/roles 時,應請平台管理者或 SQL Entra administrator 處理。

舊 App 如何切換到共用 CAE

Azure Container Apps 沒有提供用 az containerapp update 直接切換 Environment 的參數。既有 App 不能像更新 image 一樣,直接從:

cae-example-dev-001

搬到:

cae-demo-stg-001

較安全的切換方式是:

  1. 在共用 CAE 建立新的 Container App,或使用已準備好的 ca-demo-backend-dev-001 / ca-demo-frontend-dev-001
  2. 逐項複製必要的 environment variables、secrets、ingress、domain、scale 和 traffic 設定。
  3. 啟用新 App 的 system-assigned Managed Identity。
  4. 由平台管理者授予新 identity ACR AcrPull
  5. 由 SQL Entra administrator 替新的 Backend identity 建立 SQL user/roles。
  6. 將 CI/CD 的 target App name 改成新的 App。
  7. 驗證 revision、logs、API、Frontend URL、ACR pull 與 SQL 連線。
  8. 新 App 穩定後,才考慮移除舊 App。

不要先刪除舊 App 再重建,因為可能遺失 secrets、domain、ingress、traffic 和原本的部署設定。

Frontend 的特別注意事項

Frontend 只需要:

  • 從 ACR pull image。
  • 連到正確的 Backend URL。
  • 必要時讀取自己的 Key Vault secrets。

Frontend 不需要 demoproject-dev 的 SQL roles。

如果 Frontend revision 是 ActivationFailed,但 AcrPull 已經正確,請檢查:

  • image 內的 Nginx upstream 設定。
  • BACKEND_URL 或其他 Backend endpoint environment variable。
  • image 是否仍然包含舊的 backend.legacy.example.invalid hostname。
  • 新 image 是否真的已 push 到 ACR,且 App 使用了正確 tag。

這類問題通常是 image/configuration 問題,不是 SQL 權限問題。

建議的執行順序

平台管理者與開發團隊可以依照以下順序處理:

  1. 確認 GitHub OIDC Federated Credential 的 repository、branch 和 Environment subject 正確。
  2. 確認 CI Service Principal 只有部署所需的 ContributorAcrPush 等角色。
  3. 確認 Backend 和 Frontend 都位於 cae-demo-stg-001
  4. 啟用兩個 App 的 system-assigned identity。
  5. 對兩個 App identity 各自授予 ACR AcrPull
  6. 執行 az containerapp registry set --identity system
  7. 由 SQL Entra administrator 建立 Backend contained user 和必要 roles。
  8. 確認 CAE、VNet、Private DNS、Private Endpoint 的網路路徑。
  9. 只啟用一條正式部署路徑,避免舊 pipeline 和 Azure CI/CD 同時更新同一個 App。
  10. 做一次受控部署,逐項檢查 image、revision、logs、API、Frontend 和 SQL。

部署後驗證指令

確認 ACR image tag

az acr repository show-tags \
  --name acrdemostg001 \
  --repository demo-backend \
  --orderby time_desc \
  --top 5 \
  -o table

確認 Backend revision

az containerapp revision list \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "[].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" \
  -o table

正常情況應看到 revision 為 Healthy,且 running state 正常。若 revision 建立成功但沒有 running,請查 revision events 和 container logs。

查看 Backend logs

az containerapp logs show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --type console \
  --tail 100

最後確認的成功條件

  • GitHub Actions OIDC login 成功。
  • image 成功 push 到 acrdemostg001.azurecr.io
  • Backend 和 Frontend revision 能從 ACR pull image。
  • Backend revision 為 Healthy / Running
  • Backend 能以 Managed Identity 取得 SQL token。
  • SQL contained user 與必要 roles 存在。
  • Backend 能連線到 demoproject-dev
  • Frontend 使用新的 Backend URL。
  • 舊的部署系統不會再覆蓋 Azure CI/CD 的結果。

發佈留言

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