把應用程式推到 GitHub 之後,下一個很自然的需求,就是讓程式碼一有異動便自動建置並部署到 Azure Web App。
真正需要想清楚的,通常不是 dotnet publish 怎麼寫,而是 GitHub Actions 要用什麼身分登入 Azure、這個身分可以操作哪些資源,以及 workflow 最後要部署到 production 還是 deployment slot。
這次示範的是一個最小可行做法:在 GitHub Codespaces 裡準備 .NET Web App,透過 GitHub Actions 完成 restore、build、publish,再以 Service Principal 登入 Azure,把成品部署到指定的 Azure Web App。
Workflow 不只負責建置,還要處理 Azure 身分
一份部署 Azure Web App 的 workflow,大致可以分成兩個部分。前半段是一般的 .NET 建置流程:checkout 程式碼、準備 .NET 環境、restore 套件、build,最後 publish 到固定目錄。
後半段才是雲端部署:先用 azure/login 取得 Azure 身分,再交給 azure/webapps-deploy 上傳 publish 產物。
- name: Login Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: testwebapp202609
package: ./publish
azure/webapps-deploy 知道要把哪個資料夾送到哪個 Web App,但它本身不能憑空取得 Azure 權限。前面的 azure/login 才是關鍵:GitHub Actions 必須先證明「我是誰」,後續部署動作才能通過 Azure Resource Manager 的授權檢查。
Service Principal 是給自動化流程使用的 Azure 身分
人在 Azure Portal 操作時,可以使用自己的帳號登入;GitHub Actions 是無人值守的自動化流程,不適合借用某個人的帳號與密碼。因此,這裡建立一個 Service Principal,讓 workflow 以應用程式身分登入 Azure。
Service Principal 可以搭配 client secret 或憑證登入,也可以透過 OpenID Connect(OIDC)建立 federated credential,以 GitHub 簽發的短效 token 換取 Azure access token。前者容易理解與建置;後者不必保存長效 client secret,通常更適合新的正式環境。
Service Principal 與 Managed Identity 有什麼不同?
兩者都能讓程式取得 Microsoft Entra ID 身分,再透過 Azure RBAC 存取資源;主要差別在於身分由誰建立、憑證由誰管理,以及程式在哪裡執行。
| 比較項目 | Service Principal | Managed Identity |
|---|---|---|
| 主要用途 | 提供應用程式、CI/CD 或外部自動化流程一個可登入 Azure 的身分。 | 提供 Azure VM、App Service、Function 等 Azure 資源一個原生身分,用來存取其他 Azure 服務。 |
| 建立方式 | 通常由團隊建立 App Registration / Service Principal,再設定 secret、憑證或 federated credential。 | 在 Azure 資源上啟用 system-assigned,或建立可供多個資源使用的 user-assigned identity;Azure 會管理背後的 Service Principal。 |
| 憑證管理 | 使用 secret 或憑證時,團隊要負責安全保存、有效期限與輪替;使用 OIDC 則可避免長效 secret。 | 不需自行保存密碼;Azure 平台負責憑證與 token 生命週期,程式透過 Azure 身分端點取 token。 |
| 適用執行位置 | 可用於 GitHub-hosted runner、開發機、其他雲端或任何能連線到 Entra ID 的環境。 | 主要用於支援 Managed Identity 的 Azure 執行環境;不是一組可以帶到任意外部主機使用的帳密。 |
| 生命週期 | 獨立於單一 Azure 計算資源,需自行治理與移除。 | System-assigned identity 會跟著所屬資源建立與刪除;user-assigned identity 則有獨立生命週期。 |
Service Principal 的功能,是讓 Azure 外部或跨環境的自動化工具有一個明確身分,例如 GitHub-hosted runner 登入 Azure、Terraform 建立資源,或部署工具呼叫 Azure Resource Manager。
Managed Identity 的功能,則是讓「已經跑在 Azure 裡的工作負載」不必保存帳密。例如 App Service 讀取 Key Vault secret、VM 存取 Storage,或 Azure 上的 self-hosted GitHub Actions runner 登入 Azure。
azure/login 的 Managed Identity 模式。
Managed Identity 不是 Service Principal 的完全替代品;它是 Azure 代管生命週期、專門提供給 Azure 工作負載使用的一種身分。
權限重點不只是角色,還有 Scope
建立 Service Principal 時,最重要的不是把權限開得越大越省事,而是決定它的 scope。這次把 scope 限制在特定 Resource Group,並指派 Website Contributor。
這樣它可以部署該 Resource Group 底下的 Web App,但不會因此取得其他 Resource Group 的權限。如果同一套系統包含多個 Web App,這個做法能讓它們共用部署身分;若某個 workflow 只負責一個網站,還可以再評估把 scope 收斂到單一 Web App。
準備 Service Principal 的 script,通常要完成以下幾件事:
- 確認目前 Azure context、Tenant 與 Subscription。
- 確認目標 Resource Group 存在。
- 建立 App Registration 與 Service Principal。
- 建立 client secret,或改為設定 OIDC federated credential。
- 在指定 scope 建立所需的角色指派。
使用 client secret 時,最後提供給 azure/login 的 JSON 會包含 clientId、clientSecret、tenantId 與 subscriptionId。
Credentials 為什麼放在 GitHub Repository Secret?
這份 credentials 不能直接寫進 deploy.yml,也不該 commit 到 repository。可以將完整 JSON 存成名為 AZURE_CREDENTIALS 的 Repository Secret,再透過下列語法取用:
creds: ${{ secrets.AZURE_CREDENTIALS }}
GitHub Actions 執行 workflow 時會把 Secret 注入該次 job,repository 裡只看得到 Secret 名稱,不會看到原始值。這能避免憑證跟著程式碼版本流動,也方便日後替換 client secret,而不必修改 workflow。
正式環境仍要設定有效期限、定期輪替,並在不再使用時刪除。錄影、截圖、課程文件與 CI log,也都應避免留下仍有效的 secret。若是新的正式環境,優先評估 GitHub Actions OIDC。
一份完整的 .NET 部署 Workflow
以下範例在 push 到 main 時執行,先建置並 publish,再登入 Azure 並部署 production:
name: Deploy Azure Web App
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Publish
run: dotnet publish --configuration Release --no-build --output ./publish
- name: Login Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: testwebapp202609
package: ./publish
專案若不在 repository 根目錄,應在 restore、build 與 publish 明確指定 solution 或 project 路徑,避免 workflow 因目錄結構改變而建置錯誤的專案。
為什麼登入成功,部署還是失敗?
第一次補上 AZURE_CREDENTIALS 後,azure/login 已經成功,部署卻仍然失敗。原因不是權限,而是 workflow 指定了 staging slot,但目標 Web App 並沒有這個 slot。
Deployment slot 可以把新版本先部署到另一個站台,再驗證、交換到 production。它很適合 staging、藍綠部署與降低正式環境切換風險,但前提是該 slot 已存在,而且 App Service Plan 層級支援這項功能。
部署 Production
如果這次就是要部署 production,不需要提供 slot-name:
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: testwebapp202609
package: ./publish
部署 Staging Slot
如果確實要部署 staging,則應先在 Azure 建立對應 slot,再保留:
- name: Deploy to staging
uses: azure/webapps-deploy@v3
with:
app-name: testwebapp202609
slot-name: staging
package: ./publish
slot-name 不能只是從範本留下來的預設值,它必須對應 Azure 上真正存在的部署環境。
把登入問題與部署問題分開判讀
這個錯誤很值得保留,因為它把兩類問題分得很清楚:
azure/login失敗:檢查 Tenant、Subscription、client ID、Secret / OIDC 設定,以及 Service Principal 是否存在。- 登入成功但
webapps-deploy失敗:檢查 RBAC、Web App 名稱、slot、package 路徑,以及目標資源是否存在。 - 部署成功但網站沒有更新:檢查部署目標是否為預期 slot、publish 產物內容、啟動命令與應用程式 log。
只看最後一個紅燈,很容易把部署設定錯誤誤判成 Service Principal 權限不足。從「身分驗證 → 授權 → 目標資源 → 部署產物」逐層檢查,通常會更快找到真正原因。
把權限邊界與部署目標寫清楚
這套流程的價值,不只是把手動上傳改成自動部署。程式碼 push 之後,建置方式、Azure 登入身分、部署目標與錯誤紀錄都留在同一份 workflow 裡;團隊不必依賴某位成員的本機設定,也比較容易重現「哪一版程式由哪一次執行部署到哪個網站」。
實務上最值得帶走的是兩個判斷。第一,Service Principal 的 scope 應依部署責任劃分,能限制在 Resource Group,就不必直接授權整個 Subscription。第二,production 與 slot 是不同部署目標,workflow 必須反映 Azure 上真實存在的環境。
當身分、權限與部署目標都先釐清,流程就會變得很直接:
建置產物、登入 Azure、部署到明確的 Web App。然後讓每一次 push,都走同一條可檢查、可重現的交付路徑。
參考資料
- 本文參考 Repository:isdaviddong/AzWebAppDeployFromGitHubAction
- 本文參考 YouTube 影片:用 GitHub Actions 部署 Azure Web App
- Azure Login GitHub Action
- Azure Web Apps Deploy GitHub Action
- Azure RBAC 內建角色:Website Contributor
- Azure resources 的 Managed Identity
- Azure App Service deployment slots
- 使用 GitHub Actions 部署 Azure App Service