12.4 Modernizing CI/CD: Classic to YAML Migration
Key Takeaways
- Migrating from Classic visual pipelines to YAML pipelines enables Pipeline as Code (PaC), granting full version control, branch isolation, pull request peer reviews, and disaster recovery.
- Classic architecture separates Build (CI) and Release (CD) into distinct interfaces, whereas YAML unifies build, validation, and multi-stage release deployments into a single schema.
- Classic Task Groups are replaced in YAML by modular Step Templates and Job Templates, enabling parameterized reuse and centralized compliance enforcement across enterprise repositories.
- Classic Pre-Deployment and Post-Deployment Gates are translated into YAML Environment Checks & Approvals (Manual Approvals, Business Hours, REST API checks, and Azure Monitor Alerts).
- Migration mechanics utilize the 'View YAML' feature in the Classic task editor, systematic Variable Group integration, and explicit secret variable environment mapping (env:).
12.4 Modernizing CI/CD: Classic to YAML Migration
For years, Azure DevOps (and its predecessors TFS and VSTS) relied on visual web designer interfaces: Classic Build Pipelines for continuous integration and Classic Release Pipelines (Pipelines → Releases) for continuous delivery. While the graphical canvas was accessible, it created severe enterprise challenges: pipeline definitions were stored as opaque JSON blobs in internal databases, changes could not be peer-reviewed in pull requests, branch-specific delivery logic was cumbersome, and disaster recovery required tedious manual re-creation.
Modern enterprise DevOps mandates Pipeline as Code (PaC) using unified YAML Multi-Stage Pipelines. On the AZ-400 exam, candidates must design and execute migration strategies to convert legacy Classic build and release definitions into production-grade YAML pipelines, translate Classic Task Groups into templates, and map graphical release gates to YAML Environment Checks & Approvals.
1. Why Migrate: Strategic Drivers of Pipeline as Code
Migrating to YAML is not merely a syntax change; it represents an architectural transformation in software delivery governance.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ CLASSIC VS. YAML ARCHITECTURAL DRIVERS │
├──────────────────────────┬──────────────────────────┬───────────────────────────┤
│ Capability Dimension │ Classic Visual Pipelines │ Modern YAML Multi-Stage │
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Storage & Versioning │ Internal system database │ Git repository alongside │
│ │ (opaque JSON revisions) │ source code (.azure-pipelines)│
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Pull Request Governance │ None; visual edits apply │ Full PR review workflow; │
│ │ immediately to all runs │ branch policies enforce check│
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Branch Isolation │ Single pipeline for all │ Feature branches can test │
│ │ branches; hard to branch │ modified pipeline logic │
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Disaster Recovery │ Difficult export/import; │ Trivially cloned or │
│ │ visual re-wiring needed │ recreated from git commit │
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Build & Release Cohesion │ Fragmented UI: Build vs │ Unified end-to-end stages │
│ │ Release in separate tabs │ in a single YAML pipeline │
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ Reusability Model │ Classic Task Groups │ Parameterized YAML Step & │
│ │ (inflexible, UI-managed) │ Job Templates in Git │
└──────────────────────────┴──────────────────────────┴───────────────────────────┘
Branch-Specific Pipeline Testing
In Classic pipelines, if an engineer needed to test a new compiler version or deployment script, editing the visual pipeline affected every build across the organization immediately. In contrast, with YAML:
- A developer creates a git branch
feature/upgrade-dotnet-9. - In that branch, they edit
azure-pipelines.ymlto useDotNetCoreCLI@2with .NET 9 SDK. - The pipeline validation runs the modified pipeline logic exclusively for that branch, completely isolating trunk (
main) from experimental disruptions. - When validated, the pipeline modification is reviewed and merged via a Pull Request.
2. Key Architectural Differences: Classic vs. YAML
Understanding how visual constructs map to YAML hierarchical objects is fundamental to successful migration.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ CLASSIC ARCHITECTURE: SPLIT ENGINES │
│ │
│ [Classic Build (CI)] [Classic Release (CD)] │
│ • Web Canvas • Pipelines -> Releases Canvas │
│ • Agent Phase • Stages (Dev, QA, Prod) │
│ • Tasks & Task Groups • Pre/Post Deployment Approvals │
│ • Triggers: CI Push • Artifact Triggers (After Build) │
│ │
│ │ MIGRATION TO │
│ ▼ UNIFIED ENGINE │
│ │
│ YAML MULTI-STAGE ARCHITECTURE: UNIFIED PIPELINE │
│ │
│ azure-pipelines.yml │
│ • stage: BuildAndTest (CI) │
│ jobs: [ job: Compile, job: UnitTest ] │
│ • stage: DeployQA (CD) │
│ jobs: [ deployment: DeployWeb, environment: 'QA' ] │
│ • stage: DeployProduction (CD) │
│ jobs: [ deployment: DeployWeb, environment: 'Production' ] │
│ └── Approvals & Checks Governed on Environment Object │
└─────────────────────────────────────────────────────────────────────────────────┘
The Unified Execution Model
- Classic Model: Required a Build Pipeline to compile and publish artifacts, followed by an independent Release Pipeline linked to the build artifact via Continuous Deployment (CD) triggers.
- YAML Multi-Stage Model: Combines CI and CD into a single file structured into Stages (
stages:), Jobs (jobs:), and Steps (steps:). Dependencies between stages are declared usingdependsOn:and conditional execution usingcondition:.
3. Practical Migration Mechanics & Step-by-Step Translation
Migrating complex enterprise pipelines requires a disciplined, phased approach rather than attempting a single large-scale cutover.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ FIVE-PHASE PIPELINE MIGRATION ROADMAP │
├──────────────────────────┬──────────────────────────────────────────────────────┤
│ Phase │ Core Engineering Actions │
├──────────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 1: Inventory & │ • Catalog all Classic builds, releases, task groups │
│ Assessment │ • Audit service connections, secure files, and vars │
├──────────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 2: Task Group to │ • Convert reusable Task Groups into YAML templates │
│ Template Conversion │ • Store templates in a centralized git repository │
├──────────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 3: CI Pipeline │ • Export Classic tasks via 'View YAML' │
│ Translation │ • Author azure-pipelines.yml in feature branch │
│ │ • Validate PR triggers and artifact publishing │
├──────────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 4: CD & Release │ • Create Azure DevOps Environments │
│ Environment Migration │ • Configure Checks, Approvals, and Gates on Envs │
│ │ • Author deployment jobs with runOnce/canary │
├──────────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 5: Cutover & │ • Run Classic and YAML in parallel for validation │
│ Decommissioning │ • Disable Classic triggers; archive definitions │
└──────────────────────────┴──────────────────────────────────────────────────────┘
Step 1: Exporting Classic Tasks via "View YAML"
Azure DevOps provides an automated bridge for task syntax:
- Open the Classic Build or Release definition in the web edit canvas.
- Select an individual task in the phase.
- Click the View YAML button in the upper right corner of the task panel.
- Copy the generated YAML snippet. The snippet contains the exact task name, version
@vX, and parameter inputs configured in the web UI.
[!CAUTION] Exam Trap: Clicking "Export to JSON" on the entire Classic pipeline exports the internal REST API JSON representation, not valid YAML. You cannot import this JSON into YAML pipelines. Use "View YAML" at the task level or rebuild the structure using templates.
Step 2: Translating Classic Task Groups into YAML Templates
In Classic pipelines, repeated sequences of tasks were packaged as Task Groups. In YAML, Task Groups are converted to Step Templates:
# File: templates/build-and-test-steps.yml (Replaces Classic Task Group)
parameters:
- name: buildConfiguration
type: string
default: 'Release'
- name: runTests
type: boolean
default: true
steps:
- task: DotNetCoreCLI@2
displayName: 'Compile Application'
inputs:
command: 'build'
arguments: '--configuration ${{ parameters.buildConfiguration }}'
- ${{ if eq(parameters.runTests, true) }}:
- task: DotNetCoreCLI@2
displayName: 'Execute Unit Tests'
inputs:
command: 'test'
arguments: '--configuration ${{ parameters.buildConfiguration }} --no-build'
Consuming this template in the main pipeline:
# Main azure-pipelines.yml
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- template: templates/build-and-test-steps.yml
parameters:
buildConfiguration: 'Release'
runTests: true
Step 3: Translating Variables and Secret Variables
In Classic pipelines, variables were defined in the Variables tab or linked via Variable Groups.
- Variable Groups: Directly supported in YAML using the
group:syntax:variables: - group: CoreBanking-VariableGroup - name: applicationName value: 'ContosoCoreBank' - Secret Variables (Critical Exam Concept!): In Classic pipelines, secret variables were automatically accessible in scripts as
$(SecretVar). In YAML, secret variables are intentionally not decrypted or mapped to environment variables automatically. You must explicitly map secrets in theenv:block:- script: | echo "Connecting with API Key..." python deploy.py --key $SERVICE_API_KEY displayName: 'Execute Deployment Script' env: # Explicit mapping mandatory for secret variables in YAML SERVICE_API_KEY: $(SecretApiKey)
4. Translating Classic Features to YAML Equivalents Matrix
| Classic Visual Construct | YAML Multi-Stage Equivalent | Configuration Location |
|---|---|---|
| Classic Build Phase | job: JobName | Defined in azure-pipelines.yml |
| Classic Server Phase | job: ServerJob with pool: server | Run on Azure DevOps serverless compute |
| Task Group | Parameterized Step or Job Template | Stored in .yml file in Git repository |
| Continuous Integration Trigger | trigger: branches: include: ... | Defined in azure-pipelines.yml |
| Continuous Deployment Trigger | stages: with dependsOn: and resources: pipelines: | Defined in azure-pipelines.yml |
| Pre-Deployment Manual Approval | Manual Approvals check on Environment | Configured in Project Settings → Environments |
| Pre-Deployment Automated Gate | Invoke REST API / Azure Monitor Alerts | Configured in Project Settings → Environments |
| Deployment Group (VM Targets) | Environment with Virtual Machine resources | Installed agent tags linked to Environment |
| Artifact Link / Download | download: current or download: pipelineResource | Defined in deployment job steps |
| Deployment Strategy | strategy: runOnce, rolling, or canary | Defined in deployment: job block |
5. Complete Multi-Stage YAML Pipeline Example
Below is a complete, production-grade multi-stage YAML definition demonstrating how a Classic Build + Classic Release pipeline is unified into a modern pipeline with approval gates:
trigger:
branches:
include:
- main
- releases/*
variables:
- group: Enterprise-ServiceConnections
- name: buildConfiguration
value: 'Release'
stages:
# STAGE 1: CI BUILD & PACKAGE
- stage: BuildAndPackage
displayName: 'Build & Package Artifacts'
jobs:
- job: CompileJob
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
fetchDepth: 1
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
# STAGE 2: CONTINUOUS DELIVERY TO STAGING
- stage: DeployStaging
displayName: 'Deploy to Staging Environment'
dependsOn: BuildAndPackage
condition: succeeded()
jobs:
- deployment: DeployWebStaging
displayName: 'Deploy Web App to Staging'
environment: 'Staging' # Pre-deployment gates configured on Environment
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- script: echo "Deploying drop artifact to Staging App Service..."
# STAGE 3: PRODUCTION CD WITH GOVERNANCE GATES
- stage: DeployProduction
displayName: 'Deploy to Production Environment'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployWebProduction
displayName: 'Deploy Web App to Production'
environment: 'Production' # Manual approvers, ServiceNow REST check, and Alerts
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- script: echo "Deploying drop artifact to Production App Service..."
6. Realistic Exam Scenario & Common Traps
Scenario: Regulated Insurance Core Platform Modernization
Context: Great Northern Insurance operates 120 Classic Build pipelines and 35 Classic Release pipelines. Each release pipeline contains identical pre-deployment gates:
- A manual approval sign-off by the Lead Cloud Architect.
- An automated REST API check to their internal ServiceNow Change Management portal to verify an approved CAB change request.
- Shared deployment steps for database migrations managed via a Classic Task Group.
The CIO mandates migrating the entire estate to YAML pipelines to enforce pull request peer reviews for all deployment changes.
DevOps Solution:
- Task Groups: Convert the database migration Task Group into a parameterized YAML Step Template (
templates/db-migrate.yml) in a centralized governance git repository. - CI/CD Unification: Author multi-stage YAML pipelines where deployment stages target two Azure DevOps Environments:
StagingandProduction. - Release Governance: In Project Settings → Environments →
Production, configure Checks & Approvals:- Add a Manual Approvals check assigning the Lead Cloud Architect.
- Add an Invoke REST API check configured with the ServiceNow service connection, parsing the response to verify
status == 'Approved'.
- Maintain complete enterprise approval governance without writing complex approval logic in the YAML file itself, while achieving 100% Pipeline as Code compliance.
Common Exam Traps to Avoid
- Trap: Searching for Approval Syntax in YAML: You cannot write manual approval users or teams inside the
azure-pipelines.ymlcode. Approvals, business hours, and automated REST gates in YAML multi-stage pipelines are attached to Environments in Project Settings. - Trap: Assuming Secret Variables Auto-Map in Scripts: If a Classic pipeline PowerShell script accessed
$(DatabasePassword), that script will fail with an empty variable in YAML unless explicitly injected via the step'senv:block. - Trap: Confusing Deployment Groups with Environments: Classic pipelines used Deployment Groups for on-premises/IaaS virtual machine deployment. YAML pipelines replace Deployment Groups with Environments containing Virtual Machine resources.
An organization maintains 50 Classic Release Pipelines in Azure DevOps. Each release pipeline contains identical deployment step sequences for installing certificates, configuring IIS, and executing deployment scripts, duplicated across dozens of visual stages. When a certificate configuration task needs an update, administrators must manually edit 50 distinct pipelines. How can the organization modernize and centralize this repeated deployment logic using YAML pipelines?
A DevOps engineer is converting a Classic Release Pipeline to a multi-stage YAML pipeline. The Classic pipeline included a Pre-Deployment Approval gate requiring manual approval from the Lead Security Architect and an automated check verifying that no high-severity alerts exist in Azure Monitor before deploying to Production. How is this governance architecture implemented in the multi-stage YAML pipeline?
During a migration from a Classic Build pipeline to a YAML pipeline, a developer converts a visual PowerShell task that accesses a sensitive variable named DatabasePassword defined in a Variable Group. In the Classic pipeline, the script accessed $(DatabasePassword) directly. After migrating to YAML, the script outputs an empty string when accessing $env:DATABASEPASSWORD. What is the root cause and the required resolution in YAML?