GitHub Actions OIDC CI/CD 交接指南

本文件說明未來新的開發團隊如何在沒有 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 給開發團隊。

  1. 先看整體流程
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。

  1. 為什麼使用 OIDC

傳統做法是把 AZURE_CLIENT_SECRET 存在 GitHub Actions Secret。這會產生長期憑證管理問題:

  • secret 可能被誤印到 log。
  • secret 可能忘記輪替。
  • 離職或團隊更換時,需要追查 secret 被放在哪裡。

OIDC 的做法是:

  1. GitHub Actions 向 GitHub OIDC provider 取得短期 token。
  2. Azure 驗證 token 的 issuer、subject 與 audience。
  3. 只有符合 Federated Credential 條件的 workflow 才能換取 Azure access token。
  4. Azure RBAC 再決定這個 Service Principal 可以操作哪些資源。

因此 OIDC 不等於「自動擁有 Azure 權限」:

  • Federated Credential:決定「哪個 GitHub workflow 可以登入」。
  • Azure RBAC:決定「登入後可以做什麼」。
  1. 本次已建立的 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。

  1. 新團隊的責任分工

4.1 平台/環境管理者做一次

平台管理者需要:

  1. 建立該團隊專用的 App Registration 與 Service Principal。
  2. 建立只允許指定 repo、branch 或 GitHub Environment 的 Federated Credentials。
  3. 在正確的 ACR scope 授予 AcrPush。
  4. 在指定 Container App scope 授予更新權限。
  5. 在共用 CAE 授予 Reader 與 Container Apps Environment Joiner。
  6. 確認 Container App 的 Managed Identity 已具備 ACR、Key Vault 與 SQL 權限。
  7. 確認 App 位於正確的共用 CAE,而不是沒有 VNet/private DNS 的獨立 CAE。
  8. 將 Client ID、Tenant ID、Subscription ID 提供給團隊設定 GitHub Variables。

4.2 開發團隊負責

開發團隊只需要:

  1. 維護 GitHub Actions workflow。
  2. 在 main 或受保護的 staging Environment 執行 deploy。
  3. Build Docker image。
  4. Push image 到 ACR。
  5. 更新既有 Container App 的 image。
  6. 查看 revision 與 logs。

開發團隊不應該:

  • 建立或刪除 Service Principal。
  • 修改 SQL firewall、Private Endpoint 或 Private DNS。
  • 將 App secret 寫進 GitHub workflow。
  • 執行 SQL CREATE USER。
  • 修改共用 CAE 的 VNet 設定。
  1. 未來新增團隊的標準開通流程

以下流程由平台管理者執行。每個團隊建議有自己的 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 控制。

  1. 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。
  1. 目前既有 Workflow 的注意事項

目前 demoproject-backend/.github/workflows/dev.yaml 與 demoproject-frontend/.github/workflows/dev.yaml 的 Dev 流程原本是:

  1. Build/test。
  2. Push 到 harbor.sdc.nycu.club。
  3. 呼叫 n8n deployment webhook。

建立 Azure OIDC identity 不會自動改變這些 workflow。若要切換到 Azure Container Apps:

  1. 由團隊決定是否停止 Harbor+n8n deployment。
  2. 將 Azure login、ACR push、Container App update 加入新的 deploy job。
  3. 確認新 workflow 的 subject 與 Federated Credential 完全一致。
  4. 先在 staging Environment 做一次受審核測試。
  5. 確認 revision、logs、API version 與 Frontend URL 後,再移除舊流程。

不要讓 Harbor+n8n 與 Azure CI 同時部署同一個功能環境,否則最後一個完成的 pipeline 可能覆蓋前一個結果。

  1. 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。

  1. 驗證清單

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

應確認:

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
  1. 常見錯誤
  • 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
  1. 團隊撤換與緊急撤權

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"

撤換整個團隊時,應同時:

  1. 移除 GitHub Environment/repository variables。
  2. 刪除該 repo 的 Federated Credentials。
  3. 移除 App、ACR、CAE scope 的 role assignments。
  4. 確認 Container App runtime Managed Identity 不被誤刪。
  5. 保留 audit log 與變更紀錄。
  6. 新團隊開通檢查表

平台管理者

  • 確認目標 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 與功能。
  1. 相關文件
  • Staging 新手指南: STAGING-ONBOARDING-ZH-TW.md
  • Dev 部署腳本: deploy.dev.ps1
  • Backend 權限說明: demoproject-backend\docs\dev-deployment-permissions.md

發佈留言

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