Setting Up Azure DevOps Pipelines from Scratch

If your team is still building and deploying manually, you're wasting hours every week and introducing risk with every release. Here's how to set up Azure DevOps pipelines that automate the entire process, from commit to production.

Why YAML Pipelines?

Azure DevOps supports both the classic GUI editor and YAML pipelines. Use YAML. It lives in your repository alongside your code, so it's version controlled, reviewable in pull requests, and portable. When someone asks "what does the pipeline do?" the answer is in the repo, not locked inside a GUI that requires Azure DevOps admin access to view.

The classic editor has its uses for learning and experimentation, but YAML is the standard for production pipelines. It supports everything the classic editor does, plus templating, conditional logic, and reusable pipeline components that make managing multiple projects significantly easier. If you have 10 .NET projects, you can define a shared pipeline template once and reference it from each project rather than maintaining 10 separate classic pipelines.

YAML pipelines also integrate naturally with your branching strategy. You can define different pipeline behaviour for different branches — running full test suites on pull requests, deploying to staging on merges to develop, and deploying to production on merges to main. All of this is defined declaratively in the YAML file, visible to everyone on the team.

Build Pipeline Basics

A basic .NET build pipeline does four things: restore NuGet packages, build the solution, run tests, and publish the output as an artifact. In YAML, this takes about 30 lines. Trigger it on every push to your main branch and on pull requests so that every code change is validated automatically.

Key tip: use dotnet test --logger trx to generate test results that Azure DevOps can display natively in the pipeline summary. Add code coverage with coverlet and publish the results — your team will actually look at coverage when it's visible in every PR rather than buried in a separate tool.

For the build task itself, I recommend using dotnet publish rather than dotnet build for the artifact that gets deployed. The publish output includes everything needed to run the application, resolves runtime dependencies, and produces a deployment-ready package. This ensures that what you test is exactly what gets deployed — no discrepancies between build output and deployment package.

Set the build to fail on warnings, not just errors. This catches potential issues early and prevents the slow accumulation of warnings that teams learn to ignore. If a warning is genuinely irrelevant, suppress it explicitly in the code with a comment explaining why, rather than training the team to ignore yellow in the pipeline output.

Release Pipeline: Multi-Stage Deployment

For deployment, I use multi-stage YAML pipelines. Stage 1 builds and tests. Stage 2 deploys to staging with automatic approval. Stage 3 deploys to production with a manual approval gate. Each stage references the artifact from Stage 1, ensuring you deploy exactly what you tested.

The manual approval gate for production is important even if you trust your tests. It gives the team a chance to verify the staging deployment, run any manual checks they want, and confirm that the timing is right for a production release. For government teams, it also provides an audit trail showing who approved each production deployment and when — useful for change management processes and incident investigations.

For Azure App Service deployments, I strongly recommend using deployment slots. Deploy to a staging slot first, verify the deployment, then swap the staging and production slots. The swap is near-instantaneous and, crucially, reversible — if the production deployment has issues, you can swap back to the previous version in seconds. This is dramatically safer than deploying directly to production.

If your application uses a database, include database migration as a pipeline step. Entity Framework Core migrations can run as part of the deployment — apply pending migrations to the staging database, verify the application works, then apply the same migrations to production before the slot swap. This ensures database changes and code changes are always in sync.

Infrastructure as Code

Don't create Azure resources manually through the portal. Use Terraform or Bicep in your pipeline to create and update infrastructure. This means your staging and production environments are guaranteed identical (eliminating the "works in staging, breaks in production" problem), new environments can be spun up in minutes for testing or disaster recovery, infrastructure changes go through the same code review process as application changes, and your entire environment is documented in code rather than in someone's memory.

I typically use Terraform for multi-cloud or complex infrastructure and Bicep for Azure-only deployments. Bicep has the advantage of native Azure integration and doesn't require state management, but Terraform's provider ecosystem and mature state management make it the better choice for anything beyond simple Azure resources.

Store your infrastructure code in the same repository as your application code. Run terraform plan or the Bicep what-if command on pull requests so the team can review infrastructure changes before they're applied, just like they review code changes.

Branch Policies and Quality Gates

Once your build pipeline is reliable, enforce it. Configure branch policies on your main branch that require a successful build before merging, require at least one code reviewer to approve the pull request, require all PR comments to be resolved, and check for linked work items if you're tracking requirements in Azure DevOps.

These policies prevent the most common sources of production issues: untested code, unreviewed code, and code that doesn't address the intended requirement. They add a small amount of friction to the development process, but the reduction in production incidents more than compensates.

For code quality, add SonarQube or SonarCloud as a pipeline step. Configure quality gates that fail the build if code coverage drops below a threshold, if new code smells are introduced, or if security vulnerabilities are detected. This catches quality issues at the PR stage rather than after they've been merged and deployed.

Essential Additions

Once the basics are working reliably, add these in order of priority. Slack or Teams notifications for build failures so the team knows immediately when something breaks. Dependabot or Renovate for automated dependency updates, creating pull requests when new package versions are available. Scheduled nightly builds that run your full test suite including any slow integration tests you don't run on every commit. Container image scanning if you're deploying to Docker or Kubernetes. And licence scanning to ensure your dependencies don't introduce compliance issues.

Notifications deserve special attention. The default Azure DevOps notifications are noisy and most teams learn to ignore them. Configure a dedicated Teams or Slack channel that only receives notifications for build failures and deployment completions. Keep the signal-to-noise ratio high so the team pays attention when a notification arrives.

Pipeline Performance

Slow pipelines kill developer productivity and encourage people to skip the process. A build pipeline for a typical .NET solution should complete in under 5 minutes. If yours takes longer, look for these common causes: NuGet package restore downloading the same packages every time (configure a package cache), test projects being built in Debug mode (build in Release for deployment), unnecessary clean builds when incremental builds would suffice, and Docker image builds pulling base images from Docker Hub on every run instead of caching them in Azure Container Registry.

For test execution, run unit tests on every commit but run slower integration tests only on pull requests or nightly. This keeps the feedback loop fast for developers while still catching integration issues before they reach production.

Getting Started

If this feels overwhelming, start simple. Get a build pipeline running that restores, builds, and tests on every push. That alone catches most bugs before they reach production. Add deployment automation next. Then infrastructure as code. Then quality gates. Each step delivers value independently — the perfect pipeline is the enemy of the good pipeline.

If you want hands-on help setting up Azure DevOps pipelines for your team, I offer a dedicated DevOps and CI/CD service. A basic pipeline setup typically takes one to two weeks and transforms how your team ships software. Get in touch if you'd like to discuss your setup.

Need Help With This?

I offer consulting and hands-on development for .NET, Azure, and DevOps projects. Let's talk about how I can help.

Get in Touch →