
企業或專案引進「新開發團隊」接手部署時,常見的權限混亂、安全風險與網路連線問題。主要包含以下四大核心問題:
- 權限與安全問題:避免為了部署而過度授予高風險權限(例如 Azure Owner 或 SQL 權限),並改用 GitHub Actions OIDC 取代長期保存的明文金鑰( secrets )。
- 責任分工不明確:清楚劃分「平台管理者」與「開發團隊」的職責邊界,防止開發團隊誤動基礎架構(如 SQL Firewall、VNet )。
- 網路連線與架構問題:解決 Container App 因未走正確的 VNet 與 Private Endpoint 導致無法連線至 Azure SQL Database 的問題。
- 新手上手缺乏規範:提供明確的跨團隊開發指南、 CI/CD 自動化流程範本與開通檢查表,縮短團隊上手時間。
demoproject與ExampleOrg只是示範名稱。正式環境使用前,必須由 平台管理者替換所有<PLACEHOLDER>與範例資源名稱,並確認權限範圍。
1. 先看結論
新加入的開發團隊平常只需要:
- 取得 GitHub repository 與必要的 Azure RBAC 權限。
- 使用平台管理者準備好的共用 Container Apps Environment。
- 透過 GitHub Actions OIDC 登入 Azure,不使用長期 client secret。
- Build Docker image、push 到 ACR、更新既有 Container App。
- 透過 revision 與 logs 查看部署結果。
新加入的開發團隊不需要:
- Azure
Owner。 - SQL Resource Group 的
Contributor。 - 直接修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。
- 在 GitHub repository 中存放 SQL 密碼或應用程式 secrets。
- 每次部署都執行完整的基礎資源建立流程。
應用程式連接 SQL 的權限不是由 GitHub Actions Service Principal 提供, 而是由 Backend Container App 自己的 Managed Identity 在資料庫中取得。
2. Azure 服務白話說明
| Azure 服務或名詞 | 白話說明 | DemoProject 用途 |
|---|---|---|
| Subscription | Azure 帳單與資源的管理範圍 | <SUBSCRIPTION_NAME> |
| Resource Group(RG) | 把相關 Azure 資源放在一起的資料夾 | App、ACR、CAE、SQL 分開管理 |
| Azure Container Registry(ACR) | 存放 Docker image 的私有倉庫 | CI push image |
| Container Apps Environment(CAE) | Container Apps 共用的執行環境與網路邊界 | Backend、Frontend 共用 |
| Container App(CA) | 實際執行容器的 Azure 資源 | 執行 DemoProject Backend/Frontend |
| Virtual Network(VNet) | Azure 內部的私有網路 | 連接 CAE 與 SQL Private Endpoint |
| Private Endpoint(PE) | 讓 Azure 服務在 VNet 內取得私有 IP | 將 SQL 接到 VNet |
| Private DNS | 讓 hostname 在 VNet 內解析到私有 IP | 解析 SQL hostname |
| Azure SQL Server / Database | SQL Server 與其中的資料庫 | 存放 DemoProject 資料 |
| Managed Identity | Azure 資源自己的身分,不需要存密碼 | App 登入 SQL、ACR、Key Vault |
| Key Vault | 集中存放 secret、憑證與金鑰 | Backend runtime 讀取 secrets |
| Azure RBAC | 管理 Azure 資源的控制權 | 決定誰能部署或修改 App |
| SQL database role | 資料庫內的資料讀寫權限 | db_datareader、db_datawriter、db_ddladmin |
| GitHub OIDC | GitHub workflow 使用短期 token 登入 Azure | CI 不存放 client secret |
Azure RBAC 與 SQL 權限不是同一件事
- Azure
Contributor可以管理 Azure Resource Manager 資源,不等於可以 查詢 SQL table。 - SQL
db_datareader可以讀資料,不等於可以修改 Azure SQL Server。 - SQL
db_datawriter可以寫資料。 - SQL
db_ddladmin可以執行部分資料庫結構變更,應只授予需要執行 migration 的 Backend identity。 - SQL firewall 與 Private Endpoint 決定「網路能不能到 SQL」;資料庫 roles 只有在網路已連通後才會生效。
3. DemoProject 資源地圖
以下名稱全部是示範值:
| 用途 | Resource Group | 資源名稱 |
|---|---|---|
| Azure Container Registry | rg-demoproject-stg-app | acr-demoproject-stg.azurecr.io |
| Container Apps | rg-demoproject-stg-app | ca-demoproject-backend-stg、ca-demoproject-frontend-stg |
| 共用 Container Apps Environment | rg-demoproject-stg-cae | cae-demoproject-stg |
| SQL Server / Database | rg-demoproject-stg-data | sql-demoproject-stg.database.windows.net / demoproject-dev |
| SQL Private Endpoint | rg-demoproject-stg-network | pe-demoproject-sql-stg |
| Key Vault | rg-demoproject-stg-app | kv-demoproject-stg |
| VNet | rg-demoproject-stg-network | vnet-demoproject-stg |
3.1 建議的網路路徑
Backend Container App
|
| 共用 CAE 的 VNet 網路
v
vnet-demoproject-stg
|
| Private DNS 將 SQL hostname 解析到私有 IP
v
SQL Private Endpoint pe-demoproject-sql-stg
|
v
sql-demoproject-stg / demoproject-dev
如果 Container App 位於沒有 VNet/private DNS 的獨立 CAE,它可能會透過 公網出口 IP 連線 SQL,並收到:
Client with IP address '...' is not allowed to access the server
遇到這個錯誤時,先確認 App 所在 CAE 與 DNS 路徑,不要立刻把大量公網 IP 加進 SQL firewall。
4. 權限模型
4.1 開發者與 CI 的建議權限
| 範圍 | 建議角色/權限 | 用途 | SQL data access |
|---|---|---|---|
| 既有 Backend Container App | Contributor 或專用 custom role | 更新 image/revision | 否 |
| 既有 Frontend Container App | Contributor 或專用 custom role | 更新 image/revision | 否 |
| ACR | AcrPush | Docker image push | 否 |
| 共用 CAE | Reader | 讀取 CAE 設定 | 否 |
| 共用 CAE | Container Apps Environment Joiner | 使用指定 CAE | 否 |
| SQL Resource Group | 不授權 | SQL 集中由管理者控管 | 否 |
| Backend Managed Identity | SQL contained user + SQL roles | 應用程式 runtime 連線 SQL | 是 |
Container Apps Environment Joiner 應只包含:
Microsoft.App/managedEnvironments/join/action
不要為了讓團隊部署而授予共用 CAE Resource Group 的 Contributor。
4.2 平台管理者與開發團隊分工
平台管理者一次性完成:
- 建立 App Registration 與 Service Principal。
- 建立指定 repository、branch 或 GitHub Environment 的 Federated Credential。
- 在 ACR scope 授予
AcrPush。 - 在既有 Container App scope 授予更新權限。
- 在共用 CAE scope 授予
Reader與Container Apps Environment Joiner。 - 確認 App runtime Managed Identity 具備 ACR、Key Vault 與 SQL 權限。
- 確認 App 位於正確的 VNet/private DNS CAE。
開發團隊負責:
- 維護 GitHub Actions workflow。
- Build Docker image。
- Push image 到 ACR。
- 更新既有 Container App。
- 查看 revision 與 logs。
開發團隊不應該:
- 建立或刪除 Service Principal。
- 修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。
- 將 App secrets 寫入 repository 或 workflow。
- 執行 SQL
CREATE USER。 - 修改共用 CAE 的網路設定。
5. GitHub Actions OIDC
5.1 OIDC 如何運作
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
ACR push -> Container App update
OIDC 不等於自動擁有 Azure 權限:
- Federated Credential 決定「哪個 GitHub workflow 可以登入」。
- Azure RBAC 決定「登入後可以做什麼」。
5.2 新團隊專用 Service Principal
建議每個團隊、每個環境使用不同 Service Principal。不要讓所有團隊共用 一個可寫入 Staging 的 identity。
以下是示範值:
SP display name: demoproject-team-a-staging-ci
Client ID: <APP_CLIENT_ID>
Object ID: <SERVICE_PRINCIPAL_OBJECT_ID>
Tenant ID: <TENANT_ID>
Subscription ID: <SUBSCRIPTION_ID>
不需要建立 client secret。
5.3 角色配置範例
以下指令只展示格式,執行前請替換所有 placeholder:
set -euo pipefail
SUB_ID="<SUBSCRIPTION_ID>"
APP_RG="rg-demoproject-stg-app"
CAE_RG="rg-demoproject-stg-cae"
CAE_NAME="cae-demoproject-stg"
ACR_NAME="acr-demoproject-stg"
SP_NAME="demoproject-team-a-staging-ci"
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 "$APP_RG" \
--name "$ACR_NAME" \
--query id -o tsv)"
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)"
az role assignment create \
--subscription "$SUB_ID" \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type ServicePrincipal \
--role "AcrPush" \
--scope "$ACR_SCOPE"
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 只更新既有 App,建議把寫入權限限制在 App resource scope:
BACKEND_SCOPE="$(az containerapp show \
--subscription "$SUB_ID" \
--resource-group "$APP_RG" \
--name "ca-demoproject-backend-stg" \
--query id -o tsv)"
az role assignment create \
--subscription "$SUB_ID" \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type ServicePrincipal \
--role "Contributor" \
--scope "$BACKEND_SCOPE"
不要把 CI identity 授予:
rg-demoproject-stg-data
rg-demoproject-stg-cae 的 Contributor
demoproject-dev 的 SQL roles
5.4 Federated Credential
方案 A:只允許 main branch
適合單一 staging deploy workflow:
REPO="ExampleOrg/demoproject-backend"
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 Environment staging,設定:
- Required reviewers。
- 只允許
mainbranch 或指定 tag。 - 將 Azure identifiers 放在 Environment variables。
Federated Credential 的 subject 改為:
repo:ExampleOrg/demoproject-backend:environment:staging
Workflow 的 deploy job 必須包含:
environment: staging
Branch subject 與 Environment subject 不要混用。Azure 會比對完整的 sub claim,字串不完全相同就會登入失敗。
PR 不可取得 write token
以下 subject 不可綁定到具有 Contributor、AcrPush 或 App write 的 identity:
repo:ExampleOrg/demoproject-backend:pull_request
PR 程式碼尚未審核,可能修改 workflow 並呼叫 Azure CLI。PR workflow 應只 執行 lint、test、build;若真的需要查詢 Azure,另建只有 Reader 的 identity。
6. GitHub Actions Workflow 範本
以下範例使用受保護的 staging Environment。若使用 main branch subject, 請移除 environment: staging,或先建立相符的 Environment Federated Credential。
name: Deploy DemoProject Backend
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 acr-demoproject-stg
- name: Build and push image
env:
IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
run: |
docker build -t "$IMAGE" .
docker push "$IMAGE"
- name: Update existing Container App
env:
IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
run: |
az containerapp update \
--resource-group rg-demoproject-stg-app \
--name ca-demoproject-backend-stg \
--image "$IMAGE"
Frontend 只需要替換 image repository 與 Container App name。
6.1 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。
6.2 不要在 CI 執行完整部署腳本
完整部署腳本通常會包含 secrets、Key Vault policy、Managed Identity、 SQL 授權與首次建立資源的流程,不適合直接放進 GitHub Actions。
CI 建議只做:
OIDC login
-> docker build
-> ACR push
-> containerapp update --image
既有的環境變數與 secrets 由平台管理者預先設定;App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。
7. SQL Database 與 Managed Identity
7.1 App runtime 才需要 SQL role
查詢 Backend App 的 Managed Identity:
az containerapp show \
--subscription "<SUBSCRIPTION_ID>" \
--resource-group "rg-demoproject-stg-app" \
--name "ca-demoproject-backend-stg" \
--query "{type:identity.type,principalId:identity.principalId}" \
-o json
如果 Container App 被刪除後重新建立,system-assigned identity 可能改變。 管理者需要重新確認 ACR、Key Vault 與 SQL 權限。
7.2 建立 SQL contained user
以下 SQL 應由 SQL Entra administrator 或等效資料庫管理員,在 demoproject-dev 執行。將 App name 替換成實際值:
IF DATABASE_PRINCIPAL_ID(N'ca-demoproject-backend-stg') IS NULL
BEGIN
CREATE USER [ca-demoproject-backend-stg] FROM EXTERNAL PROVIDER;
END;
ALTER ROLE [db_datareader]
ADD MEMBER [ca-demoproject-backend-stg];
ALTER ROLE [db_datawriter]
ADD MEMBER [ca-demoproject-backend-stg];
-- 只有 Backend migration 確實需要時才授予
ALTER ROLE [db_ddladmin]
ADD MEMBER [ca-demoproject-backend-stg];
CI Service Principal 不需要執行這段 SQL,也不應被加入這三個 database roles。
7.3 人員需要直接查詢資料庫
不要把 Azure Contributor 當成 SQL data access。若確實需要讓人員直接 查詢:
- 管理者建立 Microsoft Entra security group。
- 將需要查詢的人員加入該群組。
- 在
demoproject-dev建立該群組的 contained user。 - 優先只授予
db_datareader。 - 確認使用者有 VNet、VPN 或其他 Private Endpoint 網路路徑。
8. Private Endpoint 與 SQL Firewall
8.1 為什麼不能只新增一個 IP?
獨立的 Container Apps Environment 可能使用公網出口 IP,而且出口 IP 可能有很多個或因環境變更而改變。錯誤訊息中的 IP 只代表當次連線, 不一定是永久 IP。
使用共用 CAE 時,應該走:
Container App
-> VNet
-> Private DNS
-> SQL Private Endpoint
-> SQL Database
不要用大量公網 IP firewall 規則取代正確的網路拓撲,也不要未經核准就 建立:
start-ip-address = 0.0.0.0
end-ip-address = 0.0.0.0
這代表允許大範圍 Azure services 存取,不是只允許本團隊。
8.2 確認 App 使用正確 CAE
$expected = "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-demoproject-stg-cae/providers/Microsoft.App/managedEnvironments/cae-demoproject-stg"
az containerapp show `
--name ca-demoproject-backend-stg `
--resource-group rg-demoproject-stg-app `
--query properties.managedEnvironmentId `
-o tsv
$expected
兩個輸出必須相同。如果 App 位於沒有 VNet/private DNS 的 CAE,先請平台 管理者確認網路路徑,不要先修改 SQL firewall。
8.3 常見錯誤
| 錯誤 | 常見原因 | 正確處理 |
|---|---|---|
Client with IP address ... is not allowed | App 走公網,SQL firewall 沒放行 | 確認 App 是否位於共用 VNet CAE |
Login failed | App identity 未建立或沒有 SQL role | 查 App principal ID,由 DB 管理者處理 |
Cannot resolve host | Private DNS 或 VNet 路徑錯誤 | 檢查 PE、DNS zone link、CAE subnet |
AuthorizationFailed | CI identity 缺少對應 scope 的 role | 由平台管理者補上 App、ACR 或 CAE role |
9. 部署後驗證
9.1 查看 App 所在 CAE 與 revision
az containerapp show `
--name ca-demoproject-backend-stg `
--resource-group rg-demoproject-stg-app `
--query "{environment:properties.managedEnvironmentId,latest:properties.latestRevisionName,ready:properties.latestReadyRevisionName}" `
-o table
az containerapp revision list `
--name ca-demoproject-backend-stg `
--resource-group rg-demoproject-stg-app `
--query "[].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" `
-o table
9.2 查看 logs
az containerapp logs show `
--name ca-demoproject-backend-stg `
--resource-group rg-demoproject-stg-app `
--type console `
--tail 100
9.3 驗證 CI 身分
az ad app federated-credential list \
--id "$APP_ID" \
--query "[].{name:name,subject:subject,issuer:issuer,audiences:audiences}" \
-o table
az role assignment list \
--assignee-object-id "$SP_OBJECT_ID" \
--all \
--query "[].{role:roleDefinitionName,scope:scope}" \
-o table
應確認:
- issuer 是
https://token.actions.githubusercontent.com。 - audience 是
api://AzureADTokenExchange。 - subject 只有指定 repo 與指定 main/environment。
- 沒有 write identity 的
pull_requestsubject。 - 沒有 SQL Resource Group 或共用 CAE Resource Group 的
Contributor。
10. 首次 bootstrap 與日常部署
10.1 首次 bootstrap
以下工作由平台管理者完成一次:
- 建立 ACR、CAE、Container App 或確認資源已存在。
- 建立 App Registration、Service Principal 與 Federated Credentials。
- 授予 CI 的 ACR/App/CAE 角色。
- 啟用 App system-assigned Managed Identity。
- 授予 App identity
AcrPull、Key Vaultget/list與 SQL roles。 - 設定 Private Endpoint、Private DNS、custom domain 與憑證。
如果共用 CAE 暫時不存在,不要讓腳本建立沒有 VNet/private endpoint 的替代 環境;應停止流程並通知平台管理者。
10.2 日常部署
日常 CI 只需:
main push
-> GitHub OIDC login
-> build image
-> push ACR
-> update existing Container App
-> check revision and logs
如果 App 被刪除並重新建立,請重新取得 identity,重新處理 ACR、Key Vault 與 SQL 授權。
11. 撤權與團隊交接
11.1 移除某個 repository 的信任
az ad app federated-credential list \
--id "$APP_ID" \
--query "[].{id:id,name:name,subject:subject}" \
-o table
az ad app federated-credential delete \
--id "$APP_ID" \
--federated-credential-id "<FEDERATED_CREDENTIAL_ID>"
11.2 移除 Azure role
az role assignment delete \
--assignee-object-id "$SP_OBJECT_ID" \
--role "AcrPush" \
--scope "$ACR_SCOPE"
團隊撤換時同時確認:
- GitHub Environment/repository variables 已移除。
- 該 repository 的 Federated Credentials 已刪除。
- App、ACR、CAE scope 的 role assignments 已移除。
- Container App runtime Managed Identity 沒有被誤刪。
- Audit log 與變更紀錄已保存。
12. 新團隊開通檢查表
平台管理者
- [ ] 所有資源名稱、Subscription、tenant 與 repo 已確認。
- [ ] 每個團隊建立獨立 Service Principal。
- [ ] 只建立 main 或受保護 Environment 的 Federated Credential。
- [ ] 沒有建立 write identity 的
pull_requestcredential。 - [ ] 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 權限。
- [ ] GitHub Environment 的 reviewers 與 branch restriction 已設定。
開發團隊
- [ ] Workflow 有
permissions: id-token: write。 - [ ]
environment或 branch subject 與 Azure FIC 完全一致。 - [ ] 使用
azure/login@v2,沒有 client secret。 - [ ] Docker image 使用不可變的 commit SHA tag。
- [ ] Push 到正確 ACR repository。
- [ ] 只更新指定 Container App。
- [ ] PR workflow 沒有 Azure write token。
- [ ] 部署後確認 revision、logs 與功能。
13. 示範值替換清單
正式使用前,至少替換以下項目:
| 示範值 | 正式值 |
|---|---|
DemoProject / demoproject | 正式專案名稱 |
ExampleOrg | GitHub organization |
<SUBSCRIPTION_ID> | 正式 Subscription ID |
<TENANT_ID> | 正式 Entra tenant ID |
rg-demoproject-* | 正式 Resource Groups |
acr-demoproject-stg | 正式 ACR |
cae-demoproject-stg | 正式 CAE |
ca-demoproject-*-stg | 正式 Container Apps |
sql-demoproject-stg.database.windows.net | 正式 SQL hostname |
demoproject-dev | 正式 Database |
C:\work\demoproject-staging | 團隊實際工作目錄 |
ExampleOrg/demoproject-* | 正式 repositories |
替換後請再次搜尋以下字串,確認沒有殘留示範 placeholder 或錯誤環境:
<SUBSCRIPTION_ID>
<TENANT_ID>
ExampleOrg
demoproject
14. 安全規則
- 不要把 client secret、SQL password、JWT secret 或 storage connection string commit 到 Git。
- 不要把 secrets 貼到 PR、Issue、聊天工具或 deployment log。
- 不要為了快速測試把 SQL firewall 設成
0.0.0.0 - 0.0.0.0。 - 不要把 SQL Resource Group 的 Contributor 當成 SQL data access。
- 不要把 write-capable Azure identity 授予
pull_request。 - 不要自行刪除共用 CAE、Private Endpoint、Private DNS 或 SQL Server。
- 遇到錯誤時,先記錄 repository、workflow run、revision 與錯誤時間, 再請平台管理者檢查權限與網路。
15. 官方參考文件
- GitHub OIDC on Azure: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure
- GitHub OIDC subject reference: https://docs.github.com/en/actions/reference/security/oidc
- Azure Login with OIDC: https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
- Azure Container Apps with GitHub Actions: https://learn.microsoft.com/en-us/azure/container-apps/github-actions
