Azure DevOps
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
- Azure DevOps Official Site
- Azure Pipelines Documentation
- Azure DevOps Labs
- Azure DevOps Roadmap
- Azure DevOps REST API
- Azure DevOps CLI
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)"