Azure DevOps

CI/CDAzure DevOpsMicrosoftYAMLCloudEnterpriseDevOpsIntegration

CI/CD Tool

Azure DevOps

Overview

Azure DevOps is Microsoft's comprehensive DevOps platform. It integrates CI/CD, project management, Git, and artifact management, providing modern workflow management through YAML pipelines.

Details

Azure DevOps is a comprehensive DevOps platform provided by Microsoft, evolved from Visual Studio Team Services as a cloud service in 2018. It integrates CI/CD, source code management, project management, artifact management, and test management in a single platform, supporting the entire process from development to deployment. In 2024-2025, multi-stage YAML pipelines have become a key feature, enabling sophisticated CI/CD workflows that split build, test, and deployment phases across multiple stages. YAML-based pipelines enable code-first configuration and management, providing version control, reusability, and easy management across multiple projects. Enhanced support for microservices architectures through integration with Azure Kubernetes Service (AKS) and Azure Container Apps, enabling unified deployments from on-premises servers to cloud-native platforms. Microsoft has focused on security improvements over the past several years, recommending the use of YAML pipelines over classic pipelines.

Pros and Cons

Pros

  • All-in-One Platform: Integrated CI/CD, Git, Issues, and Artifacts
  • Strong YAML Support: Code-first configuration and version control
  • Multi-stage Pipelines: Staged execution of complex workflows
  • Azure Integration: Native Microsoft Cloud connectivity
  • Enterprise Features: Security, governance, and scalability
  • Cross-platform: Linux, Windows, macOS support
  • Rich Marketplace: Abundant third-party extensions
  • Free Plan: Free usage for up to 5 users

Cons

  • Microsoft Ecosystem Dependency: Feature limitations outside Azure
  • Learning Curve: Time required to master due to rich features
  • UI/UX Complexity: Complex operations due to multi-functionality
  • Pricing Structure: License costs for large teams
  • Performance: Response degradation during heavy data processing
  • Customization Limitations: Flexibility constraints compared to Jenkins
  • Competing Tool Integration: Integration challenges with non-Microsoft tools

Key Links

Code Examples

Basic YAML Pipeline

# azure-pipelines.yml
trigger:
- main
- develop

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  dotNetFramework: 'net8.0'

stages:
- stage: Build
  displayName: 'Build and Test'
  jobs:
  - job: BuildJob
    displayName: 'Build Job'
    steps:
    - task: UseDotNet@2
      displayName: 'Install .NET SDK'
      inputs:
        packageType: sdk
        version: '8.x'
    
    - task: DotNetCoreCLI@2
      displayName: 'Restore packages'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Build solution'
      inputs:
        command: 'build'
        projects: '**/*.csproj'
        arguments: '--configuration $(buildConfiguration)'
    
    - task: DotNetCoreCLI@2
      displayName: 'Run tests'
      inputs:
        command: 'test'
        projects: '**/*Tests.csproj'
        arguments: '--configuration $(buildConfiguration) --collect "Code Coverage"'
    
    - task: PublishTestResults@2
      displayName: 'Publish test results'
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: '**/*.trx'
    
    - task: PublishCodeCoverageResults@1
      displayName: 'Publish code coverage'
      inputs:
        codeCoverageTool: 'Cobertura'
        summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

Multi-stage Deployment

# azure-pipelines.yml
trigger:
- main

variables:
  dockerRegistryServiceConnection: 'myDockerRegistry'
  imageRepository: 'myapp'
  containerRegistry: 'myregistry.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'

stages:
- stage: Build
  displayName: 'Build and Push'
  jobs:
  - job: Build
    displayName: 'Build job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Build and push image'
      inputs:
        command: buildAndPush
        repository: $(imageRepository)
        dockerfile: $(dockerfilePath)
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)
          latest

- stage: DeployStaging
  displayName: 'Deploy to Staging'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: 'Deploy to Staging'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'staging'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebAppContainer@1
            displayName: 'Deploy to Azure Web App'
            inputs:
              azureSubscription: 'myAzureSubscription'
              appName: 'myapp-staging'
              containers: '$(containerRegistry)/$(imageRepository):$(tag)'

- stage: DeployProduction
  displayName: 'Deploy to Production'
  dependsOn: DeployStaging
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - deployment: Deploy
    displayName: 'Deploy to Production'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureRmWebAppDeployment@4
            displayName: 'Deploy to Production'
            inputs:
              ConnectionType: 'AzureRM'
              azureSubscription: 'myAzureSubscription'
              appType: 'webAppContainer'
              WebAppName: 'myapp-production'
              DockerNamespace: '$(containerRegistry)'
              DockerRepository: '$(imageRepository)'
              DockerImageTag: '$(tag)'

Kubernetes Deployment

# azure-pipelines.yml
resources:
- repo: self

variables:
  kubernetesServiceConnection: 'myK8sConnection'
  imageRepository: 'myapp'
  containerRegistry: 'myregistry.azurecr.io'
  tag: '$(Build.BuildId)'
  imagePullSecret: 'myregistrykey'

stages:
- stage: Build
  displayName: 'Build stage'
  jobs:
  - job: Build
    displayName: 'Build job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Build and push image to container registry'
      inputs:
        command: 'buildAndPush'
        repository: '$(imageRepository)'
        dockerfile: '**/Dockerfile'
        containerRegistry: 'myDockerRegistry'
        tags: |
          $(tag)

- stage: Deploy
  displayName: 'Deploy stage'
  dependsOn: Build
  jobs:
  - deployment: Deploy
    displayName: 'Deploy job'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'production.default'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: KubernetesManifest@0
            displayName: 'Create imagePullSecret'
            inputs:
              action: 'createSecret'
              secretName: '$(imagePullSecret)'
              dockerRegistryEndpoint: 'myDockerRegistry'
              kubernetesServiceConnection: '$(kubernetesServiceConnection)'
              
          - task: KubernetesManifest@0
            displayName: 'Deploy to Kubernetes cluster'
            inputs:
              action: 'deploy'
              kubernetesServiceConnection: '$(kubernetesServiceConnection)'
              manifests: |
                $(Pipeline.Workspace)/manifests/deployment.yml
                $(Pipeline.Workspace)/manifests/service.yml
              imagePullSecrets: '$(imagePullSecret)'
              containers: '$(containerRegistry)/$(imageRepository):$(tag)'

Complex Workflows and Templates

# azure-pipelines.yml
trigger:
- main
- feature/*

resources:
  repositories:
  - repository: templates
    type: git
    name: MyProject/pipeline-templates

variables:
- template: variables/common.yml@templates

stages:
- stage: Validate
  displayName: 'Validation Stage'
  jobs:
  - template: jobs/code-quality.yml@templates
    parameters:
      solution: '**/*.sln'
      
- stage: Build
  displayName: 'Build Stage'
  dependsOn: Validate
  jobs:
  - template: jobs/build.yml@templates
    parameters:
      buildConfiguration: 'Release'
      
- stage: Test
  displayName: 'Test Stage'
  dependsOn: Build
  jobs:
  - job: UnitTests
    displayName: 'Unit Tests'
    pool:
      vmImage: 'windows-latest'
    steps:
    - template: steps/dotnet-test.yml@templates
      parameters:
        testProjects: '**/*UnitTests.csproj'
        
  - job: IntegrationTests
    displayName: 'Integration Tests'
    pool:
      vmImage: 'ubuntu-latest'
    services:
      postgres: postgres
    steps:
    - template: steps/dotnet-test.yml@templates
      parameters:
        testProjects: '**/*IntegrationTests.csproj'

- stage: Deploy
  displayName: 'Deploy Stage'
  dependsOn: 
  - Build
  - Test
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - template: jobs/deploy.yml@templates
    parameters:
      environment: 'production'
      serviceConnection: 'myAzureConnection'

Security and Governance

# azure-pipelines.yml
trigger: none # Manual execution only

pool:
  vmImage: 'ubuntu-latest'

variables:
- group: production-secrets
- name: isMain
  value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]

stages:
- stage: SecurityScan
  displayName: 'Security Scanning'
  jobs:
  - job: CodeScan
    displayName: 'Code Security Scan'
    steps:
    - task: SonarCloudPrepare@1
      inputs:
        SonarCloud: 'SonarCloud'
        organization: 'myorg'
        scannerMode: 'MSBuild'
        projectKey: 'myproject'
        
    - task: DotNetCoreCLI@2
      inputs:
        command: 'build'
        
    - task: SonarCloudAnalyze@1
    
    - task: SonarCloudPublish@1
    
    - task: WhiteSource@21
      displayName: 'WhiteSource Vulnerability Scan'
      inputs:
        cwd: '$(System.DefaultWorkingDirectory)'

- stage: Deploy
  displayName: 'Production Deploy'
  dependsOn: SecurityScan
  condition: and(succeeded(), eq(variables.isMain, true))
  jobs:
  - deployment: DeployProduction
    displayName: 'Deploy to Production'
    environment: 'production'
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureKeyVault@2
            inputs:
              azureSubscription: 'mySubscription'
              KeyVaultName: 'mykeyvault'
              SecretsFilter: '*'
              RunAsPreJob: true
              
          - task: AzureCLI@2
            inputs:
              azureSubscription: 'mySubscription'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                az webapp config appsettings set \
                  --resource-group myResourceGroup \
                  --name myapp \
                  --settings DATABASE_URL="$(database-url)"