☁️

Azure Devops Guide

Cloud & Azure Intermediate 5 min read 800 words

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

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:

  1. Variable Groups: Store secrets in Library, mark as secret
  2. Key Vault Integration: Reference Key Vault secrets directly
  3. Pipeline Variables: Mark individual variables as secret
  4. 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:

  1. Configure Environment with approvals
  2. Add branch policy (only main → production)
  3. Add business hours check
  4. Require linked work items
  5. 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

  1. Use YAML pipelines - Version control, code review, templates
  2. Implement stages - Build → Dev → Staging → Production
  3. Use environments - Approvals, checks, deployment history
  4. Template everything - Reusable, maintainable pipelines
  5. Security first - Secret scanning, dependency check, container scan
  6. Infrastructure as Code - Bicep for Azure, Terraform for multi-cloud
  7. Blue-Green deployments - Zero-downtime with slot swaps
  8. Monitor deployments - App Insights annotations, deployment gates

Further Reading

📚 Related Articles