DevOps with Azure DevOps
Azure DevOps is Microsoft’s suite for planning, developing, testing, and deploying software. It includes Azure Repos, Pipelines, Artifacts, Test Plans, and Boards — integrated with GitHub, Azure services, and third-party tools. For Azure-centric teams, it provides native service connections, environment gates, and deployment tracking.
Azure DevOps Services
| Service | Purpose |
|---|---|
| Azure Repos | Git repositories with pull requests and branch policies |
| Azure Pipelines | CI/CD for any platform, language, and cloud |
| Azure Artifacts | Package feeds (npm, NuGet, Maven, Python, Universal) |
| Azure Boards | Work tracking, sprints, backlogs, Kanban |
| Azure Test Plans | Manual, exploratory, and automated testing |
Project Setup
- Create an organization at dev.azure.com
- Create a project with Git version control and Agile process template
- Connect your repository or import from GitHub
- Configure branch policies on
main:- Require pull request with minimum 1 reviewer
- Require build validation (CI pipeline must pass)
- Require linked work items for traceability
# Install Azure DevOps CLI extension
az extension add --name azure-devops
# Configure defaults
az devops configure --defaults organization=https://dev.azure.com/myorg project=web-app
# List pipelines
az pipelines list --output table
CI Pipeline (YAML)
Create azure-pipelines.yml in your repo root:
trigger:
branches:
include:
- main
- release/*
pr:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '20.x'
buildConfiguration: 'Release'
stages:
- stage: Build
jobs:
- job: BuildAndTest
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- script: npm ci
displayName: 'Install dependencies'
- script: npm run lint
displayName: 'Lint'
- script: npm test -- --coverage
displayName: 'Run tests with coverage'
- script: npm run build
displayName: 'Build application'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/test-results.xml'
- task: PublishCodeCoverageResults@2
inputs:
summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: 'dist'
artifactName: 'drop'
CD with Environments and Approvals
Multi-stage YAML pipelines with environment approvals gate production deploys:
- stage: DeployStaging
dependsOn: Build
jobs:
- deployment: DeployWebStaging
environment: staging
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'Azure-Prod-ServiceConnection'
appType: 'webAppLinux'
appName: 'my-webapp-staging'
package: '$(Pipeline.Workspace)/drop'
- stage: DeployProduction
dependsOn: DeployStaging
jobs:
- deployment: DeployWebProduction
environment: production # Requires manual approval
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'Azure-Prod-ServiceConnection'
appType: 'webAppLinux'
appName: 'my-webapp-prod'
package: '$(Pipeline.Workspace)/drop'
deployToSlotOrASE: true
resourceGroupName: 'rg-webapp-prod'
slotName: 'staging'
- task: AzureAppServiceManage@0
inputs:
azureSubscription: 'Azure-Prod-ServiceConnection'
action: 'Swap Slots'
webAppName: 'my-webapp-prod'
resourceGroupName: 'rg-webapp-prod'
sourceSlot: 'staging'
Infrastructure as Code Pipeline
Deploy Azure resources with Bicep in the same pipeline:
- stage: DeployInfra
jobs:
- job: DeployBicep
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Prod-ServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group what-if \
--resource-group rg-webapp-prod \
--template-file infra/main.bicep \
--parameters infra/parameters.prod.json
az deployment group create \
--resource-group rg-webapp-prod \
--template-file infra/main.bicep \
--parameters infra/parameters.prod.json
Service Connections and Secrets
Create service connections under Project Settings → Service connections → Azure Resource Manager:
| Scope | Use Case |
|---|---|
| Subscription | Broad access — avoid for production |
| Resource Group | Scoped to rg-webapp-prod — recommended |
| Managed Identity | Workload identity federation — most secure |
Link Variable Groups to Azure Key Vault for secrets:
variables:
- group: prod-secrets # Linked to Key Vault
steps:
- script: echo "Deploying with secret from Key Vault"
env:
DB_PASSWORD: $(db-admin-password)
Real-World Scenario: Full CI/CD for Azure Web App
| Stage | Action |
|---|---|
| PR opened | CI runs: lint, unit tests, SAST scan |
| Merge to main | Build artifact; deploy to staging slot |
| Staging validation | Smoke tests, integration tests against staging |
| Production gate | Manual approval from release manager |
| Production deploy | Swap staging → production (zero downtime) |
| Post-deploy | Application Insights availability test verification |
Azure DevOps vs GitHub Actions
| Feature | Azure DevOps | GitHub Actions |
|---|---|---|
| Code hosting | Azure Repos or GitHub | GitHub only |
| Pipelines | YAML, classic releases | YAML workflows |
| Azure integration | Native service connections | OIDC federation |
| Work tracking | Azure Boards (built-in) | GitHub Issues/Projects |
| Package feeds | Azure Artifacts | GitHub Packages |
| Best for | Enterprise Azure teams | GitHub-centric teams |
Both are production-ready. Many teams use GitHub for code and Azure DevOps for pipelines and boards.
Common Mistakes
- Secrets in pipeline YAML — use Variable Groups linked to Key Vault
- No branch policies — direct pushes to main bypass CI validation
- Overly broad service connection — scope to resource group, not subscription
- Missing deployment gates — auto-deploy to production without approval
- No pipeline caching — every build downloads all dependencies from scratch
- Ignoring pipeline failures in PRs — merge broken code and debug in production
Troubleshooting
| Issue | Diagnosis | Fix |
|---|---|---|
| Pipeline not triggering | Branch filter mismatch | Verify trigger/pr branch includes match |
| Service connection auth failure | Expired secret or wrong scope | Regenerate SP; verify RBAC on resource group |
| Deployment succeeds but app broken | Wrong artifact or config | Check deployment logs; verify app settings |
| Slow builds | No caching, large artifacts | Add npm/pip cache; use multi-stage builds |
| Environment approval stuck | No approvers configured | Add approvers in Environment settings |
# Run pipeline manually
az pipelines run --name "web-app-ci" --branch main
# List recent runs
az pipelines runs list --pipeline-ids 1 --top 5 -o table
Best Practices
| Practice | Benefit |
|---|---|
| YAML pipelines in repo | Version-controlled, reviewable CI/CD |
| Environment approvals | Gate production deploys with audit trail |
| Variable groups + Key Vault | Secure secrets, no plaintext in YAML |
| Pipeline caching | Faster builds (npm, NuGet, Docker layers) |
| Deployment jobs (not regular jobs) | Deployment history and rollback tracking |
| Parallel test stages | Reduce total pipeline duration |
| Artifact immutability | Same build artifact promoted through all stages |