Appearance
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) | master | K8s manifest 部署,kubectl 冪等操作 |
| 情境二 | Azure App Service(單一服務) | 2025 | Azure CLI 自動建立 WebApp |
| 情境三 | Azure App Service(多服務 + Slot) | 2025 / 2026-dev | Commit 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 後執行兩步驟:
- 以
.env檔建立 K8s Secret(用--dry-run=client | kubectl apply實現冪等更新) - 用
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 的機制:
- Build Stage 的
SetCommitMsgJob 讀取 commit message,以##vso[task.setvariable ...]設為 output 變數 - Deploy Stage 透過
stageDependencies語法讀取:
yaml
commitMsg: $[ stageDependencies.Build.SetCommitMsg.outputs['set_commit_msg.commitMsg'] ]Deployment Slot 用途: 讓同一個 App Service 同時運行兩個不同容器服務(主站 + socket-prd slot),不需額外建立 App Service,節省成本。
| 服務 | 部署位置 | Port | Dockerfile |
|---|---|---|---|
| Web (Client) | App Service 主站 | 3300 | ./Dockerfile |
| Socket Server | Slot socket-prd | 5000 | ./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 Connection | wyhqacr |
| Container Registry | wyhqbizautacr.azurecr.io |
| App Service Connection | wyhqappservice |
| Azure Resource Group | wy-BizAut-dev-rsg |
| Azure Service Plan | WiwynnAngel |
| AKS Service Connection | aks prd |
| AKS Namespace | ingress-basic |
| Agent Pool | Default |
| 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_IMAGECommit 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,可補上:bashaz webapp restart --name $(azureServiceName) --resource-group $(azureResourceGroup) --slot socket-prd
⚠️ 注意:
appsetting.json內含敏感設定(連線字串、API Key 等),建議不納入版本控制(加入.gitignore),改用 Azure Key Vault reference 或 ADO Variable Groups 管理機敏值。
相關概念
- GitLab Runner Docker 部署 — GitLab 平台的 CI Runner 設定
- GitLab CI/CD 部署至 Azure App Service — 以 GitLab CI/CD +
az login直接認證部署至 App Service,Service Principal 認證與az webapp up原始碼部署選項 - Kubernetes RBAC 帳號管理 — AKS Pipeline 需要的 K8s 存取設定
- Azure Pipeline — Kubeconfig 隔離最佳實踐 — self-hosted runner 上以 TempDirectory 隔離 kubeconfig 避免 race condition
- Azure DevOps — 禁止直接 Push 到 master Branch SOP — 搭配 Pipeline 的 Branch Policy 保護:禁止直接 push、強制 PR 審核流程
- Azure Pipelines Self-Hosted Agent — Ubuntu 安裝與服務設定 — Self-Hosted Agent 在 Ubuntu 上的安裝與 systemd 常駐服務 SOP