Azure DevOps生产级CI/CD实战:从YAML踩坑到灰度发布
1. 这不是“又一个DevOps教程”,而是一份我亲手踩过坑、改过37次YAML、在生产环境扛住日均200次部署的实战手记
你点开这篇,大概率正被三件事缠住:新项目要上线但CI/CD流程还卡在本地构建;团队里有人还在手动打包、复制DLL、改配置文件再发给测试;或者你刚被老板问“为什么上个版本回滚花了47分钟”。别急——这不是PPT式概念罗列,也不是照着微软文档抄一遍的“官方翻译”。这是我过去三年在金融、SaaS、IoT三个不同技术栈项目里,把Azure DevOps从“能用”推到“敢用生产环境”的完整复盘。核心就一条:所有配置、所有YAML写法、所有权限设置,都必须经得起凌晨三点告警电话的检验。关键词里没写“CI/CD”“YAML”“Pipeline”,但它们就是呼吸一样的存在。如果你是刚接触Azure DevOps的开发者,这篇会告诉你哪些按钮点了等于白点、哪些默认值一改就崩;如果你是带团队的技术负责人,我会拆解清楚“为什么我们放弃TFVC转向Git+分支策略”“为什么Artifacts的feed权限必须按角色隔离”“为什么Test Plans里一个测试用例的标签比代码注释还重要”。它不教你怎么点开网页,而是告诉你点下去之后,系统底层在做什么、可能卡在哪、怎么一眼看出问题根因。没有“理论上可行”,只有“我昨天刚在客户现场实测过”。
2. 整体设计逻辑:为什么是这五块拼图,而不是其他组合?
2.1 不是功能罗列,而是交付流水线的天然分段
很多人第一次看Azure DevOps的五个服务,下意识当成“五个独立工具”:Boards管事、Repos管码、Pipelines管跑、Test Plans管测、Artifacts管包。错。它们本质是软件交付价值流的五个不可分割的物理切片,就像汽车装配线上的冲压、焊装、涂装、总装、质检——少一个环节,车就出不了厂。我见过太多团队把Pipelines当“自动执行脚本工具”,结果构建失败后,开发要翻Boards找对应User Story,再进Repos查提交记录,最后去Test Plans翻测试报告,全程手动串联。真正的设计起点,必须是以一次可发布的代码变更(Code Change)为原子单位,反向倒推每个环节的输入输出契约。
- 输入契约:一次变更,必须关联一个Boards中的Work Item(比如Bug #1234或Feature #5678),这是所有后续动作的“业务上下文锚点”。没有这个关联,自动化就失去意义——你无法回答“这次部署修复了哪个客户投诉”。
- 输出契约:Pipelines的最终产物,必须是一个可验证、可追溯、可回滚的制品(Artifact),它存放在Artifacts中,其元数据(版本号、构建ID、关联的Work Item ID、触发分支)必须完整嵌入制品本身,而非仅存在Pipeline日志里。我曾为某银行项目定制过一个PowerShell脚本,在dotnet publish后自动注入
buildInfo.json到NuGet包内,内容包含{ "buildId": "20240515.12", "workItemId": "1234", "branch": "release/v2.3" },测试团队拿到包第一件事就是解压读这个文件,确认“这确实是我要测的修复版本”。
提示:Azure DevOps默认不强制Work Item关联。必须在Project Settings → Policies → Pull Request中启用“Require linked work items”,并勾选“Allow only one work item per pull request”——看似多此一举?不。它堵死了“这个PR顺手修了三个Bug但只关了一个”的混乱源头。
2.2 为什么是Git而非TFVC?一次血泪教训
Azure Repos支持Git和TFVC,但我的建议非常明确:除非你维护一个15年前的.NET Framework 2.0遗留系统且团队拒绝学习Git,否则一律用Git。原因不在技术优劣,而在协作熵增定律。TFVC是集中式版本控制,所有操作依赖中央服务器。我在一个制造业MES项目吃过亏:开发A在上午10点签出Config.xml,修改了数据库连接字符串;开发B在10:05也签出同一文件,改了日志级别;两人下午3点同时签入。TFVC的合并机制是“谁后签入谁覆盖”,B的修改直接抹掉A的数据库配置,系统下午4点上线后连不上Oracle。Git的分布式特性强制每个开发者本地有完整历史,合并时必须显式解决冲突,错误在本地即暴露。更重要的是,Pipelines的触发机制深度绑定Git语义:pr触发器监听Pull Request创建/更新,push触发器监听分支推送,tag触发器监听打标行为。TFVC的“Shelveset”或“Check-in”事件无法提供同等粒度的上下文(比如无法区分“这是紧急Hotfix还是常规迭代”)。
注意:迁移到Git不是简单
git init。必须同步迁移分支策略。我们采用“GitFlow 2.0”变体:main(仅接收已验证的Release Tag)、develop(集成分支,所有Feature PR合入目标)、feature/*(每人一个,命名含Jira ID如feature/JIRA-789-login-ui)、hotfix/*(仅限线上紧急修复)。关键在于,main分支开启Branch Policy:要求至少2人Code Review + Build Validation(运行Pipelines)+ Status Check(Test Plans中该版本通过率≥95%)才能合入。这套规则在Azure Repos的Branches页面右键分支→Branch policies中配置,不是口头约定。
2.3 Pipelines为何是心脏?因为它定义了“可重复性”的数学边界
Azure Pipelines常被简化为“写YAML跑命令”,但它的真正价值在于将软件构建过程从“艺术”转化为“可计算的确定性函数”。一个合格的Pipeline,输入是代码仓库的某个Commit ID,输出是带唯一标识的制品,中间每一步(编译、单元测试、静态扫描、打包)都必须满足:
- 幂等性:相同输入,无论何时何地执行,输出完全一致。这意味着不能依赖本地全局安装的Node.js版本(
node -v可能返回16.x或18.x),而必须在YAML中声明nodeVersion: '18.x',由Microsoft-hosted agent自动安装指定版本。 - 隔离性:每次运行都在干净虚拟机(Windows/Linux/macOS)上启动,无残留状态。所以不要幻想“在Step A里
npm install,Step B里直接用node_modules”——agent重启后目录全空。所有依赖必须在每个需要它的Step里重新安装,或通过Cache@2任务缓存node_modules(需正确配置key和restoreKeys)。 - 可观测性:每个Step的执行时间、内存占用、CPU峰值、网络IO必须可量化。我们给所有Pipeline添加了
PublishTestResults@2和PublishCodeCoverageResults@2,并将覆盖率阈值设为硬性门禁(coverageThresholdForBlockBuild: 75),低于75%的构建直接失败。这不是为了好看,而是当某天发现覆盖率从82%跌到78%,你能立刻定位是哪个PR引入了未覆盖的if分支。
3. 核心细节解析:从零搭建一条生产级CI/CD流水线
3.1 Azure Boards:让需求不再“消失在邮件里”
Boards的价值常被低估,以为只是“在线看板”。实际它是整个交付链路的唯一真相源(Source of Truth)。我坚持一个原则:任何需求、缺陷、技术债,必须以Work Item形式存在于Boards,否则视为不存在。具体落地有三个硬性动作:
Work Item Type定制化:默认的User Story、Bug、Task不够用。我们新增了
TechDebt类型,字段包含ImpactScore(1-5分,影响范围)、EffortEstimate(人天)、Owner(必须指定到人)。当ImpactScore ≥ 4且EffortEstimate ≤ 2时,自动加入Sprint Backlog——避免技术债越积越多。配置路径:Project Settings → Work → Project configuration → Add work item type。Backlog层级强制对齐:禁止平铺式管理。采用三级结构:Epic(季度目标,如“提升API响应速度”)→ Feature(月度交付,如“实现Redis缓存层”)→ User Story(双周迭代,如“用户登录接口增加缓存”)。每个Feature必须关联至少一个Epic,每个User Story必须关联一个Feature。这样,当老板问“Q3重点是什么”,直接导出Epic报表;当测试问“这个Story属于哪个大功能”,点开就能看到父级Feature。
Sprint Planning的自动化校验:每次Sprint Planning会议前,运行一个自定义Query(Boards → Queries → New query):
SELECT [System.Id], [System.Title], [System.State], [Microsoft.VSTS.Scheduling.Effort] FROM WorkItems WHERE [System.WorkItemType] = 'User Story' AND [System.State] = 'To Do' AND [System.IterationPath] = @currentIteration AND [Microsoft.VSTS.Scheduling.Effort] > 0 ORDER BY [Microsoft.VSTS.Scheduling.Effort] DESC这个Query强制显示所有未估点、未分配到当前Sprint的Story。会议开始前5分钟,我把结果投屏——没人能说“这个Story我忘了估点”。流程的严肃性,始于数据的可见性。
3.2 Azure Repos:代码仓库不是“文件夹”,而是“契约签署地”
Repos的配置直接影响Pipeline的稳定性和安全性。以下是经过生产验证的关键设置:
分支保护策略(Branch Policies):对
main和develop分支,必须启用:- Build validation:指定一个专用Pipeline(如
ci-main-validation.yml),该Pipeline只做最精简的检查:dotnet restore+dotnet build+dotnet test --no-build(跳过重复编译)。耗时控制在3分钟内。任何PR合入前,此Pipeline必须成功。 - Required reviewers:至少2名非作者的Reviewer,且其中1名必须是
[Project Valid Users]组成员(避免小圈子审核)。 - Check for linked work items:强制关联Work Item,且状态不能是
Done(防止“先关Story再合代码”的倒置操作)。 - Limit merge types:仅允许
Squash merge。理由:Rebase merge会重写提交历史,导致Pipeline的Commit ID与原始PR不一致,溯源困难;No fast-forward merge产生大量无意义merge commit,污染历史。
- Build validation:指定一个专用Pipeline(如
文件路径权限(Permissions):敏感文件如
appsettings.Production.json、azure-pipelines.yml,必须设置路径级权限。右键文件 → Security → 添加组[Project Administrators],权限设为Deny(拒绝)Contribute。普通开发者只能读,修改必须通过PR + 高权限审批。我们曾因azure-pipelines.yml被误删导致所有Pipeline中断,恢复花了42分钟——现在,这种事故归零。Git Hooks替代方案:虽然Repos不支持服务端Git Hooks,但可用Pipeline前置步骤模拟。在
azure-pipelines.yml开头添加:trigger: branches: include: - main - develop pr: branches: include: - main - develop jobs: - job: PreCheck pool: vmImage: 'ubuntu-latest' steps: - checkout: self fetchDepth: 1 - script: | # 检查是否包含危险模式 if git grep -q "console\.log" -- "**/*.js"; then echo "##vso[task.logissue type=error]Found console.log in JS files. Remove before merge." exit 1 fi if git grep -q "password.*=" -- "**/*.cs"; then echo "##vso[task.logissue type=error]Found password assignment in C# files. Use secrets instead." exit 1 fi displayName: 'Pre-merge static checks'这段脚本在每次PR触发时,扫描JS文件中的
console.log和C#文件中的明文密码赋值,发现即报错阻断。它比客户端Hook更可靠,因为无法绕过。
3.3 Azure Pipelines:YAML不是配置,而是“可执行的架构文档”
Pipelines的YAML文件,是我团队内部称为“活文档”的核心。它必须清晰回答:这个应用如何被构建、测试、发布?以下是我们生产环境的标准结构(以.NET Core Web API为例):
# azure-pipelines.yml trigger: branches: include: - main - develop - feature/* tags: include: - v* pr: branches: include: - main - develop variables: # 全局变量,避免硬编码 solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' # 从Repository变量组注入,不写死 - group: 'prod-secrets' # 包含SQL_CONN_STRING, APPINSIGHTS_KEY等 stages: - stage: Build displayName: 'Build and Test' jobs: - job: BuildJob pool: vmImage: 'windows-latest' steps: - task: DotNetCoreCLI@2 displayName: 'Restore dependencies' inputs: command: 'restore' projects: '$(solution)' - task: DotNetCoreCLI@2 displayName: 'Build solution' inputs: command: 'build' projects: '$(solution)' arguments: '--configuration $(buildConfiguration) --no-restore' - task: DotNetCoreCLI@2 displayName: 'Run unit tests' inputs: command: 'test' projects: '**/*Tests.csproj' arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"' publishTestResults: true - task: PublishCodeCoverageResults@2 displayName: 'Publish code coverage' inputs: codeCoverageTool: 'Cobertura' summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml' - stage: Package displayName: 'Package for Release' dependsOn: Build condition: succeeded() jobs: - job: PackageJob pool: vmImage: 'windows-latest' steps: - checkout: self fetchDepth: 1 - task: DotNetCoreCLI@2 displayName: 'Publish web app' inputs: command: 'publish' projects: '**/MyApi.csproj' arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/publish --no-restore' - task: PublishBuildArtifacts@1 displayName: 'Publish artifacts' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/publish' ArtifactName: 'drop' publishLocation: 'Container' - stage: Deploy displayName: 'Deploy to Environments' dependsOn: Package condition: succeeded() jobs: - deployment: DeployToDev displayName: 'Deploy to Development' pool: vmImage: 'windows-latest' environment: 'Development' strategy: runOnce: deploy: steps: - download: current artifact: 'drop' - task: AzureRmWebAppDeployment@4 displayName: 'Deploy to Azure Web App (Dev)' inputs: ConnectionType: 'AzureRM' azureSubscription: 'my-dev-subscription' appType: 'webApp' WebAppName: 'myapi-dev' packageForLinux: '$(Pipeline.Workspace)/drop/**/*.zip' - deployment: DeployToProd displayName: 'Deploy to Production' pool: vmImage: 'windows-latest' environment: 'Production' # 关键:生产部署需人工审批 strategy: runOnce: deploy: steps: - download: current artifact: 'drop' - task: AzureRmWebAppDeployment@4 displayName: 'Deploy to Azure Web App (Prod)' inputs: ConnectionType: 'AzureRM' azureSubscription: 'my-prod-subscription' appType: 'webApp' WebAppName: 'myapi-prod' packageForLinux: '$(Pipeline.Workspace)/drop/**/*.zip'关键设计解析:
- Stage化而非单Job:
Build、Package、Deploy分离,便于独立重试。若部署失败,无需重新构建。 - Environment绑定:
environment: 'Production'不仅是个名字,它关联Azure DevOps中的Environment资源,可配置Approval Checks(需指定人员审批)、Checks(如运行Smoke Test Pipeline)、Audit Logs(谁在何时部署了什么)。 - Condition控制流:
condition: succeeded()确保前一Stage成功才执行,避免“构建失败还继续部署”。 - Artifact传递:
download: current明确指定从当前Pipeline下载制品,而非跨Pipeline引用,保证版本一致性。
实操心得:YAML语法错误是新手最大拦路虎。推荐两个技巧:1)在VS Code安装“Azure Pipelines”插件,实时语法高亮和智能提示;2)所有复杂Pipeline,先在本地用
az pipelines run命令测试(需安装Azure CLI和az devops扩展),比在Web界面反复提交快10倍。
3.4 Azure Test Plans:测试不是“找Bug”,而是“建立质量信心”
Test Plans常被当作“手工测试记录本”,但它的核心价值是将测试活动与代码变更、构建结果、业务需求进行三维绑定。我们强制执行以下实践:
测试用例(Test Case)与User Story强关联:每个User Story创建时,必须关联至少3个Test Case(正向、边界、异常)。在Boards中打开Story → “Test cases”选项卡 → “Link to test case”。这样,当Story状态变为
In Progress,Test Plans自动将关联的Test Case状态设为Ready;当Story完成,Test Case进入Design阶段。测试不再是开发后的补救,而是需求定义时的同步产出。测试套件(Test Suite)按环境组织:创建
Suite-Dev、Suite-Staging、Suite-Prod。Suite-Prod只包含已通过UAT的Test Case,且每个Case必须标记Priority: P0(最高优先级)。每次生产部署前,必须运行Suite-Prod,通过率100%才允许上线。我们用PowerShell脚本自动触发:$token = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:SYSTEM_ACCESSTOKEN)")) $header = @{Authorization = "Basic $token"} $body = @{ plan = @{id = "12345"} # Prod Test Plan ID suite = @{id = "67890"} # Suite-Prod ID configuration = @{id = "111"} # Prod Environment Config ID tester = @{displayName = "CI-System"} } | ConvertTo-Json Invoke-RestMethod -Uri "https://dev.azure.com/{org}/{project}/_apis/test/runs?api-version=6.0" ` -Method Post -Headers $header -Body $body -ContentType "application/json"此脚本嵌入Deploy Stage末尾,部署完成后自动执行冒烟测试。
探索性测试(Exploratory Testing)不等于“随便点点”:我们要求测试工程师使用Test Plans的Exploratory Testing Session功能,每次Session必须:1)选择一个明确的User Story作为目标;2)录制屏幕和操作日志;3)Session结束时,必须生成至少1个Bug Work Item(即使没发现Bug,也要记录“验证通过”)。这迫使探索性测试聚焦于业务价值,而非随机点击。
3.5 Azure Artifacts:包管理不是“上传下载”,而是“供应链治理”
Artifacts是交付链路的“物流中心”。我们将其定位为团队级依赖供应链的治理节点,而非简单的二进制存储。关键实践:
Feed分层策略:创建三个Feed:
public-nuget:只读,上游源为nuget.org,供所有项目拉取公共包。internal-shared:团队共享,存放公司通用库(如Company.Logging、Company.Data),发布权限仅限[Library Owners]组。project-specific:每个重大项目独享,存放该项目独有的NuGet包(如MyApi.Contracts),发布权限仅限该项目组。
这样,public-nuget的包版本升级不会影响内部库,internal-shared的变更需走严格PR流程,project-specific可快速迭代。
包版本语义化(SemVer)强制:所有内部包必须遵循
MAJOR.MINOR.PATCH规则。在csproj中:<PropertyGroup> <VersionPrefix>2.3.0</VersionPrefix> <VersionSuffix>$(BUILD_BUILDNUMBER)</VersionSuffix> <!-- 构建号追加 --> </PropertyGroup>Pipeline中
dotnet pack后,包版本为2.3.0.20240515.12。VersionPrefix由开发手动维护,VersionSuffix由Pipeline注入,确保唯一性。没有版本号的包,等同于没有许可证的药品——不准入库。依赖扫描(Dependency Scanning)集成:在Build Stage末尾添加:
- task: DotNetCoreCLI@2 displayName: 'Scan for vulnerable packages' inputs: command: 'custom' custom: 'tool' arguments: 'install --global dotnet-format' - script: | dotnet tool install -g Microsoft.CST.Scanner dotnet cscanner scan --source-path $(Build.SourcesDirectory) --output-format json --output-file $(Build.ArtifactStagingDirectory)/scan-results.json displayName: 'Run dependency scanner' - task: PublishBuildArtifacts@1 displayName: 'Publish scan results' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/scan-results.json' ArtifactName: 'security-scan' publishLocation: 'Container'扫描结果自动上传,安全团队可每日查看。发现高危漏洞(如Log4j)立即阻断Pipeline。
4. 实操过程:从创建第一个Pipeline到生产环境灰度发布
4.1 第一步:创建项目与基础配置(15分钟)
- 登录dev.azure.com → New project → 命名(如
MyBankingApp)→ Visibility设为Private→ Version control选Git→ Work item process选Agile(非Basic,因Basic缺少Epic/Feature层级)。 - 进入Project Settings → Repositories →
MyBankingApp→ Policies → 启用Require a minimum number of reviewers(2人)、Check for linked work items。 - 进入Project Settings → Pipelines → Settings → 关闭
Disable creation of classic build pipelines(避免新人误用旧版)。 - 创建第一个YAML文件:在Repos根目录新建
.pipelines/ci-base.yml,粘贴前述BuildStage模板,保存并Push。注意:首次Push后,Pipelines会自动检测YAML并创建Pipeline。若未触发,在Pipelines页面点“New pipeline” → “Use the classic editor” → “Existing Azure Pipelines YAML file”,手动选择路径。
4.2 第二步:连接代码与构建(30分钟)
- 在本地克隆仓库:
git clone https://dev.azure.com/{org}/{project}/_git/{repo}。 - 创建
src/MyBankingApp.sln和src/MyBankingApp/MyBankingApp.csproj(标准.NET Core Web API模板)。 - 修改
azure-pipelines.yml,将solution变量改为'src/MyBankingApp.sln',projects参数改为'src/MyBankingApp/MyBankingApp.csproj'。 - 提交并Push:
git add . && git commit -m "init: add basic pipeline" && git push origin main。 - 观察Pipeline运行:进入Pipelines → 点击刚创建的Pipeline → 查看日志。常见失败点:
dotnet restore失败:检查global.json中SDK版本是否与agent支持的匹配(windows-latest支持.NET 6/7/8,不支持5.0)。dotnet test失败:确认**/*Tests.csproj能匹配到测试项目(如项目名为MyBankingApp.Tests.csproj,则需改为'**/MyBankingApp.Tests.csproj')。- 超时:默认超时10分钟,若
dotnet build耗时过长,添加timeoutInMinutes: 20到Job级别。
4.3 第三步:接入测试与制品管理(45分钟)
- 在Repos中创建
tests/MyBankingApp.Tests/MyBankingApp.Tests.csproj,添加xUnit和coverlet.collectorNuGet包。 - 在
azure-pipelines.yml的BuildStage中,修改DotNetCoreCLI@2的test步骤:- task: DotNetCoreCLI@2 displayName: 'Run unit tests with coverage' inputs: command: 'test' projects: 'tests/MyBankingApp.Tests/MyBankingApp.Tests.csproj' arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage" --results-directory $(Build.ArtifactStagingDirectory)/testresults' publishTestResults: true - 创建Artifacts Feed:Project Settings → Artifacts → New feed → Name
mybanking-shared→ VisibilityOrganization→ 保存。 - 修改
PackageStage,添加dotnet pack和push步骤:- task: DotNetCoreCLI@2 displayName: 'Pack library' inputs: command: 'pack' packagesToPack: 'src/MyBankingApp/MyBankingApp.csproj' versioningScheme: 'byPrereleaseNumber' majorMinorPatch: '2.3.0' - task: DotNetCoreCLI@2 displayName: 'Push to Artifacts' inputs: command: 'push' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' nuGetFeedType: 'internal' feedPublish: 'mybanking-shared'
4.4 第四步:生产环境部署与灰度发布(60分钟)
- 在Azure Portal创建两个Resource Group:
rg-mybanking-dev和rg-mybanking-prod。 - 在DevOps中创建Environments:Pipelines → Environments → New environment → Name
Development→ Resource typeAzure Virtual Machines(或Kubernetes namespace,此处以Web App为例)→ Connect to Azure → 选择rg-mybanking-dev。重复创建Production环境。 - 为
Production环境添加Approval:点击Production→ Approvals and checks → + → Approval → 选择2名运维负责人。 - 修改
DeployStage,添加ProductionDeployment:- deployment: DeployToProd displayName: 'Deploy to Production (Gray Scale)' pool: vmImage: 'windows-latest' environment: 'Production' strategy: # 灰度策略:先部署5%流量 runOnce: preDeploy: steps: - script: echo "Switching production traffic to 5% for new version" deploy: steps: - download: current artifact: 'drop' - task: AzureRmWebAppDeployment@4 displayName: 'Deploy to Azure Web App (Prod)' inputs: ConnectionType: 'AzureRM' azureSubscription: 'my-prod-subscription' appType: 'webApp' WebAppName: 'mybanking-prod' packageForLinux: '$(Pipeline.Workspace)/drop/**/*.zip' postDeploy: steps: - script: echo "Monitoring metrics for 10 minutes..." - script: | # 调用监控API检查错误率<0.1% if [ $(curl -s "https://api.monitoring.com/metrics?app=mybanking&metric=error_rate&window=10m" | jq '.value') -gt 1 ]; then echo "##vso[task.logissue type=error]Error rate too high. Rolling back." exit 1 fi - 提交YAML,触发Pipeline。观察
Production环境的Approval卡片,等待审批通过后,灰度部署自动执行。
5. 常见问题与排查技巧实录:那些让我凌晨三点爬起来的日志
5.1 Pipeline卡在“Waiting for an agent” —— 不是没机器,是没权限
现象:Pipeline长时间显示“Waiting for an agent”,队列无报错。
根因:Agent Pool权限未授予。新创建的Pipeline默认使用Azure Pipelines托管池,但若项目启用了私有Agent(如公司内网VM),需手动授权。
排查:
- 进入Project Settings → Pipelines → Agent pools → 选择你的私有Pool → “Security”选项卡。
- 检查
[Project Name] Build Service组是否有Use permissions(允许使用)。若为Not set,点击...→Edit→ 勾选Use。
避坑:永远不要给Project Collection Build Service组授予权限,它权限过大。只给项目级Build Service。
5.2 “The nuget command failed with exit code 1” —— 不是包不存在,是源没配对
现象:dotnet restore失败,日志显示Unable to load the service index for source https://api.nuget.org/v3/index.json。
根因:Pipeline运行在Microsoft-hosted agent上,但nuget.config文件中配置了公司内网私有源,且未配置凭据。
解决:
- 在Repos根目录创建
nuget.config:<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> <add key="mycompany" value="https://pkgs.dev.azure.com/{org}/_packaging/{feed}/nuget/v3/index.json" /> </packageSources> <packageSourceCredentials> <mycompany> <add key="Username" value="devops" /> <add key="ClearTextPassword" value="$(NUGET_PAT)" /> <!-- 从变量组注入 --> </mycompany> </packageSourceCredentials> </configuration> - 在Pipeline变量组中创建
NUGET_PAT,值为Personal Access Token(Scope:Packaging (read))。
注意:ClearTextPassword虽名含“ClearText”,但Token本身是密文,且变量组已加密存储。
5.3 Test Plans中“Test case not found” —— 不是链接失效,是ID变了
现象:在Test Plans中点击“Run”按钮,提示“Test case not found”。
根因:Work Item被移动(如从feature迭代移至main迭代),导致Test Case的Iteration Path变更,但Test Plans未同步。
解决:
- 在Test Plans → Test suites → 右键对应Suite → “Manage test cases”。
- 点击“Refresh”按钮(循环箭头图标),强制从Boards重新拉取最新Work Item状态。
预防:在Boards中移动Work Item时,勾选“Move associated test cases”(移动关联的测试用例)。
5.4 Artifacts包上传成功但下游项目找不到 —— 不是网络问题,是Feed权限
现象:dotnet push成功,但在另一个项目中dotnet restore报401 Unauthorized。
根因:目标项目未被授权访问该Feed。
解决:
- 进入Artifacts → 选择Feed → “Permissions”选项卡。
- 点击
+→ “Add users/groups” → 输入目标项目名称(如OtherProject)→ 选择角色Reader(只读)或Contributor(可读写)。
关键:这里添加的是“项目”(Project),不是“用户”。项目名格式为{org}/{project}。
5.5 Pipeline部署后应用500错误 —— 不是代码问题,是配置缺失
现象:Azure Web App部署成功,但访问返回HTTP 500。
排查清单:
- 检查应用日志:Portal → Web App → Monitoring → App Service logs → 开启
Application Logging (Filesystem)→ 保存 → 重启App → 再访问 → 下载日志查看eventlog.xml。 - 检查连接字符串:Portal → Web App → Settings → Configuration → Connection strings → 确认
SQLCONNSTR_defaultConnection等名称与代码中Configuration.GetConnectionString("defaultConnection")完全一致(大小写敏感)。 - 检查启动命令:Portal → Web App → Settings → Configuration → General settings → Startup command → 对于.NET Core,应为空(由
web.config或dotnet publish自动生成);若手动填写,必须为dotnet MyBankingApp.dll。 - 检查托管身份:若代码中使用
new DefaultAzureCredential()获取Key Vault密钥,需在Portal → Web App → Settings → Identity → 状态设为On,并在Key Vault中为该Web App的托管身份授予权限。
实操心得:我建立了一个“5分钟故障树”:遇到500错误,按顺序执行:1)看App Service日志;2)看Deployment Center的部署日志(