實戰教學 · CI/CD

用 GitHub Actions
部署 Azure Web App

從 Service Principal、Repository Secret 到 deployment slot,整理一條可檢查、可重現的自動部署路徑。

把應用程式推到 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。

Deployment flow
Git Push→ Build & Publish→ Azure Login→ 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 PrincipalManaged 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。

放回這次的情境:若 workflow 使用 GitHub-hosted runner,不能直接借用某個 Azure 資源的 Managed Identity,通常應採 Service Principal 搭配 OIDC,或像本文示範一樣搭配 Repository Secret。只有 runner 本身部署在支援 Managed Identity 的 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。

Secret 放進 GitHub,不代表可以永久不管。
正式環境仍要設定有效期限、定期輪替,並在不再使用時刪除。錄影、截圖、課程文件與 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,都走同一條可檢查、可重現的交付路徑。

參考資料