Skip to content

Azure DevOps Pipeline CI/CD 設定指南

三種 Azure DevOps Pipeline 部署情境的完整 YAML 設定:AKS 部署、App Service 單服務、App Service 多服務加 Deployment Slot。

概述

Azure DevOps Pipeline 是 Microsoft 的 CI/CD 平台,以 YAML 定義建置與部署流程。本文記錄三種實務常見的部署情境:

情境部署目標觸發分支特色
情境一AKS(Azure Kubernetes Service)masterK8s manifest 部署,kubectl 冪等操作
情境二Azure App Service(單一服務)2025Azure CLI 自動建立 WebApp
情境三Azure App Service(多服務 + Slot)2025 / 2026-devCommit Message 控制部署目標

三種情境共用相同的基礎架構:建置 Docker Image → 推送至 ACR(Azure Container Registry)→ 部署至目標環境。

核心內容

Pipeline 基本結構

Azure DevOps Pipeline YAML 的核心元素:

  • trigger:定義觸發條件,batch: true 確保同一分支同時只跑一次
  • variables:Pipeline 層級變數,$() 語法引用,$ 用於編譯時展開
  • stages:Build Stage(建置 Image)→ Deploy Stage(部署)
  • condition:Job 執行條件,可組合分支名稱、自訂變數進行精細控制
  • dependsOn:Stage 相依關係,確保 Deploy 在 Build 完成後才執行

情境一:AKS 部署設計

Build Stage 登入 ACR 並建置推送 Image;Deploy Stage 登入 AKS 後執行兩步驟:

  1. .env 檔建立 K8s Secret(用 --dry-run=client | kubectl apply 實現冪等更新)
  2. sed 替換 K8s manifest 中的 AZURE_IMAGE 佔位符後套用

情境二:App Service 單服務部署

部署腳本採「檢查存在再建立」的冪等邏輯,避免重複執行時報錯。App Settings 透過 appsetting.json 統一管理(放在 repo 根目錄)。

前置需求: Agent Host 必須預先安裝 Azure CLI。(參考 在 Linux 上安裝 Azure CLI

情境三:多服務 + Commit Message 控制 + Deployment Slot

透過讀取最新 commit message 中的關鍵字([Client][Socket]),決定本次要建置和部署的服務,避免每次 push 都建置所有服務。

跨 Stage 傳遞 commit message 的機制:

  1. Build Stage 的 SetCommitMsg Job 讀取 commit message,以 ##vso[task.setvariable ...] 設為 output 變數
  2. Deploy Stage 透過 stageDependencies 語法讀取:
yaml
commitMsg: $[ stageDependencies.Build.SetCommitMsg.outputs['set_commit_msg.commitMsg'] ]

Deployment Slot 用途: 讓同一個 App Service 同時運行兩個不同容器服務(主站 + socket-prd slot),不需額外建立 App Service,節省成本。

服務部署位置PortDockerfile
Web (Client)App Service 主站3300./Dockerfile
Socket ServerSlot socket-prd5000./flask/Dockerfile

關鍵要點

  • condition 語法可組合多個判斷條件,精細控制哪些 Job 執行
  • 跨 Stage 傳遞變數必須透過 stageDependencies,Build Stage 需主動設定 output 變數
  • 冪等操作(先檢查再建立)是 CI/CD 腳本的重要原則,避免重複觸發報錯
  • App Service 部署前必須確認 Agent Host 有 Azure CLI
  • Deployment Slot 適合在同一個 App Service 下服務多個容器,適用小規模微服務架構

實際應用

  • 單一後端服務:情境一(AKS)或情境二(App Service),選擇取決於是否已有 K8s 叢集
  • 前後端分離:情境三,用 commit message 控制分別建置,避免每次都全量建置
  • 需要暫存/灰度環境:善用 Deployment Slot 的 swap 功能實現 blue-green 部署

部署設定參考

以下為實際部署時使用的完整 YAML 設定,供日後查詢與複製使用。

共用環境參數

項目
ACR Service Connectionwyhqacr
Container Registrywyhqbizautacr.azurecr.io
App Service Connectionwyhqappservice
Azure Resource Groupwy-BizAut-dev-rsg
Azure Service PlanWiwynnAngel
AKS Service Connectionaks prd
AKS Namespaceingress-basic
Agent PoolDefault
Image Tag 策略$(Build.BuildId) + latest

情境一:AKS 部署完整 YAML

yaml
trigger:
  batch: true
  branches:
    include:
      - master

resources:
- repo: self

variables:
  serverImageName: $(Build.Repository.Name)-server
  aks-env: aks prd
  namespace: ingress-basic
  dockerRegistryServiceConnection: wyhqacr
  containerRegistry: wyhqbizautacr.azurecr.io
  tag: $(Build.BuildId)

stages:
- stage: Build
  displayName: Build image
  jobs:
  - job: Build
    displayName: Build
    pool:
      name: Default
    steps:
    - task: Docker@2
      displayName: login to ACR
      inputs:
        containerRegistry: $(dockerRegistryServiceConnection)
        command: login
    - task: Docker@2
      displayName: Build Server Image
      inputs:
        containerRegistry: $(dockerRegistryServiceConnection)
        repository: $(serverImageName)
        tags: |
          $(tag)
          latest
        command: buildAndPush
        Dockerfile: Dockerfile
        buildContext: .

- stage: Deploy
  displayName: Deploy to AKS
  dependsOn: Build
  jobs:
  - job: Deploy
    displayName: "Deploy to PRD"
    pool:
      name: Default
    steps:
    - task: Kubernetes@1
      displayName: 'AKS Login'
      inputs:
        kubernetesServiceEndpoint: $(aks-env)
        command: 'login'
    - script: |
        kubectl create secret generic background-task-env --from-env-file=.env -n $(namespace) -o yaml --dry-run=client | kubectl apply -f -
        sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(serverImageName):$(tag)|g" k8s/server.yaml | kubectl apply -f -

情境二:App Service 單服務完整 YAML

yaml
trigger:
  batch: true
  branches:
    include:
      - "2025"

resources:
  - repo: self

variables:
  serverImageName: ${{ lower(variables['Build.Repository.Name']) }}-server
  dockerRegistryServiceConnection: wyhqacr
  containerRegistry: wyhqbizautacr.azurecr.io
  tag: "$(Build.BuildId)"
  appServiceConnection: wyhqappservice
  azureResourceGroup: 'wy-BizAut-dev-rsg'
  azureServicePlan: 'WiwynnAngel'
  azureServiceName: 'budget-python-service'

stages:
  - stage: Build
    displayName: Build image
    jobs:
      - job: BuildServer
        displayName: Build Server
        pool:
          name: Default
        steps:
          - task: Docker@2
            displayName: login to ACR
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              command: login
          - task: Docker@2
            displayName: Build Server Image
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              repository: $(serverImageName)
              tags: |
                $(tag)
                latest
              command: buildAndPush
              Dockerfile: ./Dockerfile
              buildContext: .

  - stage: Deploy
    displayName: "Deploy to Azure App Service"
    condition: eq(variables['Build.SourceBranchName'], '2025')
    dependsOn: Build
    jobs:
      - job: DeployPrd
        displayName: "Deploy Server to Prd"
        pool:
          name: Default
        steps:
          - checkout: self
          - task: AzureCLI@2
            displayName: "Deploy Server to Azure App Service PRD"
            inputs:
              azureSubscription: $(appServiceConnection)
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                set -e
                IMAGE_NAME="$(containerRegistry)/$(serverImageName):$(tag)"
                if az webapp show --name $(azureServiceName) --resource-group $(azureResourceGroup) &>/dev/null; then
                  echo "Web App $(azureServiceName) exists, skip create"
                else
                  echo "Web App $(azureServiceName) not exist, create it now"
                  az webapp create --name $(azureServiceName) \
                                   --resource-group $(azureResourceGroup) \
                                   --plan $(azureServicePlan) \
                                   --https-only \
                                   --container-image-name $IMAGE_NAME
                  echo "Web App $(azureServiceName) created"
                fi
                az webapp config appsettings set --resource-group $(azureResourceGroup) --name $(azureServiceName) --settings @appsetting.json
                az webapp config container set --name $(azureServiceName) --resource-group $(azureResourceGroup) --container-image-name $IMAGE_NAME
                az webapp restart --name $(azureServiceName) --resource-group $(azureResourceGroup)

情境三:多服務 + Commit Message + Slot 完整 YAML

yaml
trigger:
  batch: true
  branches:
    include:
      - "2025"
      - "2026-dev"

variables:
  containerRegistry: wyhqbizautacr.azurecr.io
  dockerRegistryServiceConnection: wyhqacr
  tag: "$(Build.BuildId)"
  serviceImageName: ${{ lower(variables['Build.Repository.Name']) }}-service
  socketImageName: ${{ lower(variables['Build.Repository.Name']) }}-socket
  appServiceConnection: wyhqappservice
  azureResourceGroup: "wy-BizAut-dev-rsg"
  azureServicePlan: "WiwynnAngel"
  azureServiceName: "budget-web-service"
  servicePort: "3300"
  socketPort: "5000"

stages:
  - stage: Build
    displayName: Build images
    jobs:
      - job: SetCommitMsg
        displayName: "Set Commit Message"
        steps:
          - checkout: self
          - script: |
              msg=$(git log -1 --pretty=%B)
              echo "##vso[task.setvariable variable=commitMsg;isOutput=true]$msg"
            displayName: "Get Commit Message"
            name: set_commit_msg
      - job: BuildWeb
        displayName: Build Web (Client)
        condition: |
          and(
            succeeded(),
            or(
              eq(variables['Build.SourceBranchName'], '2025'),
              eq(variables['Build.SourceBranchName'], '2026-dev')
            ),
            contains(variables['commitMsg'], '[Client]')
          )
        pool:
          name: Default
        steps:
          - task: Docker@2
            displayName: Login to ACR
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              command: login
          - task: Docker@2
            displayName: Build and Push Service Image
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              repository: $(serviceImageName)
              tags: |
                $(tag)
                latest
              command: buildAndPush
              Dockerfile: ./Dockerfile
              buildContext: .
      - job: BuildSocket
        displayName: Build Socket Server
        condition: |
          and(
            succeeded(),
            or(
              eq(variables['Build.SourceBranchName'], '2025'),
              eq(variables['Build.SourceBranchName'], '2026-dev')
            ),
            contains(variables['commitMsg'], '[Socket]')
          )
        pool:
          name: Default
        steps:
          - task: Docker@2
            displayName: Login to ACR
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              command: login
          - task: Docker@2
            displayName: Build and Push Socket Image
            inputs:
              containerRegistry: $(dockerRegistryServiceConnection)
              repository: $(socketImageName)
              tags: |
                $(tag)
                latest
              command: buildAndPush
              Dockerfile: ./flask/Dockerfile
              buildContext: ./flask

  - stage: Deploy
    displayName: Deploy to Azure
    dependsOn: Build
    variables:
      commitMsg: $[ stageDependencies.Build.SetCommitMsg.outputs['set_commit_msg.commitMsg'] ]
    jobs:
      - job: DeployWeb
        displayName: Deploy Web (Client)
        condition: |
          and(
            eq(variables['Build.SourceBranchName'], '2025'),
            contains(variables['commitMsg'], '[Client]')
          )
        pool:
          name: Default
        steps:
          - checkout: self
          - task: AzureCLI@2
            displayName: Deploy Web to Azure
            inputs:
              azureSubscription: $(appServiceConnection)
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                set -e
                IMAGE_NAME="$(containerRegistry)/$(serviceImageName):$(tag)"
                if az webapp show --name $(azureServiceName) --resource-group $(azureResourceGroup) &>/dev/null; then
                  echo "Web App $(azureServiceName) exists, skip create"
                else
                  az webapp create --name $(azureServiceName) \
                                   --resource-group $(azureResourceGroup) \
                                   --plan $(azureServicePlan) \
                                   --https-only \
                                   --container-image-name $IMAGE_NAME
                fi
                az webapp config container set --name $(azureServiceName) --resource-group $(azureResourceGroup) --container-image-name $IMAGE_NAME
                az webapp config appsettings set --resource-group $(azureResourceGroup) --name $(azureServiceName) --settings WEBSITES_PORT=$(servicePort)
                az webapp restart --name $(azureServiceName) --resource-group $(azureResourceGroup)

      - job: DeploySocket
        displayName: Deploy Socket Server
        condition: |
          and(
            eq(variables['Build.SourceBranchName'], '2025'),
            contains(variables['commitMsg'], '[Socket]')
          )
        pool:
          name: Default
        steps:
          - checkout: self
          - task: AzureCLI@2
            displayName: Deploy Socket to Azure slot
            inputs:
              azureSubscription: $(appServiceConnection)
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                set -e
                SOCKET_IMAGE="$(containerRegistry)/$(socketImageName):$(tag)"
                if az webapp show --name $(azureServiceName) --resource-group $(azureResourceGroup) &>/dev/null; then
                  echo "Web App $(azureServiceName) exists, skip create"
                else
                  az webapp create --name $(azureServiceName) \
                                   --resource-group $(azureResourceGroup) \
                                   --plan $(azureServicePlan) \
                                   --https-only \
                                   --container-image-name $SOCKET_IMAGE
                fi
                if az webapp deployment slot list --name $(azureServiceName) --resource-group $(azureResourceGroup) | grep -q '"name": "socket-prd"'; then
                  echo "Slot socket-prd exists, skip create"
                else
                  az webapp deployment slot create --name $(azureServiceName) --resource-group $(azureResourceGroup) --configuration-source $(azureServiceName) --slot socket-prd
                fi
                az webapp config appsettings set --resource-group $(azureResourceGroup) --name $(azureServiceName) --slot socket-prd --settings WEBSITES_PORT=$(socketPort)
                az webapp config container set --name $(azureServiceName) --resource-group $(azureResourceGroup) --slot socket-prd --container-image-name $SOCKET_IMAGE

Commit Message 標記規則(情境三)

標記觸發效果
[Client]Build Web + Deploy Web(branch=2025)
[Socket]Build Socket + Deploy Socket(branch=2025)
[Client][Socket]同時觸發兩個服務的 Build / Deploy
無標記所有 Build / Deploy Job 跳過(skipped)

⚠️ 注意: 標記需完整包含中括號,大小寫須與條件一致([Client][Socket])。

Dockerfile 路徑對照(情境三)

服務Dockerfile 路徑buildContext
Web (Client)./Dockerfile.
Socket Server./flask/Dockerfile./flask

⚠️ 注意: DeploySocket Job 目前未包含 az webapp restart 步驟。若部署後需立即重啟 slot,可補上:

bash
az webapp restart --name $(azureServiceName) --resource-group $(azureResourceGroup) --slot socket-prd

⚠️ 注意: appsetting.json 內含敏感設定(連線字串、API Key 等),建議不納入版本控制(加入 .gitignore),改用 Azure Key Vault reference 或 ADO Variable Groups 管理機敏值。

相關概念

來源