From Classic to YAML: How We Cut Azure DevOps Deployment Times by 60%
Since moving from QA automation into a DevOps role at Isogent, one of my main projects has been converting our Azure DevOps classic UI pipelines into version-controlled YAML pipeline definitions. I converted all 20 of our pipelines in two weeks, and deployments now finish about 60% faster than before.
The speed is the headline, but it isn't the biggest win. Our pipelines now live in Git next to the code they build. Every change is reviewed in a pull request, and we can see exactly who changed what and when. This post covers why we made the move, how I approached it, and what I'd tell anyone planning the same migration.
Why classic pipelines were holding us back
Classic pipelines are built by clicking through the Azure DevOps web UI. That works when you have a handful of pipelines, but it gets harder to manage as the number grows:
No code review. A change to a build or release takes effect as soon as someone clicks Save. Nobody reviews it in a pull request first.
Copy-paste sprawl. When a new service needs a pipeline, the fastest path is cloning an existing one. Over time the copies drift apart, and a fix in one never reaches the others.
Pipelines don't branch with the code. If a feature branch needs a different build step, there's no clean way to change the pipeline for that branch only.
Hard to reproduce. Rebuilding a pipeline from scratch means remembering every setting someone clicked through months ago.
Microsoft now points teams toward YAML pipelines for new work, and that's where new Azure DevOps features land first. Moving was the right call for us long term, not just for speed.
How I approached the migration
I didn't try to convert everything at once. Here's the process I followed, one pipeline at a time:
Take inventory. List every classic build and release pipeline, what it deploys, where it deploys, and who depends on it. Some will turn out to be unused, and you can retire those instead of converting them.
Start from the generated YAML. For classic build pipelines, Azure DevOps can show the YAML for each job and task. It's a rough first draft, not a finished pipeline, but it saves typing.
Rewrite releases as stages. Classic release pipelines don't export to YAML, so I rebuilt each one as deployment stages in the same multi-stage pipeline as its build.
Pull shared steps into templates. Steps every service repeats, like building and pushing a container image, went into reusable YAML templates. Fix a template once and every pipeline that uses it gets the fix.
Keep secrets out of the YAML. Secrets stay in variable groups linked to Azure Key Vault, so no secret ever lands in the repository.
Run old and new side by side. I ran the YAML pipeline alongside the classic one until the results matched, then disabled the classic pipeline. I didn't delete it right away.
Here's a simplified example of what a converted pipeline looks like: build an image, then deploy it to AKS.
trigger:
branches:
include:
- main
variables:
- group: app-settings # backed by Azure Key Vault
- name: imageName
value: myapp
stages:
- stage: Build
jobs:
- job: BuildAndPush
pool:
vmImage: ubuntu-latest
steps:
- template: templates/docker-build-push.yml
parameters:
imageName: $(imageName)
tag: $(Build.BuildId)
- stage: DeployProd
dependsOn: Build
jobs:
- deployment: DeployToAKS
environment: aks-prod # records deployment history
pool:
vmImage: ubuntu-latest
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@1
inputs:
action: deploy
connectionType: azureResourceManager
azureSubscriptionConnection: $(azureServiceConnection)
azureResourceGroup: $(aksResourceGroup)
kubernetesCluster: $(aksClusterName)
manifests: k8s/*.yml
containers: $(acrLoginServer)/$(imageName):$(Build.BuildId)What made deployments faster
The 60% didn't come from YAML itself. YAML is just a file format. It came from the changes that YAML made easy:
Running jobs in parallel. Independent jobs, like unit tests and image builds, now run at the same time instead of one after another.
Merging build and release. Build and release used to be separate pipelines with a wait in between. Now one multi-stage pipeline flows straight from build into deployment.
Removing steps nobody needed. Rewriting each pipeline by hand surfaced old steps that no longer did anything useful, and they didn't make the move.
None of these changes is dramatic on its own. Applied across 20 pipelines, they add up.
Lessons learned
A few things I'd tell anyone starting this migration:
Treat the generated YAML as a draft. It mirrors the classic pipeline exactly, including the parts you'd want to fix.
Build templates early. The second pipeline you convert will show you which steps belong in a template. Don't wait until the tenth.
Watch your variables. Classic pipelines mix variables set in the UI, variable groups, and release-scoped values. Map each one to a clear place in YAML before switching over.
Keep the classic pipeline as a fallback. Disable it rather than deleting it until the YAML version has run cleanly for a while.
Bring the tests along. My QA background paid off here. Every pipeline I converted kept its automated test stage, so speeding up deployments never meant skipping quality checks.
Wrapping up
Moving from classic to YAML pipelines took deployments from something we clicked through to something we review, version, and reuse. Faster deployments were a welcome bonus.
If your team is still on classic pipelines, start with one low-risk pipeline, build a template from it, and grow from there.
I write about DevOps, Kubernetes, Terraform, and test automation here. If you're working through a similar migration, I'd like to hear how it's going. Connect with me on LinkedIn.


Comments