Azure DevOps CI/CD 实战
大约 11 分钟约 3395 字
Azure DevOps CI/CD 实战
简介
Azure DevOps 是微软提供的一站式 DevOps 平台,涵盖了从代码管理、持续集成、持续交付到项目管理的完整工具链。它支持任何语言、任何平台,不仅限于 .NET 和 Azure 云服务。Azure DevOps 提供了 SaaS 版本(Azure DevOps Services)和私有化部署版本(Azure DevOps Server),满足不同规模和合规要求。
本文将深入 Azure DevOps 的 CI/CD 实践,从基础 Pipeline 配置到高级多阶段部署,帮助团队建立自动化、可追溯、高质量的交付流水线。
特点
- 全平台支持:支持 Windows、Linux、macOS 构建代理
- YAML 即代码:Pipeline 使用 YAML 定义,纳入版本控制
- 多阶段部署:支持构建、测试、预发布、生产等环境阶段
- 审批门控:支持环境审批、检查和门控
- 丰富扩展:Marketplace 提供大量扩展插件
- 混合云支持:支持自托管代理和云代理
Azure DevOps 项目设置
组织与项目结构
Azure DevOps 组织结构:
Organization (组织)
├── Project: CorePlatform
│ ├── Repos (代码仓库)
│ │ ├── src/
│ │ │ ├── Api/
│ │ │ ├── Web/
│ │ │ └── Shared/
│ │ ├── tests/
│ │ ├── pipelines/
│ │ └── arm-templates/
│ ├── Pipelines (流水线)
│ │ ├── ci-api.yml
│ │ ├── ci-web.yml
│ │ └── cd-deploy.yml
│ ├── Boards (看板)
│ ├── Test Plans (测试计划)
│ ├── Artifacts (制品仓库)
│ └── Environments (环境)
│ ├── Development
│ ├── Staging
│ └── Production服务连接配置
# 使用 Azure CLI 配置服务连接
az devops service-endpoint create \
--organization https://dev.azure.com/myorg \
--project CorePlatform \
--service-endpoint-type azurerm \
--name "Azure-Production" \
--azure-rm-service-principal-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--azure-rm-subscription-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--azure-rm-subscription-name "Production Subscription" \
--azure-rm-tenant-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"Pipeline YAML 详解
基本 CI Pipeline
# pipelines/ci-api.yml
trigger:
branches:
include:
- main
- develop
paths:
include:
- src/Api/*
- src/Shared/*
- tests/Api.Tests/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
projectName: 'MyApp.Api'
stages:
- stage: Build
displayName: '构建'
jobs:
- job: BuildJob
displayName: '构建与单元测试'
steps:
- task: UseDotNet@2
displayName: '安装 .NET SDK'
inputs:
packageType: 'sdk'
version: $(dotnetVersion)
includePreview: false
- task: DotNetCoreCLI@2
displayName: '还原 NuGet 包'
inputs:
command: 'restore'
projects: 'src/Api/Api.csproj'
feedsToUse: 'select'
vstsFeed: 'myorg/my-feed'
- task: DotNetCoreCLI@2
displayName: '构建项目'
inputs:
command: 'build'
projects: 'src/Api/Api.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: '运行单元测试'
inputs:
command: 'test'
projects: 'tests/Api.Tests/Api.Tests.csproj'
arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
publishTestResults: true
- task: DotNetCoreCLI@2
displayName: '发布应用'
inputs:
command: 'publish'
publishWebProjects: false
projects: 'src/Api/Api.csproj'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory) --no-build'
zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: '发布制品'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'api-drop'
publishLocation: 'Container'多阶段 CD Pipeline
# pipelines/cd-deploy.yml
trigger:
none # 由 CI 完成后触发
resources:
pipelines:
- pipeline: ci-pipeline
source: 'CI-API'
trigger:
branches:
include:
- main
variables:
- group: 'global-variables'
- group: 'deployment-variables'
stages:
- stage: Deploy_Dev
displayName: '部署到开发环境'
dependsOn: []
condition: succeeded()
jobs:
- deployment: Deploy
displayName: '部署 API 到 Dev'
environment: 'Development'
strategy:
runOnce:
deploy:
steps:
- download: ci-pipeline
artifact: api-drop
patterns: '**/*.zip'
- task: AzureWebApp@1
displayName: '部署到 Azure Web App (Dev)'
inputs:
azureSubscription: 'Azure-Dev'
appName: '$(DevAppName)'
package: $(Pipeline.Workspace)/ci-pipeline/api-drop/*.zip
runtimeStack: 'DOTNETCORE|8.0'
- task: DotNetCoreCLI@2
displayName: '运行集成测试'
inputs:
command: 'test'
projects: 'tests/Api.Integration.Tests/*.csproj'
arguments: '--configuration Release'
env:
ApiBaseUrl: 'https://$(DevAppName).azurewebsites.net'
- stage: Deploy_Staging
displayName: '部署到预发布环境'
dependsOn: Deploy_Dev
condition: succeeded()
jobs:
- deployment: Deploy
displayName: '部署 API 到 Staging'
environment: 'Staging'
strategy:
runOnce:
deploy:
steps:
- download: ci-pipeline
artifact: api-drop
- task: AzureWebApp@1
displayName: '部署到 Staging Slot'
inputs:
azureSubscription: 'Azure-Staging'
appName: '$(StagingAppName)'
deployToSlotOrASE: true
resourceGroupName: '$(ResourceGroup)'
slotName: 'staging'
package: $(Pipeline.Workspace)/ci-pipeline/api-drop/*.zip
- script: |
echo "运行冒烟测试..."
curl -f https://$(StagingAppName)-staging.azurewebsites.net/health || exit 1
displayName: '冒烟测试'
- stage: Deploy_Production
displayName: '部署到生产环境'
dependsOn: Deploy_Staging
condition: succeeded()
jobs:
- deployment: Deploy
displayName: '部署 API 到 Production'
environment: 'Production'
strategy:
runOnce:
deploy:
steps:
- download: ci-pipeline
artifact: api-drop
- task: AzureWebApp@1
displayName: '部署到 Production'
inputs:
azureSubscription: 'Azure-Production'
appName: '$(ProdAppName)'
deployToSlotOrASE: true
resourceGroupName: '$(ProdResourceGroup)'
slotName: 'staging'
package: $(Pipeline.Workspace)/ci-pipeline/api-drop/*.zip
- script: |
echo "生产环境冒烟测试..."
curl -f https://$(ProdAppName)-staging.azurewebsites.net/health || exit 1
displayName: '生产冒烟测试'
- task: AzureAppServiceManage@0
displayName: '交换部署槽位'
inputs:
azureSubscription: 'Azure-Production'
WebAppName: '$(ProdAppName)'
ResourceGroupName: '$(ProdResourceGroup)'
SourceSlot: 'staging'
SwapWithProduction: true自托管构建代理(Self-hosted Agents)
安装自托管代理
# 下载 Azure DevOps 代理
# Linux
mkdir -p /opt/azagent && cd /opt/azagent
wget https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-linux-x64-3.230.0.tar.gz
tar zxvf vsts-agent-linux-x64-*.tar.gz
# 配置代理
./config.sh
# 输入组织 URL: https://dev.azure.com/myorg
# 输入 PAT token
# 输入代理池名称
# 输入代理名称
# 安装为系统服务
sudo ./svc.sh install
sudo ./svc.sh start
# Windows (PowerShell 管理员)
mkdir C:\azagent
cd C:\azagent
Invoke-WebRequest -Uri https://vstsagentpackage.azureedge.net/agent/3.230.0/vsts-agent-win-x64-3.230.0.zip -OutFile agent.zip
Expand-Archive agent.zip
.\config.cmd
.\svc.cmd install
.\svc.cmd start自托管代理 Docker 配置
# Dockerfile for Azure DevOps Agent
FROM ubuntu:22.04
ENV AZP_AGENT_VERSION=3.230.0
ENV AZP_WORK=_work
RUN apt-get update && apt-get install -y \
curl \
git \
jq \
libicu70 \
dotnet-sdk-8.0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /azp
COPY start.sh .
RUN chmod +x start.sh
CMD ["./start.sh"]#!/bin/bash
# start.sh - Azure DevOps Agent 启动脚本
set -e
if [ -z "$AZP_URL" ]; then
echo "Error: AZP_URL not set"
exit 1
fi
if [ -z "$AZP_TOKEN" ]; then
echo "Error: AZP_TOKEN not set"
exit 1
fi
# 下载代理
curl -L -o agent.tar.gz "https://vstsagentpackage.azureedge.net/agent/${AZP_AGENT_VERSION}/vsts-agent-linux-x64-${AZP_AGENT_VERSION}.tar.gz"
tar zxvf agent.tar.gz
# 配置代理
./config.sh --unattended \
--url "${AZP_URL}" \
--auth pat \
--token "${AZP_TOKEN}" \
--pool "${AZP_POOL:-Default}" \
--agent "${AZP_AGENT_NAME:-$(hostname)}" \
--replace \
--acceptTeeEula
# 清理函数
cleanup() {
./config.sh remove --auth pat --token "${AZP_TOKEN}"
}
trap 'cleanup; exit 0' EXIT
# 运行代理
./run.sh# Kubernetes 部署自托管代理
apiVersion: apps/v1
kind: Deployment
metadata:
name: azure-devops-agent
spec:
replicas: 3
selector:
matchLabels:
app: azagent
template:
metadata:
labels:
app: azagent
spec:
containers:
- name: agent
image: myregistry/azagent:latest
env:
- name: AZP_URL
value: "https://dev.azure.com/myorg"
- name: AZP_TOKEN
valueFrom:
secretKeyRef:
name: azp-secrets
key: pat-token
- name: AZP_POOL
value: "Build-Pool"
- name: AZP_AGENT_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2"变量组与密钥管理
变量组配置
# 使用变量组
variables:
- group: 'shared-variables' # 变量组
- group: '${{ parameters.env }}-variables' # 动态变量组
- name: buildConfiguration
value: 'Release'
# Pipeline 中的参数化配置
parameters:
- name: environment
type: string
default: 'dev'
values:
- dev
- staging
- production
- name: runTests
type: boolean
default: true
stages:
- stage: Deploy
variables:
- group: '${{ parameters.environment }}-config'
jobs:
- deployment: Deploy
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to ${{ parameters.environment }}"# 使用 Azure CLI 管理变量组
# 创建变量组
az pipelines variable-group create \
--name "production-config" \
--variables \
ApiUrl="https://api.example.com" \
LogLevel="Warning" \
MaxRetryCount="3"
# 添加密钥变量
az pipelines variable-group variable create \
--group-id 1 \
--name "DatabaseConnectionString" \
--value "Server=prod-db;Database=MyApp;User=app;Password=Secret123" \
--secret true
# 在 Pipeline 中引用 Key Vault
az pipelines variable-group create \
--name "keyvault-variables" \
--authorize true \
--key-vault "my-keyvault" \
--variables "DatabasePassword" "ApiKey" "CertificatePassword"# 在 Pipeline 中链接 Azure Key Vault
variables:
- group: 'app-variables'
steps:
- task: AzureKeyVault@2
displayName: '获取 Key Vault 密钥'
inputs:
azureSubscription: 'Azure-Production'
KeyVaultName: 'my-keyvault'
SecretsFilter: 'DatabasePassword,ApiKey,CertificatePassword'
RunAsPreJob: false
- script: |
echo "使用 Key Vault 中的密钥..."
dotnet publish --configuration Release
env:
DB_PASSWORD: $(DatabasePassword)
API_KEY: $(ApiKey)环境审批与门控
环境配置
# 生产环境需要审批
# 在 Azure DevOps UI 中配置:
# Pipelines -> Environments -> Production -> Checks
# 审批检查类型:
# 1. Approvals - 指定审批人
# 2. Branch control - 限制分支
# 3. Evaluate artifact - 制品评估
# 4. Exclusive lock - 排他锁
# 5. Required template - 必须使用指定模板
# Pipeline 中使用环境
stages:
- stage: DeployProd
jobs:
- deployment: DeployToProd
environment: 'Production' # 此环境配置了审批
strategy:
runOnce:
deploy:
steps:
- script: echo "部署到生产环境"条件部署
# 基于条件的部署策略
stages:
- stage: Build
jobs:
- job: Build
steps:
- script: echo "Building..."
- script: echo "##vso[task.setvariable variable=shouldDeploy;isOutput=true]true"
name: setDeployFlag
- stage: Deploy
dependsOn: Build
condition: and(
succeeded(),
eq(dependencies.Build.outputs['Build.setDeployFlag.shouldDeploy'], 'true')
)
jobs:
- deployment: Deploy
environment: 'Staging'
strategy:
runOnce:
deploy:
steps:
- script: echo "条件部署执行"制品管理(Artifact Management)
发布和消费制品
# 发布 NuGet 包到 Azure Artifacts
steps:
- task: DotNetCoreCLI@2
displayName: '打包 NuGet'
inputs:
command: 'pack'
packagesToPack: 'src/**/*.csproj'
configuration: '$(buildConfiguration)'
versioningScheme: 'byPrereleaseNumber'
majorVersion: '1'
minorVersion: '0'
patchVersion: '0'
- task: NuGetCommand@2
displayName: '推送到 Azure Artifacts'
inputs:
command: 'push'
publishVstsFeed: 'myorg/my-feed'
allowPackageConflicts: true
# 消费 Azure Artifacts 中的 NuGet 包
steps:
- task: NuGetAuthenticate@1
displayName: 'NuGet 认证'
- task: DotNetCoreCLI@2
displayName: '还原私有包'
inputs:
command: 'restore'
feedsToUse: 'select'
vstsFeed: 'myorg/my-feed'Docker 镜像管理
# 构建、扫描、推送 Docker 镜像
steps:
- task: Docker@2
displayName: '构建 Docker 镜像'
inputs:
containerRegistry: 'my-acr-connection'
repository: 'myapp/api'
Dockerfile: 'src/Api/Dockerfile'
buildContext: 'src/Api'
tags: |
$(Build.BuildId)
latest
- task: Docker@2
displayName: '推送镜像到 ACR'
inputs:
containerRegistry: 'my-acr-connection'
repository: 'myapp/api'
command: 'push'
tags: |
$(Build.BuildId)
latest测试集成
测试结果收集
# 完整的测试流水线
stages:
- stage: Test
jobs:
- job: UnitTests
displayName: '单元测试'
steps:
- task: DotNetCoreCLI@2
displayName: '运行单元测试'
inputs:
command: 'test'
projects: 'tests/**/*.Unit.Tests/*.csproj'
arguments: >
--configuration Release
--collect:"XPlat Code Coverage"
--logger "trx"
--results-directory $(Build.SourcesDirectory)/test-results
continueOnError: false
- task: PublishTestResults@2
displayName: '发布测试结果'
inputs:
testResultsFiles: '**/*.trx'
testResultsFormat: 'VSTest'
mergeTestResults: true
failTaskOnFailedTests: true
condition: always()
- task: PublishCodeCoverageResults@2
displayName: '发布代码覆盖率'
inputs:
summaryFileLocation: '**/coverage.cobertura.xml'
pathToSources: $(Build.SourcesDirectory)
condition: always()
- job: IntegrationTests
displayName: '集成测试'
dependsOn: UnitTests
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
ACCEPT_EULA: 'Y'
SA_PASSWORD: 'Test123!@#'
ports:
- 1433:1433
steps:
- script: |
echo "等待 SQL Server 启动..."
for i in {1..30}; do
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Test123!@#' -C -Q "SELECT 1" && break
sleep 2
done
displayName: '等待数据库就绪'
- task: DotNetCoreCLI@2
displayName: '运行集成测试'
inputs:
command: 'test'
projects: 'tests/**/*.Integration.Tests/*.csproj'
arguments: '--configuration Release --logger "trx"'
env:
ConnectionStrings__DefaultConnection: "Server=localhost;Database=TestDb;User=sa;Password=Test123!@#;TrustServerCertificate=True"
- task: PublishTestResults@2
displayName: '发布集成测试结果'
inputs:
testResultsFiles: '**/*.trx'
testResultsFormat: 'VSTest'
condition: always()部署槽位(Deployment Slots)
# 蓝绿部署使用部署槽位
stages:
- stage: BlueGreenDeploy
jobs:
- deployment: DeployToSlot
environment: 'Production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: '部署到 Staging Slot'
inputs:
azureSubscription: 'Azure-Production'
appName: '$(ProdAppName)'
deployToSlotOrASE: true
resourceGroupName: '$(ProdResourceGroup)'
slotName: 'staging'
package: $(Pipeline.Workspace)/**/*.zip
- script: |
# 等待应用启动
sleep 30
# 预热请求
curl -s https://$(ProdAppName)-staging.azurewebsites.net/health
# 运行冒烟测试
curl -f https://$(ProdAppName)-staging.azurewebsites.net/api/health || exit 1
displayName: '预热和冒烟测试'
- task: AzureAppServiceManage@0
displayName: '交换 Staging 到 Production'
inputs:
azureSubscription: 'Azure-Production'
WebAppName: '$(ProdAppName)'
ResourceGroupName: '$(ProdResourceGroup)'
SourceSlot: 'staging'
SwapWithProduction: true
- script: |
# 交换后验证
curl -f https://$(ProdAppName).azurewebsites.net/api/health || exit 1
displayName: '交换后验证'ARM/Bicep 部署
Bicep 基础设施即代码
// infra/main.bicep - Azure 基础设施定义
targetScope = 'subscription'
@description('环境名称')
param environment string = 'dev'
@description('区域')
param location string = 'eastasia'
@description('应用名称')
param appName string = 'myapp'
var tags = {
Environment: environment
Application: appName
ManagedBy: 'AzureDevOps'
}
var resourceToken = '${appName}-${environment}'
// 资源组
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'rg-${resourceToken}'
location: location
tags: tags
}
// App Service Plan
module appServicePlan './modules/app-service-plan.bicep' = {
scope: rg
name: 'appServicePlan'
params: {
name: 'plan-${resourceToken}'
location: location
skuName: (environment == 'production') ? 'P1v3' : 'B2'
tags: tags
}
}
// Web App
module webApp './modules/web-app.bicep' = {
scope: rg
name: 'webApp'
params: {
name: 'app-${resourceToken}'
location: location
appServicePlanId: appServicePlan.outputs.id
runtimeName: 'dotnet'
runtimeVersion: '8.0'
keyVaultName: 'kv-${resourceToken}'
tags: tags
}
}
// SQL Database
module sqlDb './modules/sql-database.bicep' = {
scope: rg
name: 'sqlDb'
params: {
serverName: 'sql-${resourceToken}'
databaseName: 'sqldb-${resourceToken}'
location: location
administratorLogin: 'sqladmin'
administratorLoginPassword: '${environment == 'production' ? '' : ''}'
skuName: (environment == 'production') ? 'S2' : 'S0'
tags: tags
}
}
output webAppName string = webApp.outputs.name
output webAppUrl string = webApp.outputs.url
output sqlServerName string = sqlDb.outputs.serverName在 Pipeline 中部署 Bicep
# Bicep 部署 Pipeline
stages:
- stage: Infrastructure
displayName: '基础设施部署'
jobs:
- job: DeployInfra
displayName: '部署 Azure 基础设施'
steps:
- task: AzureCLI@2
displayName: '验证 Bicep 模板'
inputs:
azureSubscription: 'Azure-${{ parameters.environment }}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment sub validate \
--location $(location) \
--template-file infra/main.bicep \
--parameters environment=${{ parameters.environment }} \
--parameters appName=$(appName)
- task: AzureCLI@2
displayName: 'What-If 预览变更'
inputs:
azureSubscription: 'Azure-${{ parameters.environment }}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment sub what-if \
--location $(location) \
--template-file infra/main.bicep \
--parameters environment=${{ parameters.environment }} \
--parameters appName=$(appName)
- task: AzureCLI@2
displayName: '部署 Bicep 模板'
inputs:
azureSubscription: 'Azure-${{ parameters.environment }}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment sub create \
--location $(location) \
--template-file infra/main.bicep \
--parameters environment=${{ parameters.environment }} \
--parameters appName=$(appName) \
--name "deploy-$(Build.BuildNumber)"模板复用
Pipeline 模板
# templates/build-dotnet.yml - 可复用的构建模板
parameters:
- name: projectPath
type: string
- name: testProjectPath
type: string
- name: buildConfiguration
type: string
default: 'Release'
- name: dotnetVersion
type: string
default: '8.0.x'
steps:
- task: UseDotNet@2
displayName: '安装 .NET SDK ${{ parameters.dotnetVersion }}'
inputs:
packageType: 'sdk'
version: '${{ parameters.dotnetVersion }}'
- task: DotNetCoreCLI@2
displayName: '还原 NuGet 包'
inputs:
command: 'restore'
projects: '${{ parameters.projectPath }}'
- task: DotNetCoreCLI@2
displayName: '构建 ${{ parameters.projectPath }}'
inputs:
command: 'build'
projects: '${{ parameters.projectPath }}'
arguments: '--configuration ${{ parameters.buildConfiguration }} --no-restore'
- task: DotNetCoreCLI@2
displayName: '运行测试 ${{ parameters.testProjectPath }}'
inputs:
command: 'test'
projects: '${{ parameters.testProjectPath }}'
arguments: '--configuration ${{ parameters.buildConfiguration }} --no-build --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: '发布应用'
inputs:
command: 'publish'
publishWebProjects: false
projects: '${{ parameters.projectPath }}'
arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory) --no-build'
zipAfterPublish: true# 使用模板的 Pipeline
trigger:
branches:
include: [main]
resources:
repositories:
- repository: templates
type: git
name: 'PipelineTemplates'
extends:
template: templates/standard-pipeline.yml@templates
parameters:
stages:
- stage: Build
jobs:
- job: BuildApi
steps:
- template: templates/build-dotnet.yml@templates
parameters:
projectPath: 'src/Api/Api.csproj'
testProjectPath: 'tests/Api.Tests/Api.Tests.csproj'
- job: BuildWeb
steps:
- template: templates/build-dotnet.yml@templates
parameters:
projectPath: 'src/Web/Web.csproj'
testProjectPath: 'tests/Web.Tests/Web.Tests.csproj'总结
Azure DevOps 提供了完整的 CI/CD 工具链,通过 YAML Pipeline 实现基础设施即代码,支持多阶段部署、环境审批、制品管理和自动化测试集成。
关键实践:Pipeline 即代码、最小权限服务连接、环境门控、自动化测试、蓝绿部署。
关键知识点
- Azure DevOps 支持 YAML 和经典编辑器两种 Pipeline 定义方式
- 变量组集中管理跨 Pipeline 的配置
- 自托管代理适合需要特殊构建环境的场景
- 部署槽位实现零停机部署
- Bicep 是 Azure 推荐的基础设施即代码语言
- 模板复用减少 Pipeline 代码重复
常见误区
误区1:Pipeline YAML 不需要版本控制
Pipeline YAML 应该与应用代码一起纳入版本控制,确保变更可追溯。
误区2:密钥直接写在 YAML 中
密钥应使用 Azure Key Vault 或变量组的密钥功能,不要硬编码在 YAML 中。
误区3:只在 CI 中运行测试
测试应该在 CD 的各个阶段都运行,包括部署后的冒烟测试和集成测试。
误区4:忽略部署槽位预热
直接将流量切换到冷启动的应用会导致用户请求超时。应在交换前预热 staging slot。
进阶路线
- 多仓库 Pipeline:跨仓库触发和制品共享
- 安全扫描集成:SonarQube、Snyk 等安全工具集成
- 性能测试集成:Apache JMeter、k6 等负载测试
- GitOps 模式:结合 Flux/ArgoCD 的 GitOps 部署
- 多云部署:通过 Azure DevOps 部署到 AWS/GCP
适用场景
- .NET 应用程序的完整 CI/CD
- Azure 云服务的自动化部署
- 微服务架构的多服务流水线
- 团队协作的代码审查和合并策略
- 企业级合规审计要求
落地建议
- 从简单的 CI Pipeline 开始,逐步增加 CD 阶段
- 使用 YAML Pipeline 而非经典编辑器,便于版本控制
- 为每个环境创建独立的服务连接和变量组
- 启用生产环境的审批门控
- 将基础设施部署(Bicep)和应用部署分离为独立 Pipeline
- 定期审查和清理未使用的 Pipeline 和制品
排错清单
复盘问题
- 你的 CI/CD Pipeline 从提交到部署需要多长时间?
- 生产部署是否有自动化冒烟测试?
- 你的部署失败率是多少?主要失败原因是什么?
- 基础设施变更是不是也纳入了 Pipeline?
- 你的 Pipeline 是否有定期清理和优化?
延伸阅读
- Azure DevOps Documentation
- YAML Schema Reference
- Bicep Documentation
- Azure DevOps YAML Pipeline Templates
- 《Azure DevOps Server 实战》- 徐磊
