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:).
Last updated: September 2026

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 (PipelinesReleases) 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:

  1. A developer creates a git branch feature/upgrade-dotnet-9.
  2. In that branch, they edit azure-pipelines.yml to use DotNetCoreCLI@2 with .NET 9 SDK.
  3. The pipeline validation runs the modified pipeline logic exclusively for that branch, completely isolating trunk (main) from experimental disruptions.
  4. 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 using dependsOn: and conditional execution using condition:.

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:

  1. Open the Classic Build or Release definition in the web edit canvas.
  2. Select an individual task in the phase.
  3. Click the View YAML button in the upper right corner of the task panel.
  4. 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 the env: 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 ConstructYAML Multi-Stage EquivalentConfiguration Location
Classic Build Phasejob: JobNameDefined in azure-pipelines.yml
Classic Server Phasejob: ServerJob with pool: serverRun on Azure DevOps serverless compute
Task GroupParameterized Step or Job TemplateStored in .yml file in Git repository
Continuous Integration Triggertrigger: branches: include: ...Defined in azure-pipelines.yml
Continuous Deployment Triggerstages: with dependsOn: and resources: pipelines:Defined in azure-pipelines.yml
Pre-Deployment Manual ApprovalManual Approvals check on EnvironmentConfigured in Project Settings → Environments
Pre-Deployment Automated GateInvoke REST API / Azure Monitor AlertsConfigured in Project Settings → Environments
Deployment Group (VM Targets)Environment with Virtual Machine resourcesInstalled agent tags linked to Environment
Artifact Link / Downloaddownload: current or download: pipelineResourceDefined in deployment job steps
Deployment Strategystrategy: runOnce, rolling, or canaryDefined 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:

  1. A manual approval sign-off by the Lead Cloud Architect.
  2. An automated REST API check to their internal ServiceNow Change Management portal to verify an approved CAB change request.
  3. 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: Staging and Production.
  • Release Governance: In Project SettingsEnvironmentsProduction, 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.yml code. 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's env: 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.
Loading diagram...
Classic vs. YAML Multi-Stage Pipeline Architecture
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D