Azure DevOps & CI/CD Guide for .NET Developers
Introduction
Azure DevOps provides a comprehensive suite of tools for software development - from source control and work tracking to continuous integration and deployment. This guide covers Azure Pipelines, Azure Repos, Infrastructure as Code, and deployment strategies for .NET applications.
Table of Contents
- Azure DevOps Services Overview
- Azure Repos (Git)
- Azure Pipelines
- Build Pipelines for .NET
- Release Pipelines and Environments
- Azure Artifacts
- GitHub Actions Integration
- Infrastructure as Code
- Deployment Patterns
- Security Scanning
- Monitoring Deployments
- Interview Questions
Azure DevOps Services Overview
Services Comparison
| Service | Purpose | Alternative |
|---|---|---|
| Azure Boards | Work tracking, sprints | Jira, GitHub Issues |
| Azure Repos | Git repositories | GitHub, GitLab |
| Azure Pipelines | CI/CD | GitHub Actions, Jenkins |
| Azure Artifacts | Package management | NuGet.org, npm registry |
| Azure Test Plans | Test management | TestRail |
Azure DevOps vs GitHub
| Feature | Azure DevOps | GitHub |
|---|---|---|
| Best for | Enterprise, Microsoft stack | Open source, modern teams |
| Work tracking | Comprehensive (Boards) | Basic (Issues, Projects) |
| Package feeds | Artifacts | Packages |
| CI/CD | Pipelines (YAML/Classic) | Actions (YAML only) |
| Integration | Strong Azure integration | Strong community |
Azure Repos (Git)
Branching Strategies
GitFlow
main (production)
├── develop
│ ├── feature/add-login
│ ├── feature/payment-api
│ └── feature/user-profile
├── release/1.0.0
│ └── bugfix/login-fix
└── hotfix/security-patch
GitHub Flow (Recommended for CI/CD)
main (always deployable)
├── feature/add-login → PR → main
├── feature/payment-api → PR → main
└── bugfix/fix-crash → PR → main
Branch Policies
# Branch policy configuration (via Azure DevOps REST API or UI)
policies:
- type: minimumApprovers
settings:
minimumApproverCount: 2
creatorVoteCounts: false
- type: buildPolicy
settings:
buildDefinitionId: 123
displayName: "PR Build"
queueOnSourceUpdateOnly: true
- type: commentRequirements
settings:
requireResolutionOfComments: true
- type: mergeStrategy
settings:
allowSquash: true
allowRebase: false
allowMerge: false
Pull Request Templates
<!-- .azuredevops/pull_request_template.md -->
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing completed
## Checklist
- [ ] Code follows project style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No new warnings introduced
Azure Pipelines
YAML vs Classic
| Feature | YAML | Classic |
|---|---|---|
| Source control | Yes | No |
| Code review | Yes (PR on pipeline changes) | No |
| Reusability | Templates | Task groups |
| Recommended | Yes | Legacy |
Pipeline Structure
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
paths:
include:
- src/*
exclude:
- docs/*
pr:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
- group: app-common-variables
- name: buildConfiguration
value: 'Release'
- name: dotnetVersion
value: '8.0.x'
stages:
- stage: Build
displayName: 'Build & Test'
jobs:
- job: BuildJob
steps:
- template: templates/build.yml
parameters:
configuration: $(buildConfiguration)
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- deployment: DeployDev
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy.yml
parameters:
environment: dev
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProd
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy.yml
parameters:
environment: prod
Templates for Reusability
# templates/build.yml
parameters:
- name: configuration
type: string
default: 'Release'
- name: projects
type: string
default: '**/*.csproj'
steps:
- task: UseDotNet@2
displayName: 'Use .NET SDK'
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '${{ parameters.projects }}'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '${{ parameters.projects }}'
arguments: '--configuration ${{ parameters.configuration }} --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration ${{ parameters.configuration }} --no-build --collect:"XPlat Code Coverage"'
- task: PublishCodeCoverageResults@2
displayName: 'Publish Coverage'
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
# templates/deploy.yml
parameters:
- name: environment
type: string
- name: serviceConnection
type: string
default: 'azure-subscription'
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to App Service'
inputs:
azureSubscription: '${{ parameters.serviceConnection }}'
appType: 'webAppLinux'
appName: 'myapp-${{ parameters.environment }}'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
runtimeStack: 'DOTNETCORE|8.0'
Build Pipelines for .NET
Complete .NET 8 Build Pipeline
# azure-pipelines.yml
trigger:
- main
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
stages:
- stage: Build
jobs:
- job: Build
pool:
vmImage: 'ubuntu-latest'
steps:
# Use global.json for SDK version
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
useGlobalJson: true
workingDirectory: $(System.DefaultWorkingDirectory)
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
feedsToUse: 'select'
vstsFeed: 'my-feed' # Private feed if needed
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '**/*.sln'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >
--configuration $(buildConfiguration)
--no-build
--collect:"XPlat Code Coverage"
--logger trx
--results-directory $(Agent.TempDirectory)/TestResults
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'
mergeTestResults: true
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- task: DotNetCoreCLI@2
displayName: 'Publish application'
inputs:
command: 'publish'
publishWebProjects: true
arguments: >
--configuration $(buildConfiguration)
--output $(Build.ArtifactStagingDirectory)
--no-build
--runtime linux-x64
--self-contained false
- task: PublishBuildArtifacts@1
displayName: 'Publish artifacts'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
Multi-Project Solution
# For solutions with multiple deployable projects
stages:
- stage: Build
jobs:
- job: BuildApi
displayName: 'Build API'
steps:
- template: templates/build-project.yml
parameters:
projectPath: 'src/Api/Api.csproj'
artifactName: 'api'
- job: BuildWorker
displayName: 'Build Worker'
steps:
- template: templates/build-project.yml
parameters:
projectPath: 'src/Worker/Worker.csproj'
artifactName: 'worker'
- job: BuildFunctions
displayName: 'Build Functions'
steps:
- template: templates/build-functions.yml
parameters:
projectPath: 'src/Functions/Functions.csproj'
artifactName: 'functions'
Docker Build
# Docker build and push
- stage: BuildDocker
jobs:
- job: DockerBuild
steps:
- task: Docker@2
displayName: 'Build and push'
inputs:
containerRegistry: 'acr-service-connection'
repository: 'myapp'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: |
$(Build.BuildId)
latest
Release Pipelines and Environments
Environment Configuration
# Define environments in Azure DevOps
# Settings → Pipelines → Environments
# Environment: development
# - No approvals
# - Auto-deploy on build
# Environment: staging
# - Require 1 approval
# - Deploy after dev succeeds
# Environment: production
# - Require 2 approvals
# - Business hours only
# - Lock during incidents
Multi-Stage Deployment
stages:
- stage: DeployDev
displayName: 'Deploy to Development'
jobs:
- deployment: Deploy
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-webapp.yml
parameters:
environment: dev
slotName: ''
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: DeployDev
jobs:
- deployment: Deploy
environment: 'staging'
strategy:
runOnce:
preDeploy:
steps:
- script: echo "Running pre-deployment checks"
deploy:
steps:
- template: templates/deploy-webapp.yml
parameters:
environment: staging
slotName: ''
routeTraffic:
steps:
- script: echo "Routing traffic"
postRouteTraffic:
steps:
- template: templates/smoke-tests.yml
on:
failure:
steps:
- template: templates/rollback.yml
success:
steps:
- script: echo "Deployment successful"
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
jobs:
- deployment: DeploySlot
displayName: 'Deploy to Staging Slot'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-webapp.yml
parameters:
environment: prod
slotName: staging
- job: ApproveSwap
displayName: 'Manual Approval for Swap'
dependsOn: DeploySlot
pool: server
steps:
- task: ManualValidation@0
inputs:
notifyUsers: 'devops-team@company.com'
instructions: 'Verify staging slot and approve swap'
- deployment: SwapSlots
displayName: 'Swap to Production'
dependsOn: ApproveSwap
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
inputs:
azureSubscription: 'azure-subscription'
Action: 'Swap Slots'
WebAppName: 'myapp-prod'
ResourceGroupName: 'myapp-rg'
SourceSlot: 'staging'
TargetSlot: 'production'
Approvals and Checks
# Configure in Environment settings:
# 1. Approvals - Manual approval before deployment
# 2. Business Hours - Only deploy during work hours
# 3. Branch Control - Only deploy from specific branches
# 4. Invoke Azure Function - Custom validation
# 5. Query Work Items - Ensure all PBIs are approved
# Example: Branch control
# Only allow deployments from main branch to production
Azure Artifacts
Creating a NuGet Feed
# Publish to Azure Artifacts
- task: NuGetCommand@2
displayName: 'Pack NuGet'
inputs:
command: 'pack'
packagesToPack: '**/MyLibrary.csproj'
versioningScheme: 'byEnvVar'
versionEnvVar: 'PackageVersion'
- task: NuGetCommand@2
displayName: 'Push to Artifacts'
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'internal'
publishVstsFeed: 'my-project/my-feed'
Consuming Private Packages
<!-- NuGet.config -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="MyFeed" value="https://pkgs.dev.azure.com/myorg/myproject/_packaging/my-feed/nuget/v3/index.json" />
</packageSources>
</configuration>
npm Packages
# Publish npm package
- task: Npm@1
inputs:
command: 'publish'
workingDir: '$(System.DefaultWorkingDirectory)/src/frontend'
publishRegistry: 'useFeed'
publishFeed: 'my-project/my-feed'
GitHub Actions Integration
Using GitHub Actions with Azure
# .github/workflows/deploy.yml
name: Deploy to Azure
on:
push:
branches: [main]
env:
AZURE_WEBAPP_NAME: myapp
DOTNET_VERSION: '8.0.x'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Build
run: dotnet build --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
- name: Publish
run: dotnet publish -c Release -o ./publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
path: ./publish
- name: Deploy to Azure
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}-dev
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_DEV }}
package: ./publish
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
path: ./publish
- name: Deploy to Azure
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}-prod
slot-name: staging
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_PROD }}
package: ./publish
- name: Swap slots
uses: azure/cli@v2
with:
inlineScript: |
az webapp deployment slot swap \
--name ${{ env.AZURE_WEBAPP_NAME }}-prod \
--resource-group myapp-rg \
--slot staging \
--target-slot production
Infrastructure as Code
Bicep (Recommended)
// main.bicep
@description('Environment name')
param environment string
@description('Location for resources')
param location string = resourceGroup().location
@description('App Service Plan SKU')
param appServicePlanSku string = 'P1v3'
// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
name: 'asp-myapp-${environment}'
location: location
sku: {
name: appServicePlanSku
capacity: 1
}
kind: 'linux'
properties: {
reserved: true
}
}
// Web App
resource webApp 'Microsoft.Web/sites@2022-09-01' = {
name: 'app-myapp-${environment}'
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
alwaysOn: true
ftpsState: 'Disabled'
minTlsVersion: '1.2'
}
httpsOnly: true
}
identity: {
type: 'SystemAssigned'
}
}
// SQL Database
resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
name: 'sql-myapp-${environment}'
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: sqlAdminPassword
publicNetworkAccess: 'Disabled'
}
}
resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
parent: sqlServer
name: 'db-myapp'
location: location
sku: {
name: 'GP_Gen5_2'
tier: 'GeneralPurpose'
}
}
// Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
name: 'kv-myapp-${environment}'
location: location
properties: {
tenantId: subscription().tenantId
sku: {
family: 'A'
name: 'standard'
}
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: webApp.identity.principalId
permissions: {
secrets: ['get', 'list']
}
}
]
}
}
// Outputs
output webAppName string = webApp.name
output webAppHostName string = webApp.properties.defaultHostName
Deploying Bicep in Pipeline
# Deploy infrastructure
- stage: DeployInfrastructure
jobs:
- job: DeployBicep
steps:
- task: AzureCLI@2
displayName: 'Deploy Bicep'
inputs:
azureSubscription: 'azure-subscription'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create \
--resource-group myapp-$(environment)-rg \
--template-file infrastructure/main.bicep \
--parameters environment=$(environment) \
--parameters sqlAdminPassword=$(SQL_ADMIN_PASSWORD)
Terraform Alternative
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstatemyapp"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
provider "azurerm" {
features {}
}
variable "environment" {
type = string
}
resource "azurerm_resource_group" "main" {
name = "myapp-${var.environment}-rg"
location = "East US"
}
resource "azurerm_service_plan" "main" {
name = "asp-myapp-${var.environment}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "P1v3"
}
resource "azurerm_linux_web_app" "main" {
name = "app-myapp-${var.environment}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.main.id
site_config {
application_stack {
dotnet_version = "8.0"
}
always_on = true
}
identity {
type = "SystemAssigned"
}
}
Deployment Patterns
Blue-Green Deployment
# Deploy to staging slot, test, then swap
- stage: DeployProduction
jobs:
- deployment: DeployToSlot
environment: 'production'
strategy:
runOnce:
deploy:
steps:
# Deploy to staging slot
- task: AzureWebApp@1
inputs:
azureSubscription: 'azure-subscription'
appName: 'myapp-prod'
slotName: 'staging'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
# Run smoke tests against staging slot
- task: Bash@3
displayName: 'Smoke tests'
inputs:
targetType: 'inline'
script: |
response=$(curl -s -o /dev/null -w "%{http_code}" https://myapp-prod-staging.azurewebsites.net/health)
if [ $response != "200" ]; then
echo "Health check failed with status $response"
exit 1
fi
# Swap slots
- task: AzureAppServiceManage@0
displayName: 'Swap slots'
inputs:
azureSubscription: 'azure-subscription'
Action: 'Swap Slots'
WebAppName: 'myapp-prod'
ResourceGroupName: 'myapp-rg'
SourceSlot: 'staging'
Canary Deployment
# Gradual traffic routing
- stage: CanaryDeployment
jobs:
- deployment: Deploy
environment: 'production'
strategy:
canary:
increments: [10, 25, 50, 100]
preDeploy:
steps:
- script: echo "Preparing canary deployment"
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'azure-subscription'
appName: 'myapp-prod'
slotName: 'canary'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
routeTraffic:
steps:
- task: AzureAppServiceManage@0
inputs:
azureSubscription: 'azure-subscription'
Action: 'Traffic Routing'
WebAppName: 'myapp-prod'
ResourceGroupName: 'myapp-rg'
SourceSlot: 'canary'
TrafficPercentage: $(Strategy.Increment)
postRouteTraffic:
steps:
- task: Bash@3
displayName: 'Monitor metrics'
inputs:
targetType: 'inline'
script: |
# Check error rates in Application Insights
# Fail if error rate > threshold
Rolling Deployment
# Deploy to multiple targets sequentially
- stage: DeployRolling
jobs:
- deployment: Deploy
environment: 'production'
strategy:
rolling:
maxParallel: 2
preDeploy:
steps:
- script: echo "Pre-deploy on $(Agent.MachineName)"
deploy:
steps:
- task: IISWebAppDeploymentOnMachineGroup@0
inputs:
WebSiteName: 'MyWebApp'
Package: '$(Pipeline.Workspace)/drop/**/*.zip'
on:
failure:
steps:
- script: echo "Rolling back on $(Agent.MachineName)"
Security Scanning
SAST (Static Application Security Testing)
# Security scanning stage
- stage: SecurityScan
jobs:
- job: SAST
steps:
# SonarCloud analysis
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'sonarcloud-connection'
organization: 'myorg'
scannerMode: 'MSBuild'
projectKey: 'myapp'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
inputs:
pollingTimeoutSec: '300'
- job: DependencyCheck
steps:
# OWASP Dependency Check
- task: DotNetCoreCLI@2
displayName: 'Restore for vulnerability scan'
inputs:
command: 'restore'
- task: Bash@3
displayName: 'Run dotnet list package --vulnerable'
inputs:
targetType: 'inline'
script: |
dotnet list package --vulnerable --include-transitive 2>&1 | tee vulnerability-report.txt
if grep -q "has the following vulnerable packages" vulnerability-report.txt; then
echo "##vso[task.logissue type=warning]Vulnerable packages detected"
# Optionally fail the build
# exit 1
fi
Container Scanning
# Trivy container scanning
- task: Bash@3
displayName: 'Scan container image'
inputs:
targetType: 'inline'
script: |
docker pull aquasec/trivy:latest
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL \
myregistry.azurecr.io/myapp:$(Build.BuildId)
Secret Scanning
# Prevent secrets in code
- task: CredScan@3
displayName: 'Run CredScan'
inputs:
toolMajorVersion: 'V2'
outputFormat: 'pre'
debugMode: false
- task: PostAnalysis@2
displayName: 'Post Analysis'
inputs:
CredScan: true
Monitoring Deployments
Application Insights Annotations
# Create deployment annotation
- task: AzureCLI@2
displayName: 'Create App Insights annotation'
inputs:
azureSubscription: 'azure-subscription'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az monitor app-insights component update-tags \
--app myapp-insights \
--resource-group myapp-rg \
--tags "Deployment=$(Build.BuildNumber)"
# Or use REST API for annotation
curl -X PUT \
-H "Content-Type: application/json" \
-H "X-AADTOKEN: $(accessToken)" \
-d '{
"AnnotationName": "Deployment",
"Category": "Deployment",
"Properties": {
"BuildNumber": "$(Build.BuildNumber)",
"Branch": "$(Build.SourceBranchName)"
}
}' \
"https://aigs1.aisvc.visualstudio.com/applications/$(APP_INSIGHTS_APP_ID)/Annotations"
Deployment Gates
# Query Application Insights for errors
- task: AzureMonitor@1
displayName: 'Check error rate'
inputs:
connectedServiceNameARM: 'azure-subscription'
ResourceGroupName: 'myapp-rg'
ResourceType: 'Application Insights'
ResourceName: 'myapp-insights'
AlertRules: 'Error rate above 5%'
Interview Questions
1. Explain the difference between YAML and Classic pipelines.
Answer:
| Feature | YAML | Classic |
|---|---|---|
| Version control | Pipeline as code in repo | Stored in Azure DevOps |
| Code review | PR reviews on changes | No review process |
| Portability | Copy between projects | Manual recreation |
| Templates | Full template support | Task groups (limited) |
| Recommendation | Use for new projects | Legacy support only |
YAML pipelines follow Infrastructure as Code principles - changes are trackable, reviewable, and portable.
2. How do you secure secrets in Azure Pipelines?
Answer:
- Variable Groups: Store secrets in Library, mark as secret
- Key Vault Integration: Reference Key Vault secrets directly
- Pipeline Variables: Mark individual variables as secret
- Service Connections: Use managed identity or certificate auth
Best practices:
- Never echo secrets in logs
- Use Key Vault for rotation
- Apply least-privilege access
- Audit secret access
3. What deployment strategies are available and when to use each?
Answer:
- Rolling: Deploy to targets sequentially. Use for VM deployments.
- Blue-Green: Deploy to slot, swap. Use for App Service, zero-downtime.
- Canary: Gradual traffic shift (10% → 25% → 50% → 100%). Use when you need to monitor impact.
- Recreate: Stop old, deploy new. Use for dev/test only.
For most Azure web apps, Blue-Green with slots is recommended.
4. How would you implement a gated deployment to production?
Answer:
- Configure Environment with approvals
- Add branch policy (only main → production)
- Add business hours check
- Require linked work items
- Add automated gates (health checks, App Insights queries)
# In environment settings:
# - Approvals: 2 required
# - Business hours: Mon-Fri 9am-5pm
# - Branch control: refs/heads/main only
5. Explain Bicep vs ARM templates vs Terraform.
Answer:
| Feature | ARM (JSON) | Bicep | Terraform |
|---|---|---|---|
| Syntax | Verbose JSON | Clean DSL | HCL |
| State | Azure manages | Azure manages | Remote state file |
| Multi-cloud | Azure only | Azure only | Yes |
| Learning curve | Steep | Easy | Moderate |
| Tooling | Basic | Excellent (VS Code) | Excellent |
Recommendations:
- New Azure projects: Bicep (cleaner than ARM, native)
- Multi-cloud: Terraform
- Existing ARM templates: Decompile to Bicep
Key Takeaways
- Use YAML pipelines - Version control, code review, templates
- Implement stages - Build → Dev → Staging → Production
- Use environments - Approvals, checks, deployment history
- Template everything - Reusable, maintainable pipelines
- Security first - Secret scanning, dependency check, container scan
- Infrastructure as Code - Bicep for Azure, Terraform for multi-cloud
- Blue-Green deployments - Zero-downtime with slot swaps
- Monitor deployments - App Insights annotations, deployment gates