Azure Pipelines配置全攻略:从YAML到自托管代理的CI/CD避坑实践
简介:这是一份面向 Azure DevOps 用户的 Azure Pipelines 官方文档操作手册,适合需要搭建持续集成与持续交付流程的开发、运维及 DevOps 工程师阅读。内容系统覆盖核心概念与落地操作:从代理、条件、表达式、环境、作业、阶段、任务、模板到触发器均有说明,并针对 GitHub、Azure Repos、Bitbucket 等仓库集成、多环境部署、容器构建、测试执行及代理池管理等场景给出操作指南,可帮助读者快速掌握管道编写与发布配置。资源为 1 个 PDF 文件,整体大小 106.6MB,页面内容为 Azure Pipelines 文档的中文版节选,既适合新手从零了解 CI/CD 理念,也对已使用者查阅具体任务参数与排错思路有参考价值。目前已有 412 人学习浏览。通过阅读这份手册,读者可获得一份结构完整的 Azure Pipelines 速查资料,便于在项目中落地自动化构建、测试与部署流程,减少摸索时间。
1. 从找不到代理开始:为什么配置 Azure Pipelines 的第一步就让人想退坑
某次帮同事调一个 Node.js 项目的持续集成,YAML 文件写了三行,自认为没什么可错的,结果 pipeline 一跑就报错:No agent found in pool。折腾了两个小时,最后发现是自托管代理压根没装,代理池名字还对不上。这个报错几乎每个刚接触 Azure Pipelines 的人都会遇到——它不会告诉你该装什么、该配什么,只会给你一个毫无头绪的错误信息。Azure Pipelines 是 Azure DevOps 的核心 CI/CD 服务,支持从 GitHub、Azure Repos、Bitbucket 拉代码,在 Windows、Linux、macOS 上并行构建,把 .NET、Java、Python、Node.js、Go 这类主流生态全部覆盖。但它的配置链路很长:组织、项目、代理、服务连接、YAML 语法,任何一环断了都跑不通。这篇笔记就是按我实际装过的顺序,从版本选型、注册组织,到写第一条 YAML、配自托管代理,再到排错清单,一步步给你走完一遍。
2. 准备环境:版本选型、免费额度与注册组织
2.1 版本选型:Azure DevOps Services、Server 2022 和 TFS 怎么选
装 Azure Pipelines 之前,先要分清你用的是哪一套环境,因为注册方式、功能范围和踩坑点都不一样。
Azure DevOps Services 是微软的云服务版本,直接浏览器访问https://dev.azure.com/{你的组织名}就能用。它更新最快,YAML 管道、环境、审批这些新特性都在这个版本上最先落地。私有项目每个月有 1800 分钟(30 小时)的免费管道运行时长,公共项目可以直接免费跑管道,这个额度对个人学习和小团队试用完全够。
Azure DevOps Server 是自托管版本,对应 2019、2022 两个大版本。它适合代码必须留在内网、不允许出域的场景。注意 Server 版本里某些 YAML 功能是缺失的,比如环境的部署作业(deployment job)在 Server 2022 上支持,但部分容器作业能力不如云版本。TFS2018 属于老古董了,除非你被老项目绑死,否则不建议新装,因为很多文档里的新特性它都没有。
判断方法很简单:你的代码在 GitHub 或别人的云端仓库上,选 Services;代码只能留在公司内网,选 Server。我一般建议个人开发者直接上 Services,省掉自己维护服务器的成本。Server 版本要额外买许可证,五个活跃用户以内用 Express 版是免费的,但安装复杂度高不少。
2.2 注册与创建组织:免费启动流程
无论用 Microsoft 账户还是 GitHub 账户都能注册 Azure DevOps。用 GitHub 账户注册有个前提:如果 GitHub 邮箱关联的是支持 Azure AD 的组织,就必须改用 Azure AD 账户登录,否则会卡在授权那一步。
打开 Azure DevOps 官网,选择「免费启动」,会进入组织创建页。需要填三项:组织名称、主机位置、验证字符。组织名称就是将来访问 URL 的一部分,比如填devops-learning,最终地址就是https://dev.azure.com/devops-learning。主机位置一般选离你最近的数据中心,国内用户通常选东南亚或东亚区域,网络延迟更友好。
提示:组织名创建后可以改显示名称,但 URL 的标识部分基本定了。起名别太随意,后面所有 SSH 克隆地址、服务连接、pipeline 配置都会带上这个标识。
创建完成后系统会提示创建一个项目。项目有公共和专用两种可见性,CI/CD 学习阶段选专用就好。项目名最好和仓库名对应,后面在 pipeline 里引用时会省事。
接着用 Azure CLI 验证一下环境是否就绪:
az login az devops configure --defaults organization=https://dev.azure.com/devops-learning project=my-first-project az devops user show --org https://dev.azure.com/devops-learning第一行是登录 Azure 账户,第二行设置默认组织和项目,让后续命令不用每次都带--org参数。第三行如果返回了用户信息,说明命令行工具已经能访问你的 DevOps 组织了,这一步可以作为环境是否配通的快速自检。
2.3 代码仓库准备:GitHub 还是 Azure Repos
Azure Pipelines 支持的代码源不少:GitHub、Azure Repos Git、Bitbucket Cloud、TFVC、Subversion。绝大多数新项目用的是 GitHub 或 Azure Repos。
选 GitHub 的话,pipeline 可以从 GitHub 仓库拉代码,但需要在 Azure DevOps 里创建一个 GitHub 服务连接。选 Azure Repos 省掉服务连接这一步,因为同在一个组织里,天然打通。
从实际操作体验来说,我建议第一个 pipeline 用 Azure Repos 练手。原因有两点:一是少配一层 GitHub 授权,二是 Azure Repos 的 PR 分支策略和 pipeline 触发器的联动更直接。GitHub 集成可以等把 YAML 语法跑熟之后再加,那时候就算遇到授权问题,也知道是哪里断了。
仓库里至少要先放一个代码文件。不需要多复杂,一个 README 或者一个简单的main.py都行。因为后面创建 pipeline 时要能拉到一次代码,空仓库会导致管道创建失败。
3. 创建第一个管道:从 YAML 到第一次绿勾
3.1 两种管道形态:YAML 与经典编辑器
Azure Pipelines 提供两种定义方式:YAML 管道和经典管道。
YAML 管道的核心是一个azure-pipelines.yml文件,放在代码仓库根目录。管道定义跟着代码走,改了文件就是改了管道配置,PR 里能看到管道变更的 diff,这个特性在排查问题时非常有用。当前官方推荐的方式,所有新功能基本都优先支持 YAML。
经典管道是在网页界面上用拖拽表单的方式配置构建和发布,配置存在 Azure DevOps 服务端,不跟代码走。它也有存在价值:发布管道(Release Pipeline)里的 Gates 功能(部署前自动收集外部运行状况信号)只在经典发布管道里可用,YAML 里没有。另外任务组(Task Group)这个封装复用能力也是经典编辑器专属,YAML 里对应的是模板(Template)。
如果是新项目,直接学 YAML。但要知道经典编辑器还活着,老项目里大量存在,遇到历史管道维护时至少得能读懂。
3.2 最小可用的 azure-pipelines.yml:逐字段拆解
从空仓库开始,第一个目标不是复杂部署,而是让管道跑出一个绿勾。以下是我实际用过的最小 YAML:
trigger: - main pool: vmImage: ubuntu-latest steps: - script: echo "Hello, Azure Pipelines!" displayName: 'Print greeting' - script: | python -c "print('Python is ready')" displayName: 'Check Python'第一段trigger定义什么时候触发管道,main表示推送到 main 分支就跑。第二段pool指定代理池,这里用的是微软托管的 Ubuntu 镜像,不需要自己维护机器。第三段steps是任务列表,每个script就是一条命令,displayName是这个步骤在运行日志里显示的名字。
把默认分支改成main,然后推到远端。推完之后去 Azure DevOps 页面的 Pipelines 菜单里,应该能看到这次运行已经自动启动。点击进入运行详情,看到两个步骤都变成绿色的对勾,第一个管道就算跑通了。
这里有个细节要注意:YAML 的缩进语法比较严格,steps下的每一项都要保持相同的缩进层级。我在实际使用中因为displayName缩进多了一格导致解析失败,报错信息非常抽象,根本看不出来是缩进问题。如果遇到 YAML 解析错误,优先检查缩进和冒号后面有没有空格。
3.3 从模板克隆与自定义
页面上的「创建管道」向导其实不是从零写 YAML,而是基于仓库类型和语言生成一个模板,再让你在编辑器里改。
选择 GitHub 或 Azure Repos 仓库后,向导会扫描代码,推荐对应模板。Node.js 项目会生成安装 npm 依赖、跑测试、发布工件的模板。Python 项目会生成pip install和pytest的模板。
从模板起步的好处是,官方模板已经帮你处理了缓存、依赖安装、条件执行这些细节。比如 Node.js 模板里会有:
- task: NodeTool@0 inputs: versionSpec: '20.x' displayName: 'Install Node.js'这个NodeTool@0任务专门负责在托管代理上安装指定版本的 Node.js。versionSpec就是版本号,可以写成20.x这样的通配符。自己写的话得手动调apt-get或nvm,用任务更可靠。
模板能覆盖常见语言,但一些特殊场景要自己加任务:比如 Android 构建要装 JDK 和 Android SDK,iOS 构建必须用 macOS 代理。模板只是开始,真正生产级的管道基本都要手改。
3.4 推送触发:把第一个管道跑起来
创建完 YAML 管道后,管道会等代码推送触发。验证触发逻辑是否正常,只需要改任意一个文件提交,然后在 Pipelines 页面看运行列表里有没有新增一条记录。
如果要同时验证 GitHub 和 Azure Repos 两种仓库源,GitHub 那边需要在项目设置里创建 GitHub 服务连接。创建过程会引导跳转到 GitHub 授权页面,授权时需要选择仓库访问范围,选「仅限选定仓库」更安全,避免把整个 GitHub 账号暴露给 Azure DevOps。
注意:服务连接的名称会在 YAML 里被引用。我习惯命名为
github-read-only这种带用途的名字,比默认的GitHub connection 1直观得多。
我一般会故意把一个错误加进 YAML(比如让脚本退出码非零),确认管道确实会失败并展示红色状态,然后再修回来。这一步能同时验证日志系统、失败通知和修复流程,是花五分钟换来后面省半小时的做法。
4. 把管道做厚:阶段、作业、任务、模板与自托管代理
4.1 阶段与作业:从单步构建到多环境部署
单个管道要支持多环境部署,靠的是阶段(stage)和作业(job)两层编排。
阶段是管道里最大的逻辑分组,通常对应环境:build、test、deploy-dev、deploy-prod。默认情况下阶段按顺序执行,但可以用dependsOn控制依赖关系。作业是阶段内的执行单元,一个阶段可以跑多个作业,这些作业可以并行。
stages: - stage: build displayName: 'Build Stage' jobs: - job: compile pool: vmImage: ubuntu-latest steps: - script: echo "Compiling..." displayName: 'Compile code' - stage: deploy_dev displayName: 'Deploy to Dev' dependsOn: build condition: succeeded() jobs: - job: deploy pool: vmImage: ubuntu-latest steps: - script: echo "Deploying to dev..." displayName: 'Deploy dev'dependsOn: build表示这个阶段必须等 build 阶段跑完,condition: succeeded()表示只有前面阶段全部成功后才会执行。这两个组合是最常见的部署闸门写法。
condition是一个很关键的表达式字段。常见的取值有succeeded()、failed()、always(),还有基于变量的判断如eq(variables['build.reason'], 'PullRequest'),后者可以区分推送触发的运行和 PR 触发的运行。
多作业并行是托管代理最实用的能力。默认情况下 Azure DevOps 会给每个作业分配一个代理实例,免费额度是按分钟计的,并行作业越多消耗越快。自托管代理不会消耗 Microsoft 托管额度,这也是很多团队搭自托管代理的原因。
4.2 任务与脚本:跑命令、装依赖、发工件
任务是管道的最小执行单元。Azure Pipelines 内置了几百个任务,常见的有NodeTool@0(装 Node)、UsePythonVersion@0(选 Python 版本)、DotNetCoreCLI@2(处理 .NET 命令)、CopyFiles@2(复制文件)和PublishBuildArtifacts@1(发布工件)。
通用任务无法覆盖的场景,用script直接跑命令行、PowerShell 或 Shell。比如拉依赖和跑测试:
steps: - checkout: self - script: | python -m venv .venv source .venv/bin/activate pip install -r requirements.txt displayName: 'Install Python dependencies' - script: | source .venv/bin/activate pytest tests/ --junitxml=test-results.xml displayName: 'Run tests with pytest' - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' testResultsFiles: 'test-results.xml' displayName: 'Publish test results'多行命令用|拼接,每行一条命令。checkout: self显式拉取当前仓库代码,默认行为也会拉,但写出来更明确。PublishTestResults@2把测试结果文件发布到管道运行记录里,这样页面上能看到测试通过率、失败用例列表,比只看命令行输出直观得多。
发布工件是 CI 和 CD 的衔接点,CI 阶段产出的文件通过PublishBuildArtifacts@1保存,CD 阶段再用下载任务取回来。工件有保留期设置,默认 30 天,生产环境上会设为 90 天或更长,方便回溯历史版本。
4.3 变量、变量组与运行时参数
写死配置是管道维护的灾难源头。Azure Pipelines 的三层变量机制能避免这个问题。
第一层是管道内变量,直接在 YAML 里定义:
variables: buildConfiguration: 'Release' imageName: 'myapp:${{ parameters.tag }}'第二层是变量组(Variable Group),存在 Azure DevOps 库(Library)里,可以跨管道共享。适合存不常变的配置,比如服务器地址、告警 Webhook URL。同一个变量组可以被多个管道引用,改一处就全部生效。
第三层是密钥变量,存在变量组里并标记为密钥,运行时在日志里自动打码。数据库密码、API Key 这类敏感信息必须用这种方式,不能明文写在 YAML 里。
引用变量组的写法:
variables: - group: production-config - task: AzureCLI@2 inputs: azureSubscription: 'prod-service-connection' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | echo "Deploying to $(serverUrl)"$(serverUrl)会在运行时从production-config变量组里取值。注意 YAML 里变量引用的两种语法:$(varName)是运行时替换,模板里的${{ varName }}是模板展开时替换。前者在任务执行时解析,后者在管道编译阶段就定下来了,用错场景会导致取值不对。
运行时参数(Runtime Parameters)是更灵活的输入方式:
parameters: - name: environment displayName: 'Deployment Target' type: string default: 'dev' values: - dev - staging - prod stages: - stage: deploy_${{ parameters.environment }} displayName: 'Deploy to ${{ parameters.environment }}' jobs: - job: deploy_job steps: - script: echo "Deploying to ${{ parameters.environment }}"运行时参数可以在手动运行管道时从页面上选择,也可以被 API 调用时传入。values限制了可选范围,避免手滑部署到错误环境。我一般会给所有多环境部署管道都加这个参数,省得每次改 YAML。
4.4 模板与触发器:告别复制粘贴
两个以上的管道出现相同的任务序列时,就应该抽成模板。模板文件通常放仓库的pipelines/templates目录下,后缀还是.yml。
基础模板示例:
# pipelines/templates/build-node.yml parameters: - name: nodeVersion default: '20.x' steps: - task: NodeTool@0 inputs: versionSpec: ${{ parameters.nodeVersion }} displayName: 'Install Node ${{ parameters.nodeVersion }}' - script: | npm ci npm run test displayName: 'Install and test'引用模板的管道:
trigger: - main pool: vmImage: ubuntu-latest extends: template: pipelines/templates/build-node.yml parameters: nodeVersion: '22.x'extends是模板继承,管道本身不再写steps,全部由模板决定。另一种是steps里局部引用模板:
steps: - template: templates/build-node.yml parameters: nodeVersion: '20.x'两种方式的选择标准:管道结构和模板完全一致用extends,只有部分步骤复用用- template。模板可以嵌套,但要控制层级,三层以上排查问题就很痛苦了。
触发器除了分支触发,还有计划触发和路径触发:
trigger: branches: include: - main - release/* paths: exclude: - docs/* schedules: - cron: "0 2 * * *" displayName: 'Daily 2 AM build' branches: include: - mainbranches.include是包含模式,release/*会匹配所有 release 开头的分支。paths.exclude排除docs/*目录,文档变更不触发构建。schedules是定时触发,注意计划触发的时区默认是 UTC,国内团队用的话要减 8 小时换算。
PR 触发器在 GitHub 场景里很重要:
pr: - main这个配置让 PR 创建和更新时自动跑管道验证,防止坏代码合并进主干。
4.5 自托管代理:从下载、配置到加入代理池
微软托管代理省心但有两个硬伤:自定义软件装不了,免费分钟数有限。跑大型编译任务或者需要内网资源时,自托管代理几乎是必须的。
自托管代理的安装流程在不同操作系统上一致,以 Linux 为例:
mkdir myagent && cd myagent curl -o agent.tar.gz -L https://vstsagentpackage.azureedge.net/agent/3.x/vsts-agent-linux-x64-3.x.tar.gz tar zxvf agent.tar.gz ./config.sh --url https://dev.azure.com/devops-learning --agentpool default --token XXXXX --unattended --acceptteeeula ./run.sh第一步创建目录并下载代理包。第二步解压。第三步是配置代理,--url指向组织地址,--agentpool指定代理池名称,--token用的是个人访问令牌(PAT),需要在 Azure DevOps 用户设置里生成,至少要有 Agent Pools 的读和管理权限。--unattended表示非交互模式,--acceptteeeula接受许可协议。最后./run.sh启动代理。
代理启动后,回到 Azure DevOps 页面,在 Project Settings -> Agent Pools -> Default 里应该能看到这台代理显示为在线状态。此时 YAML 里把pool改成:
pool: name: default不再写vmImage,管道就会跑到这台自托管代理上。
提示:自托管代理的
run.sh在终端退出后就停了。生产环境要用systemd或者注册成 Windows 服务,让代理常驻后台。我一般用 systemd service 管理,同时加上Restart=always,代理进程挂了能自动拉起来。
公司网络有代理的,需要在.env文件里加VSTS_HTTP_PROXY环境变量指向代理地址,否则代理连不上 Azure DevOps。
自托管代理的代价是你要为环境负责。托管代理上自带几百个预装软件,自托管代理默认只有基础环境,缺什么装什么。我一般会在配置脚本里写好apt-get install的依赖清单,新代理从空白系统开始,跑一遍脚本就能达到可用状态。
5. 避坑与排查:五条最常见的翻车记录
5.1 YAML 解析失败,报错却看不出来原因
现象:管道启动后立刻失败,错误信息是YAML schematic validation failed或者Expecting a mapping,不告诉具体哪一行有问题。
原因:绝大多数情况是缩进错误。YAML 对缩进极其敏感,steps下的每一项必须保持层级一致,script: |多行内容里的命令如果比父级多缩进或少缩进,解析器会直接报错。另一种常见原因是冒号后面忘了加空格。
解决:先把 YAML 丢到本地编辑器里开 YAML 语法检查,配合 Azure DevOps 页面上的编辑校验提醒一边改一边看。我的习惯是本地用 VS Code 写,实时标错,比到网页上试错快得多。另外 YAML 里不要用 Tab 键缩进,统一两个空格,这基本是行业共识了。
5.2 私有项目每月 1800 分钟额度不够用
现象:月中管道就开始排队,运行记录里显示No hosted parallelism has been purchased or granted。
原因:私有项目默认每月只送 1800 分钟(30 小时),多个并行作业或者长构建任务很快就会把额度吃完。公共项目可以免费无限时跑管道,但如果代码是私有的,这个方案不可行。
解决:短期做法是把不必要的 PR 触发器关掉,减少自动运行次数。长期做法是搭自托管代理,因为自托管代理不占托管额度。我遇到一个跑 Android 构建的团队,每次构建要 25 分钟,一个月 60 多次构建,托管额度完全不够,换了自托管代理后成本直降为零。
5.3 自托管代理连不上,一直显示离线
现象:Agent Pools 里代理状态是 offline,run.sh启动时卡在Connecting to server。
原因:最常见的是公司网络出口有防火墙限制,代理需要访问https://dev.azure.com以外的多个域名,比如*.vlabs.servicebus.windows.net、*.visualstudio.com,这些域名被拦了代理就永远处于连接中状态。另一个原因是代理版本太老,和 Azure DevOps Server 版本不兼容。
解决:先确认网络策略,可以在代理机上执行curl -I https://dev.azure.com验证能不能通。代理程序的日志在_diag目录下,排查时先看这个文件。网络没问题的话,升级代理到最新版再重新config一次。
5.4 Azure 资源管理器服务连接失败
现象:AzureCLI@2或AzureResourceManagerTemplateDeployment@3任务报错,提示Failed to get Azure Resource Manager service connection,或者直接 401。
原因:服务连接(Service Connection)的认证凭证过期了,或者创建服务连接时只剩选了资源组级别的权限。常见的是把服务连接创建成了「服务主体认证」模式,但服务主体密钥过期,没有及时轮转。
解决:检查项目设置里的 Service Connections,选中对应的连接点「Edit」,走一遍重新授权流程。如果是 Azure AD 服务主体模式,去 Azure AD 重新生成客户端密钥,再回 DevOps 更新连接信息。排查时先确认连接权限范围:给资源组权限的就别去尝试列订阅级别的资源,否则会看到权限不足的假报错。
5.5 经典发布管道和 YAML 管道功能不对称
现象:照着文档写 YAML 部署,发现在 YAML 里没有 Gates 功能。看老项目里有任务组的封装,自己在 YAML 里找不到对应概念。
原因:Azure Pipelines 的两套体系功能并集才是完整能力,YAML 管道的某些能力(比如环境部署作业)是经典发布管道没有的,反之经典的 Gates 和任务组也是 YAML 没有的。
解决:新管道优先 YAML,但碰到老项目或者特定需求(比如必须用发布 Gates),就别硬在 YAML 里折腾组件。正确做法是 YAML 管道负责构建和测试,经典发布管道负责需要 Gates 的部署环节,两者通过工件衔接。架构上多一层,但每个环节用最合适的方式,比强行统一省心。
6. 装完怎么验证:一条流水线从提交到上线的完整检查
管道配置完,最后的验证不只是看页面上的绿色对勾。我从实践中沉淀了一套检查清单,照着走一遍,能确认整条链路是真的通了,而不是表面绿了。
第一项检查提交触发。改动代码推到 main 分支,去 Pipelines 页面的运行列表里看有没有新增记录。如果没触发,先看 trigger 配置里的分支名对不对,再看仓库的默认分支是不是 main。很多从 GitHub 导入的仓库默认分支是 master,YAML 里写的 main 自然不会触发。
第二项检查代理状态。跑到自托管代理上的管道,在运行详情里看 Agent 名称是不是你的那台机器。如果显示的是 Microsoft-hosted agent,说明 YAML 里pool还是默认的azure-pipelines,没切到自托管池。
第三项检查测试报告。管道步骤里加了PublishTestResults@2的,去运行页面的 Tests 标签看统计:总共多少用例、多少失败、失败的具体函数名。这里能看出测试数据有没有成功采集,比终端日志里的输出更结构化。
第四项检查工件。进入运行详情找 Artifacts 区域,确认构建产物发布了多少 MB、文件列表对不对。再点下载,解压后抽查一个文件内容是不是最新代码编译出来的。这个验证能提前发现工件没更新这种坑人的问题。
第五项检查部署结果。如果是多环境管道,走到最后确认deploy_prod阶段的日志里有明确的成功输出。我会在部署脚本最后加一条验证请求,比如curl -I打到部署后的服务地址,确认 HTTP 状态码是 200 才返回成功,否则让管道报失败。
最后的技巧是:在管道末尾加一个自检任务,把构建时间、提交 SHA、部署目标环境打出来,然后跑一次完整的 PR 验证。我的习惯是改完azure-pipelines.yml,先在本地把 YAML 缩进校验一遍,再往一个 topic 分支推一次触发完整流程,确认每个阶段都过了一遍,才敢把变更移到 main 上。这套仪式感看似笨,但已经帮我挡过多次回归事故了——最危险的不是管道失败,而是管道绿了但产品是坏的。希望这些经验能帮到你少走几次弯路。
本文还有配套的精品资源,点击获取