Azure DevOps 进阶用法:从入门到精通
我为什么要折腾 Azure DevOps
去年我们团队遇到一个很头疼的问题:十几个微服务仓库,CI 配置各写各的,发布流程全靠人肉在网页上点点点,一次发布要两个小时,还经常漏掉某个服务的数据库迁移脚本。有一次周五下午发布,一个服务的流水线悄悄用了三个月前缓存的 Docker 镜像,直接把生产环境搞挂了。
我下定决心把整个流程收拢到 Azure DevOps 里,用 YAML Pipeline + 模板 + 多阶段发布重做一遍。这篇文章就是我把团队从“人肉发布”带到“一键发布”过程中踩过的坑和总结出来的经验。下面都以实际项目为例,假设你已经有基本的 Pipeline 概念。
第一步:用模板统一所有仓库的 CI
十几个仓库各写各的 YAML 显然不行。我的做法是建一个独立的 pipeline-templates 仓库,把公共逻辑抽出来:
# pipeline-templates/templates/dotnet-build.yml
parameters:
- name: projectPath
type: string
- name: runTests
type: boolean
default: true
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- script: dotnet restore ${{ parameters.projectPath }}
- script: dotnet build ${{ parameters.projectPath }} --no-restore -c Release
- ${{ if eq(parameters.runTests, true) }}:
- script: dotnet test ${{ parameters.projectPath }} --no-build -c Release --collect:"XPlat Code Coverage"
业务仓库里就只剩几行:
# service-a/azure-pipelines.yml
resources:
repositories:
- repository: templates
type: git
name: DevOps/pipeline-templates
stages:
- stage: Build
jobs:
- job: Build
pool: vmImage: 'ubuntu-latest'
steps:
- template: templates/dotnet-build.yml@templates
parameters:
projectPath: 'src/ServiceA/ServiceA.csproj'
踩过的坑:模板仓库的引用默认用的是触发时的 commit,如果模板改了但没在业务仓库里更新 ref,跑的还是旧逻辑。后来我用 ref: refs/tags/stable 固定到 tag,模板升级走 tag 流程,所有仓库再统一升版本。这一点一定要在一开始就定好,不然各仓库模板版本漂移会很痛苦。
第二步:多阶段流水线和审批门禁
原来的发布是手动点按钮,我改成了多阶段 + Environment 审批:
- stage: DeployStaging
jobs:
- deployment: DeployStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-k8s.yml@templates
parameters:
namespace: service-a-staging
- stage: DeployProduction
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProduction
environment: 'production' # 在这里配置审批人和检查
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-k8s.yml@templates
parameters:
namespace: service-a-prod
关键在 Project Settings → Environments → production 里配置 Approvals and Checks。我们配了两个人审批、一个业务时间窗口(禁止周五下午和周末发布),还有调用内部健康检查 API 的 Invoke REST API 检查。
意外收获:Environment 检查里的 "Exclusive Lock" 也很好用,可以防止两个流水线同时往生产环境部署。我们以前真出过两个人同时发布互相覆盖的事故。
第三步:变量管理的正确姿势
入门时大家都爱把连接字符串硬编码在 YAML 里(或者更糟,明文写在变量组里)。正确的分层是:
- Variable Groups(变量组,在 Library 里创建)放环境级配置,链接 Azure Key Vault 的密钥
- Pipeline variables(流水线变量)放流水线级配置
- 运行时参数放
parameters
variables:
- group: 'service-a-production' # 这个组链接了 Key Vault
- name: dotnetVersion
value: '8.0.x'
链接 Key Vault 后,secret 名就是 KV 里的 secret 名,直接 $(ConnectionString) 引用,不用同步两份。坑:Key Vault 的访问策略(或 RBAC)要给 DevOps 的 Service Connection 对应的服务主体授权,报错信息很不直观,只会说 "secret not found",我在这上面浪费了一下午。
另外注意:$(VAR) 是运行时展开的,$() 写在 script 里如果变量不存在会原样输出,别把 secret 用 setvar 回写到日志——DevOps 会自动遮蔽已知的 secret 变量,但自己拼出来的字符串不会。
第四步:Monorepo 的路径触发和多阶段优化
我们有几个服务放在同一个仓库里,全量触发太浪费。路径过滤是基本操作:
trigger:
branches:
include: [ main ]
paths:
include:
- src/ServiceA/*
- shared/*
exclude:
- '**/*.md'
但注意 shared/* 一改,所有服务都得重新构建,单靠 paths 做不到。我的方案是加一个 detect-changes job,用 git diff 算出受影响的服务列表,再传给下游 stage:
- job: detect
steps:
- script: |
CHANGED=$(git diff --name-only HEAD~1 HEAD | grep '^shared/' || true)
if [ -n "$CHANGED" ]; then
echo "##vso[task.setvariable variable=forceAll;isOutput=true]true"
fi
name: detectStep
下游用 condition: or(succeeded(), eq(...)) 加上 stage 依赖组合。这块逻辑写起来繁琐,但省下的构建时间非常可观——我们把平均 CI 时长从 25 分钟压到了 8 分钟。
第五步:发布历史、回滚和可观测性
进阶最重要的一点:每次发布必须可追溯、可回滚。我的实践:
- 部署脚本把
$(Build.BuildId)、commit hash 记录到 Kubernetes annotation 里 - 用 Deployment job 的 Redeploy 功能一键回滚到上一个成功的 stage run
- 在 Pipeline 里加 Publishing Build Artifacts,把 IaC 的渲染结果(helm template 输出)存档,出问题能看当时到底部署了什么
- script: helm template ./chart -f values-prod.yaml > rendered.yaml
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: 'rendered.yaml'
artifactName: 'manifests-$(Build.BuildId)'
有一次生产问题,就是靠对比两个 build id 的 rendered.yaml,十分钟就定位到是某个 values 改错了。
实用技巧汇总
- Pipeline 缓存:用
Cache@2task 缓存 NuGet/npm,第一次构建之后每次能省 3-5 分钟 - Pipeline analytics:在 Pipelines → Analytics 里看成功率和最慢的 task,建议每周看一次
- Service Connection 权限最小化:别图省事用 Owner 身份的连接,出事范围太大
continueOnError: true谨慎用:它会显示绿色但实际已经失败,要配合succeededOrFailed()才有意义- YAML 里用
extends:比template更适合整条流水线级别的复用
局限和不满
老实说几个槽点:YAML 报错信息经常指向错误的行号,调试模板嵌套很痛苦,我的做法是本地用 VS Code 的 Azure Pipelines 插件做 schema 校验。Environment checks 配置复杂了以后,排查超时很费劲,日志分散在好几个地方。还有 Classic release pipeline 和 YAML 的功能不完全对齐,网上老教程混着两种写法,照抄会踩坑。最后,审批锁死在 Environment 上,如果需要更灵活的动态审批(比如按变更大小决定审批人),原生能力不够,得自己调 REST API。
总结
从人肉发布到现在的体系,我们发布时间从两小时降到十五分钟,生产事故明显减少。核心思路就三条:模板化保证一致性、多阶段+门禁保证安全、artifacts+记录保证可回滚。入门容易,真正精通在于把这些模式固化下来,并忍住在早期就把所有花哨功能塞进去的冲动——先让流程简单可靠,再逐步加检查。