
本文件說明未來新的開發團隊如何在沒有 Azure Owner role 的情況下,使用 GitHub Actions 將 Backend 或 Frontend 部署到專案 Staging。
文件目標是讓接手環境管理工作的同仁能快速回答以下問題:
- 為什麼不需要把 Azure client secret 放進 GitHub?
- 哪些工作必須由平台管理者做一次?
- 開發團隊需要哪些最小 Azure 權限?
- GitHub OIDC、Service Principal、Federated Credential、ACR 與 Container Apps 之間如何串接?
- 為什麼 PR 不可以直接取得可寫入 Staging 的 Azure token?
- SQL 權限應該給誰?
適用環境
本文件適用目前的專案 Staging:
- Subscription: DEMO-PoC
- App/ACR Resource Group: rg-demo-stg-jpe-001
- 共用 Container Apps Environment: cae-stg-jpe-001
- ACR: acrdemostgjpe001.azurecr.io
- SQL Server: sql-stg-jpe-001.database.windows.net
- SQL Database: demoproject-dev
重要
目前的 CI 身分由環境管理者建立,開發團隊只負責維護 GitHub Actions。不要要求開發團隊取得 Owner,也不要把 SQL Resource Group 的 Contributor 給開發團隊。
- 先看整體流程
GitHub push to main
|
| GitHub 發出短期 OIDC token
v
GitHub Actions
|
| azure/login@v2
v
Microsoft Entra App / Service Principal
|
| Federated Credential 比對 repo + branch/environment
| Azure RBAC 比對允許的 scope
v
Azure Container Registry (AcrPush)
|
| push Docker image
v
Azure Container App
|
| 使用 App 自己的 Managed Identity
v
SQL Private Endpoint -> demoproject-dev
這裡有兩個不同的身分,聲明如下:
- GitHub Actions Service Principal:用途為 Build、push Image、更新 Container App。是否應有 SQL data access:否。
- Backend Container App Managed Identity:用途為執行中的 Backend 連接 SQL、ACR、Key Vault。是否應有 SQL data access:是,僅限該 App。
GitHub Actions 的 OIDC Service Principal 只負責部署。它不應該被加入 demoproject-dev 的 SQL roles,也不應該讀取應用程式 secrets。
- 為什麼使用 OIDC
傳統做法是把 AZURE_CLIENT_SECRET 存在 GitHub Actions Secret。這會產生長期憑證管理問題:
- secret 可能被誤印到 log。
- secret 可能忘記輪替。
- 離職或團隊更換時,需要追查 secret 被放在哪裡。
OIDC 的做法是:
- GitHub Actions 向 GitHub OIDC provider 取得短期 token。
- Azure 驗證 token 的 issuer、subject 與 audience。
- 只有符合 Federated Credential 條件的 workflow 才能換取 Azure access token。
- Azure RBAC 再決定這個 Service Principal 可以操作哪些資源。
因此 OIDC 不等於「自動擁有 Azure 權限」:
- Federated Credential:決定「哪個 GitHub workflow 可以登入」。
- Azure RBAC:決定「登入後可以做什麼」。
- 本次已建立的 CI 身分
本次已建立以下 CI Service Principal:
Display name: demoproject-staging-ci
Client ID: 188a7d0a-30d5-4a0d-9b04-3e118d3d46e7
Object ID: 33ec7c42-e5b5-443c-aae5-e97e1e056d10
Tenant ID: 7ef65350-5b77-4958-aca5-0ccadb6bd0b7
沒有建立 client secret。
3.1 目前已授予的角色
- Scope: rg-demo-stg-jpe-001 | Role: Contributor | 目的: 目前 CI 更新 App/ACR 的過渡權限
- Scope: acrdemostgjpe001 | Role: AcrPush | 目的: 允許 Docker push 到 ACR
- Scope: cae-stg-jpe-001 | Role: Reader | 目的: 讀取共用 CAE 設定
- Scope: cae-stg-jpe-001 | Role: Container Apps Environment Joiner | 目的: 允許使用共用 CAE;只有 Microsoft.App/managedEnvironments/join/action
目前沒有授予:
- rg-cae-stg-jpe-001 的 Contributor。
- rg-data-stg-jpe-001 的任何角色。
- demoproject-dev 的 SQL role。
- Key Vault data-plane secrets 讀取權限。
rg-demo-stg-jpe-001 的 Contributor 是為了讓現有流程先能運作的過渡方案。未來新增團隊時,建議改成各 App resource scope 的 Contributor 或專用 custom role,不要複製成每個團隊都能管理整個 RG。
3.2 目前已信任的 GitHub repositories
目前只建立以下兩個 main branch Federated Credentials:
repo:NYCUITSC/demoproject-backend:ref:refs/heads/main
repo:NYCUITSC/demoproject-frontend:ref:refs/heads/main
刻意沒有建立:
- pull_request credential。
- NYCUITSC/demoproject-api credential。
demoproject-api 目前是 Frontend 用來產生 SDK 的 dependency,不是 Azure deployment repository。除非它未來真的擁有部署 job,否則不要給它 Azure write identity。
- 新團隊的責任分工
4.1 平台/環境管理者做一次
平台管理者需要:
- 建立該團隊專用的 App Registration 與 Service Principal。
- 建立只允許指定 repo、branch 或 GitHub Environment 的 Federated Credentials。
- 在正確的 ACR scope 授予 AcrPush。
- 在指定 Container App scope 授予更新權限。
- 在共用 CAE 授予 Reader 與 Container Apps Environment Joiner。
- 確認 Container App 的 Managed Identity 已具備 ACR、Key Vault 與 SQL 權限。
- 確認 App 位於正確的共用 CAE,而不是沒有 VNet/private DNS 的獨立 CAE。
- 將 Client ID、Tenant ID、Subscription ID 提供給團隊設定 GitHub Variables。
4.2 開發團隊負責
開發團隊只需要:
- 維護 GitHub Actions workflow。
- 在 main 或受保護的 staging Environment 執行 deploy。
- Build Docker image。
- Push image 到 ACR。
- 更新既有 Container App 的 image。
- 查看 revision 與 logs。
開發團隊不應該:
- 建立或刪除 Service Principal。
- 修改 SQL firewall、Private Endpoint 或 Private DNS。
- 將 App secret 寫進 GitHub workflow。
- 執行 SQL CREATE USER。
- 修改共用 CAE 的 VNet 設定。
- 未來新增團隊的標準開通流程
以下流程由平台管理者執行。每個團隊建議有自己的 Service Principal,不要讓所有團隊共用一個可寫入 Staging 的 identity。
5.1 設定資源變數
以下是目前資源的範例。新增團隊時,App name 與 repo 名稱應換成新團隊實際值。
set -euo pipefail
SUB_ID=”56b72537-d985-4530-88f3-b6ed07e71c67″
APP_RG=”rg-demo-stg-jpe-001″
CAE_RG=”rg-cae-stg-jpe-001″
CAE_NAME=”cae-stg-jpe-001″
ACR_RG=”rg-demo-stg-jpe-001″
ACR_NAME=”acrdemostgjpe001″
每個團隊使用不同名稱,例如:
demoproject-sdc-team-2027-ci
SP_NAME=”demoproject-new-team-ci”
BACKEND_CA_NAME=”ca-demo-backend-dev-jpe-001″
FRONTEND_CA_NAME=”ca-demo-frontend-dev-jpe-001″
不要把 SQL Resource Group 放進 CI role scope:
rg-data-stg-jpe-001
5.2 建立 App Registration 與 Service Principal
執行者必須具備 Entra App 建立權限。建立 Azure role assignment 另外需要 Owner 或 User Access Administrator;新加入的開發團隊不需要具備這些管理權限。
APP_ID=”$(az ad app create
–display-name “$SP_NAME” \ –query appId -o tsv)” SP_OBJECT_ID=”$(az ad sp create
–id “$APP_ID”
–query id -o tsv)”
echo “clientId: $APP_ID”
echo “servicePrincipalObjectId: $SP_OBJECT_ID”
不要用以下方式反查 App ID:
az ad app list –display-name “$SP_NAME”
因為 display name 可能重複,會拿到錯誤的 App。
5.3 授予 CI 最小必要角色
先取得 resource IDs:
APP_SCOPE=”/subscriptions/${SUB_ID}/resourceGroups/${APP_RG}”
CAE_SCOPE=”$(az containerapp env show
–subscription “$SUB_ID”
–resource-group “$CAE_RG”
–name “$CAE_NAME” \ –query id -o tsv)” ACR_SCOPE=”$(az acr show
–subscription “$SUB_ID”
–resource-group “$ACR_RG”
–name “$ACR_NAME” \ –query id -o tsv)” BACKEND_SCOPE=”$(az containerapp show
–subscription “$SUB_ID”
–resource-group “$APP_RG”
–name “$BACKEND_CA_NAME” \ –query id -o tsv)” FRONTEND_SCOPE=”$(az containerapp show
–subscription “$SUB_ID”
–resource-group “$APP_RG”
–name “$FRONTEND_CA_NAME”
–query id -o tsv)”
長期建議只給既有 App scope 的 Contributor:
az role assignment create
–subscription “$SUB_ID”
–assignee-object-id “$SP_OBJECT_ID”
–assignee-principal-type ServicePrincipal
–role “Contributor”
–scope “$BACKEND_SCOPE”
az role assignment create
–subscription “$SUB_ID”
–assignee-object-id “$SP_OBJECT_ID”
–assignee-principal-type ServicePrincipal
–role “Contributor”
–scope “$FRONTEND_SCOPE”
CI push ACR 需要另外的 data-plane role:
az role assignment create
–subscription “$SUB_ID”
–assignee-object-id “$SP_OBJECT_ID”
–assignee-principal-type ServicePrincipal
–role “AcrPush”
–scope “$ACR_SCOPE”
使用共用 CAE 需要:
az role assignment create
–subscription “$SUB_ID”
–assignee-object-id “$SP_OBJECT_ID”
–assignee-principal-type ServicePrincipal
–role “Reader”
–scope “$CAE_SCOPE”
az role assignment create
–subscription “$SUB_ID”
–assignee-object-id “$SP_OBJECT_ID”
–assignee-principal-type ServicePrincipal
–role “Container Apps Environment Joiner”
–scope “$CAE_SCOPE”
若團隊需要由 CI 建立全新的 Container App,而不是更新既有 App,先由平台管理者評估是否真的需要 RG Contributor。可以先由管理者建立 App,再把後續 CI 限制為既有 App scope。
5.4 建立 Federated Credential
方案 A:只允許 main branch
適合簡單的單一 staging deploy workflow:
REPO=”NEW_ORG/NEW_REPO”
SAFE_NAME=”${REPO//\//-}-main” SUBJECT=”repo:${REPO}:ref:refs/heads/main”
PAYLOAD=”$(jq -n
–arg name “$SAFE_NAME”
–arg subject “$SUBJECT”
‘{name: $name, issuer: “https://token.actions.githubusercontent.com
“, subject: $subject, audiences: [“api://AzureADTokenExchange”] }’)”
az ad app federated-credential create
–id “$APP_ID”
–parameters “$PAYLOAD”
方案 B:使用受保護的 GitHub Environment(推薦新團隊)
在 GitHub repository 建立 staging Environment,設定:
- Required reviewers。
- 只允許 main branch 或指定 tag。
- 將 Azure identifiers 放在 Environment variables。
Federated Credential 的 subject 改為:
repo:NEW_ORG/NEW_REPO:environment:staging
Workflow 的 deploy job 必須設定:
environment: staging
Branch subject 與 Environment subject 不要混用。Azure 會比對完整的 sub claim,字串不完全相同就會登入失敗。
絕對不要把 write credential 給 pull_request
以下 subject 不可綁定到具有 Contributor、AcrPush 或 App write 的 identity:
repo:NEW_ORG/NEW_REPO:pull_request
PR 的程式碼尚未審核,任何可以修改 workflow 的 PR 都可能嘗試呼叫 Azure CLI。PR workflow 應只執行 lint/test/build;如果真的需要查詢 Azure,另建只有 Reader 的 CI identity。
5.5 提供 GitHub Variables
提供給團隊的只有識別資訊:
AZURE_CLIENT_ID =
AZURE_TENANT_ID = 7ef65350-5b77-4958-aca5-0ccadb6bd0b7
AZURE_SUBSCRIPTION_ID = 56b72537-d985-4530-88f3-b6ed07e71c67
不需要也不應該提供:
AZURE_CLIENT_SECRET
Client ID、Tenant ID、Subscription ID 本身不是登入密碼;真正的信任條件由 Azure Federated Credential 與 GitHub workflow permission 控制。
- GitHub Actions Workflow 範本
以下範例使用「受保護 staging Environment」方案。若使用目前既有的 main branch credential,請移除 environment: staging,或先建立相符的 Environment Federated Credential。
name: Deploy Backend to Azure Staging
on:
push:
branches:
– main
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: staging
steps:
– name: Checkout
uses: actions/checkout@v4
– name: Login to Azure with OIDC
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} – name: Login to ACR run: az acr login –name acrdemostgjpe001 – name: Build and push image env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}
run: |
docker build -t “$IMAGE” .
docker push “$IMAGE” – name: Update existing Container App env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}
run: |
az containerapp update
–resource-group rg-demo-stg-jpe-001
–name ca-demo-backend-dev-jpe-001
–image “$IMAGE”
Frontend 只需要替換 Image repository 與 Container App name。
6.1 不要在 CI 直接執行完整 deploy.dev.ps1
deploy.dev.ps1 適合管理者或本機部署流程,不適合直接放入 GitHub Actions,原因包括:
- 會要求 deploy.secrets.ps1。
- 會將應用程式 secrets 套用到 Container App。
- 會包含 Managed Identity、Key Vault policy、SQL 授權等 bootstrap 工作。
- 可能嘗試建立或修改基礎資源。
CI 的正常流程應是:
OIDC login -> docker build -> ACR push -> containerapp update –image
既有的環境變數與 secrets 由平台管理者預先設定;App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。
6.2 PR Workflow
PR workflow 不應取得 Azure write token:
on:
pull_request:
branches:
– main
permissions:
contents: read
PR 可執行 lint、unit test、build 或不涉及 Staging 的 container build,但不要:
- id-token: write。
- azure/login。
- Push Staging ACR。
- Update 或 delete Container App。
- 目前既有 Workflow 的注意事項
目前 demoproject-backend/.github/workflows/dev.yaml 與 demoproject-frontend/.github/workflows/dev.yaml 的 Dev 流程原本是:
- Build/test。
- Push 到 harbor.sdc.nycu.club。
- 呼叫 n8n deployment webhook。
建立 Azure OIDC identity 不會自動改變這些 workflow。若要切換到 Azure Container Apps:
- 由團隊決定是否停止 Harbor+n8n deployment。
- 將 Azure login、ACR push、Container App update 加入新的 deploy job。
- 確認新 workflow 的 subject 與 Federated Credential 完全一致。
- 先在 staging Environment 做一次受審核測試。
- 確認 revision、logs、API version 與 Frontend URL 後,再移除舊流程。
不要讓 Harbor+n8n 與 Azure CI 同時部署同一個功能環境,否則最後一個完成的 pipeline 可能覆蓋前一個結果。
- SQL 與網路權限不要放到 CI
8.1 CI identity 不需要 SQL role
CI Service Principal 只更新 Container App image。SQL runtime access 由 Backend Container App 的 system-assigned Managed Identity 處理:
GitHub CI identity -> ACR push -> Container App update
Backend App Managed Identity -> Private Endpoint -> demoproject-dev
如果 App 被刪除後重新建立,system-assigned identity 可能改變。平台/DB 管理者需要重新確認:
- AcrPull。
- Key Vault secret get/list。
- demoproject-dev contained user。
- db_datareader、db_datawriter、必要時 db_ddladmin。
8.2 CI identity 不需要修改 SQL firewall
目前 SQL 使用 Private Endpoint 與 Private DNS。應用程式應部署到有正確 VNet 路徑的共用 cae-stg-jpe-001。
遇到以下錯誤時,不要直接把 GitHub Actions Service Principal 加到 SQL 或把 firewall 設成 0.0.0.0:
Client with IP address ‘…’ is not allowed to access the server
這通常代表 Container App 位於沒有 VNet/private DNS 的 CAE,或 hostname 解析走到公網。請先檢查 App 的 managedEnvironmentId。
- 驗證清單
9.1 管理者驗證 App 與 Federated Credentials
az ad app federated-credential list
--id "$APP_ID"
--query "[].{name:name,subject:subject,issuer:issuer,audiences:audiences}"
-o table
應確認:
- issuer 是 https://token.actions.githubusercontent.com。
- audience 是 api://AzureADTokenExchange。
- subject 只有指定 repo 與指定 main/environment。
- 沒有 write identity 的 pull_request subject。
9.2 驗證 Azure RBAC
az role assignment list
--assignee-object-id "$SP_OBJECT_ID"
--all
--query "[].{role:roleDefinitionName,scope:scope}"
-o table
不應出現:
- Contributor at rg-cae-stg-jpe-001。
- 任何 role at rg-data-stg-jpe-001。
- 不必要的 SQL 或 Key Vault data-plane role。
9.3 團隊驗證 workflow
部署成功後確認:
az acr repository show-tags
--name acrdemostgjpe001
--repository demo-dev-backend
--orderby time_desc
--top 5
-o table
az containerapp revision list
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--query "[].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}"
-o table
az containerapp logs show
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--type console
--tail 100
- 常見錯誤
- AADSTS70021: No matching federated identity record found | 原因:subject、branch/environment、issuer 或 audience 不一致 | 處理方式:對照 GitHub workflow 的 trigger 與 environment,列出 FIC 逐字比較
- AuthorizationFailed | 原因:CI identity 沒有對應 scope 的 role | 處理方式:由平台管理者補上 App scope、ACR 或 CAE join role
- denied: requested access to the resource is denied | 原因:ACR 沒有 AcrPush | 處理方式:在 ACR scope 加 AcrPush,不是只加 Contributor
- Container App cannot join environment | 原因:缺 Microsoft.App/managedEnvironments/join/action | 處理方式:在共用 CAE scope 加 Container Apps Environment Joiner
- SQL Client with IP … not allowed | 原因:App 走公網,沒有走 Private Endpoint | 處理方式:檢查 App 是否位於共用 CAE,不要先加大量 firewall IP
- SQL Login failed | 原因:App Managed Identity 未建立 contained user 或 SQL role | 處理方式:查 App identity,由 DB 管理者處理,不是修改 CI role
- Image push 後沒有新 revision | 原因:workflow push 到錯誤 ACR/tag,或沒有執行 az containerapp update | 處理方式:檢查 image URI、ACR repository、revision list 與 workflow log
- Workflow 使用 tag 觸發但 Azure login 失敗 | 原因:只有 main branch subject,沒有符合 tag/environment 的 FIC | 處理方式:改成只由 main deploy,或由管理者建立精準的 tag/environment FIC
- 團隊撤換與緊急撤權
OIDC 不使用長期 client secret,因此撤權主要是刪除 Federated Credential 或 Azure role assignment。
移除某個 repo 的信任
先列出 credential ID:
az ad app federated-credential list
--id "$APP_ID"
--query "[].{id:id,name:name,subject:subject}"
-o table
再刪除指定 credential:
az ad app federated-credential delete
--id "$APP_ID"
--federated-credential-id ""
移除 Azure role
az role assignment delete
--assignee-object-id "$SP_OBJECT_ID"
--role "AcrPush"
--scope "$ACR_SCOPE"
撤換整個團隊時,應同時:
- 移除 GitHub Environment/repository variables。
- 刪除該 repo 的 Federated Credentials。
- 移除 App、ACR、CAE scope 的 role assignments。
- 確認 Container App runtime Managed Identity 不被誤刪。
- 保留 audit log 與變更紀錄。
- 新團隊開通檢查表
平台管理者
- 確認目標 repo 與實際部署的 Backend/Frontend。
- 每個團隊建立獨立 Service Principal。
- 只建立 main 或受保護 Environment 的 FIC。
- 不建立 write identity 的 pull_request FIC。
- ACR scope 有 AcrPush。
- 既有 App scope 有更新權限。
- 共用 CAE scope 有 Reader + Container Apps Environment Joiner。
- 沒有授予 SQL Resource Group 或共用 CAE Resource Group Contributor。
- 確認 App runtime Managed Identity 的 SQL/Key Vault/ACR 權限。
- 將三個 Azure identifiers 提供給 GitHub Environment variables。
開發團隊
- Workflow 有 permissions: id-token: write。
- Deploy job 的 environment 或 branch subject 與 Azure FIC 完全一致。
- 使用 azure/login@v2,沒有 client secret。
- Docker image 使用不可變的 commit SHA tag。
- Push 到正確的 ACR repository。
- 只更新指定 Container App,不修改 CAE/VNet/SQL。
- PR workflow 沒有 Azure write token。
- 部署後確認 revision、logs 與功能。
- 相關文件
- Staging 新手指南: STAGING-ONBOARDING-ZH-TW.md
- Dev 部署腳本: deploy.dev.ps1
- Backend 權限說明: demoproject-backend\docs\dev-deployment-permissions.md
