Advanced AZ-400

AZ-400: Build and Release Pipelines

Create and configure YAML pipelines, testing strategies, deployments, package management and IaC.

Module 1 – CI/CD Pipeline Overview

DevOps vs SRE

  • DevOps: cultural practice of dev + ops collaboration to deliver quickly and with quality.
  • SRE (Site Reliability Engineering): discipline that applies software engineering to operations.
  • Fundamental tension: developers want to change frequently, ops want stability.
  • CI/CD pipelines resolve this tension by automating build + deployment + verification.

Module 2 – Creating and Configuring Pipelines (Part 1)

Classic vs YAML Pipelines

AspectClassicYAML
InterfaceGUI in Azure DevOpsCode (.yml in the repo)
VersioningNot versioned with codeVersioned with source code
ReusabilityTask GroupsYAML Templates
VisibilityPortal onlyVisible in the repo
RecommendationLegacyRecommended

Azure DevOps Pipeline Taxonomy

Pipeline
  └── Stages (optional, multiple in YAML)
        └── Jobs
              └── Steps (Tasks or Scripts)

Job Types

TypeDescriptionExamples
Agent JobsRuns on an agent (virtual machine)Compile, test, deploy
Agentless JobsRuns on Azure DevOps infrastructure (no agent required)API polling, delay, invoke Azure Function

Task Groups (Classic Pipelines)

  • Set of reusable tasks across multiple Classic pipelines.
  • Versionable: v1, v2, etc.
  • Modifying a Task Group → impacts all pipelines that use it.

Module 3 – Creating and Configuring Pipelines (Part 2)

YAML Pipeline Structure

trigger:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - task: DotNetCoreCLI@2
            inputs:
              command: build
              projects: '**/*.csproj'

Service Connections

  • Encapsulate credentials for external systems.
  • Example: connection to an Azure subscription (via Service Principal in Entra ID).
  • Used in deployment tasks with azureSubscription: '<ServiceConnectionName>'.
  • Allow pipelines to access Azure resources without storing credentials in plain text.

Advanced YAML Features

Build Numbering with Rev

name: '$(Rev:r)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)'

Checkout of Multiple Repos

steps:
  - checkout: git://MyOrg/Repo1
  - checkout: git://MyOrg/Repo2

Upstream Pipeline Artifacts

resources:
  pipelines:
    - pipeline: upstream
      source: 'CI Pipeline'
      trigger: true

YAML Templates

  • Extract stages, jobs, or steps into separate .yml files.
  • Modify once → impacts all pipelines that reference the template.
# template.yml
steps:
  - task: DotNetCoreCLI@2
    inputs:
      command: test

# main-pipeline.yml
steps:
  - template: templates/test-steps.yml

Library: Variable Groups and Secure Files

  • Variable Groups: centralize reusable variables across pipelines.
    • Linkable to Azure Key Vault for secret variables.
  • Secure Files: store sensitive files (certificates, signing keys).

Module 4 – Creating and Configuring Pipelines (Part 3)

Approvals and Checks

  • Attached to resources (service connections, environments) in YAML pipelines.
  • Approval types: Human approval (list of users).
  • Check types: Azure Monitor alerts, ServiceNow, Invoke REST API, Required template.

YAML Flow Control

jobs:
  - job: Deploy
    dependsOn: Build                    # Waits for Build to finish
    condition: succeeded('Build')       # Runs only if Build succeeded

Agent Types

TypeDescription
Microsoft-hostedDS2_v2 (2 vCPU, 7GB RAM). Always up-to-date, ephemeral, cost per minute.
Self-hostedMachine managed by the organization. Full control, custom software, private network access.
Managed DevOps PoolsAgent pool as-a-service: Microsoft manages the VMs, organization defines the specs.

GitHub Actions – Comparison with Azure Pipelines

AspectAzure PipelinesGitHub Actions
Triggertrigger:, schedules:on:
ReusabilityTemplatesReusable workflows
IntegrationService ConnectionsSecrets + OIDC
StagesYesNo (jobs only)
Matrix buildsstrategy: matrixstrategy: matrix
Hosted agentsAzure-hostedGitHub-hosted

GitHub Actions – Basic Structure

name: Build

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: dotnet build

Scheduled Workflows (GitHub Actions)

on:
  schedule:
    - cron: '0 2 * * 0'  # Sunday at 2am

Connecting GitHub Repos to Azure Pipelines

MethodDescription
GitHub App (recommended)Installed on the GitHub org, works with OAuth
OAuthAccess to all repos in the GitHub account
PATPersonal Access Token, fine-grained control

Module 5 – Testing Strategies

What Is Quality?

  • Functionality: do what was requested.
  • Time-to-market: deliver quickly.
  • Cost: don’t spend more than necessary.
  • Build the right thing: meet real user needs.

Test Types

TypeScopeSpeedExamples
UnitIsolated function or classVery fastxUnit, NUnit, Jest
IntegrationModule + dependenciesMediumDB tests, API
End-to-End (E2E)Entire applicationSlowPlaywright, Selenium
LoadPerformance under loadVariableAzure Load Testing
SecurityVulnerabilitiesVariableSAST, DAST, pen test
ExploratoryManual, improvisedVariableQA sessions

Test Progression

Inner loop (local IDE) 
  → Code Review (PR)
  → Build Pipeline (CI): unit + integration + security
  → Deployment Pipeline (CD): E2E + load + DAST

Azure Load Testing Service

  • Managed Azure service for load and performance testing.
  • Performance test: test system performance.
  • Load test: test performance under a high number of simultaneous users.
  • Generate load from multiple Azure regions.

Security Testing

Static Analysis (SAST/Whitebox)

  • GitHub Advanced Security:
    • Secret scanning.
    • Dependency Review (dependency graph + known vulnerabilities).
    • Code scanning (CodeQL).
  • Code is NOT executed.

Dynamic Testing (DAST/Blackbox)

  • OWASP ZAP (Zed Attack Proxy): automated, simulates an external attacker.
  • Penetration testing: manual, performed by security professionals.
  • Running application is required.

Module 6 – Deployments (Part 1)

CI vs CD

  • CI (Continuous Integration): verify code integration + generate an immutable artifact.
  • CD (Continuous Deployment): deploy the artifact without further modifications.
  • The build artifact should NOT change between environments (same binary, different config).

Classic Release Pipelines

  • Triggers: build artifact, repository, schedule.
  • Stages with pre/post conditions.
  • Reusable Task Groups.
  • Release Gates: automatically check before deploying (e.g., Azure Monitor, ServiceNow).

Multi-Stage YAML Pipeline (Deployments)

stages:
  - stage: Build
    jobs:
      - job: Build
        steps:
          - script: dotnet publish -o $(Build.ArtifactStagingDirectory)
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - stage: DeployDev
    dependsOn: Build
    jobs:
      - deployment: DeployToDev
        environment: dev
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop

Pipeline Decorators

  • Inject mandatory steps into all pipelines of an organization.
  • Use cases: compliance (security checks), cost allocation, blocking certain tasks.
  • Distributed as an Azure DevOps extension.

GitOps vs Traditional Pipelines

AspectPush (traditional)Pull (GitOps)
TriggerPipeline pushes to the clusterOperator watches Git and pulls changes
PermissionsPipeline needs write access to the clusterCluster pulls from Git (fewer pipeline privileges)
ToolsAzure Pipelines, GitHub ActionsFlux, ArgoCD

Module 7 – Deployments (Part 2)

Deployment Strategies

StrategyDescriptionAdvantagesDisadvantages
CanaryDeploy to a small % of servers/usersValidation in real conditionsComplex to manage
Blue-GreenTwo identical environments, switch trafficZero downtime, easy rollbackDoubled cost
RollingProgressive deployment server by serverZero downtimeSlow to deploy

Feature Toggles (Feature Flags)

  • Enable/disable features without redeployment.
  • Enables trunk-based development (main always deployable).
  • Example tool: LaunchDarkly.

Azure DevOps Environments

  • Group deployment resources (VM, K8s) with history + health checks.
  • Supports deployment strategies (rolling on VMs, canary on K8s).
  • Checks + approvals attached to the environment.

Databases and IaC

DACPAC vs BACPAC

FormatContentsUsage
DACPACSQL schema onlySchema deployments (prod)
BACPACSchema + dataFull export/import (dev/test)

Entity Framework Migrations

  • Generate a SQL script from EF migrations.
  • Run the script in the pipeline.
dotnet ef migrations script --output schema.sql

Module 8 – Maintaining Pipelines

Pipeline Optimization

  • Build solution file: use the .sln file rather than individual projects (parallel build).
  • Caching: cache NuGet, npm, etc. between builds.
  • Skip unchanged stages: only rebuild what has changed.
  • Split test runs: distribute tests across multiple agents in parallel.

Flaky Tests

  • Tests that intermittently pass and fail.
  • Dangers: false positives ignored → reduced confidence in the pipeline.
  • Solution: identify, isolate, fix or remove.
  • Azure DevOps: “Flaky tests” section in test reports.

Pipeline Retention

  • Configure in Project Settings:
    • Number of days of retention for all runs.
    • Specific retention for PRs.
    • Manual leases (keep specific runs).

Task Version Management

  • Pin task versions in the pipeline to avoid breaking changes.
  • Proactively update SDK versions.
  • Prohibit global PATs.
  • Prohibit full-scope PATs.
  • Configured maximum lifetime.
  • Entra ID conditional access enabled.

Classic → YAML Migration

  • In the Azure Pipelines portal → ellipsis menu → Export to YAML.
  • Manual review required after export.

Module 9 – Package Management

Azure Artifacts

  • Supports: NuGet, npm, Maven, pip (Python), Cargo (Rust), Universal Packages.
  • Feeds: package container (scope = organization or project).
  • Views: Local (default), Prerelease, Release. Promote packages between views.
  • Upstreams: cache public packages (nuget.org, npmjs.com) in the private feed.
  • Immutable packages: once published, a version cannot be modified.

Semantic Versioning

  • Format: MAJOR.MINOR.PATCH.
    • MAJOR: breaking changes.
    • MINOR: new compatible features.
    • PATCH: bug fixes.

GitHub Packages vs Azure Artifacts

AspectGitHub PackagesAzure Artifacts
FormatsNuGet, npm, Maven, containersNuGet, npm, Maven, pip, Cargo, Universal
FeedsNoYes (org + project)
ViewsNoYes (Local/Prerelease/Release)
UpstreamsNoYes

Modules 10-11 – Infrastructure as Code (IaC)

Azure Resource Manager (ARM)

  • All Azure APIs go through ARM.
  • ARM → Resource Providers → Resources.
  • ARM/Bicep templates = declaration of desired state.

Bicep DSL

  • Designed for Azure only.
  • Extensions available for Entra ID and Kubernetes.
  • Uses symbolic names (internal references within the file).

Azure Deployment Environments

  • The platform team defines blueprints (ARM/Terraform templates).
  • The application team deploys environments without write access to resources.
  • Avoids manual and ad-hoc provisioning.

Azure Automation

  • Runbooks: PowerShell or Python scripts, executable on schedule or on demand.
  • Use cases: auto-shutdown/startup of VMs, maintenance, reporting.

Azure Policy

  • JSON-based: policyRule (if condition → effect).
  • Effects: Audit (log only), Deny (block), DeployIfNotExists (automatically deploy).
  • Assignment to a scope (subscription, resource group, management group).

Key Points for the Exam

  • Classic pipelines: GUI-based, not versioned, Task Groups.
  • YAML pipelines: code-first, Templates for reuse, recommended.
  • Service Connections: encapsulate credentials for external systems.
  • Approvals and Checks: attached to resources/environments in YAML.
  • GitOps: pull-based, Flux/ArgoCD, fewer pipeline privileges.
  • Canary / Blue-Green / Rolling: progressive deployment strategies.
  • DACPAC = SQL schema only; BACPAC = schema + data.
  • Azure Artifacts Feeds: Views (Local/Prerelease/Release) + Upstreams for public caching.
  • Pipeline Decorators: inject mandatory steps into all pipelines via extension.
  • Managed DevOps Pools: agent pool as-a-service (Microsoft manages the VMs).

Complete CI/CD Architecture — Reference Diagrams

Global CI/CD Pipeline Flow

flowchart LR
    DEV[Developer] -->|git push| REPO["(Azure Repos\nor GitHub)"]
    REPO -->|trigger CI| BUILD[Build Pipeline\nCI Stage]

    subgraph CI
        BUILD --> COMPILE[Compile]
        COMPILE --> TEST[Unit Tests\nIntegration Tests]
        TEST --> ANALYZE[Static Analysis\nSonarQube / CodeQL]
        ANALYZE --> PACKAGE[Package\nImmutable Artifact]
        PACKAGE --> PUBLISH[Publish\nAzure Artifacts]
    end

    subgraph CD
        PUBLISH -->|trigger CD| DEV_ENV[Deploy → Dev\nSmoke Tests]
        DEV_ENV -->|gate: tests passed| QA_ENV[Deploy → Test/QA\nE2E Tests]
        QA_ENV -->|gate: approval| PROD_ENV[Deploy → Production\nPost-deploy Load Tests]
    end

    PROD_ENV --> MONITOR[Azure Monitor\nApplication Insights]
    MONITOR -->|feedback| DEV

Artifact Lifecycle

flowchart TD
    SRC[Source Code\ngit push to main] --> BUILD_STAGE[Build Stage\ndotnet publish]
    BUILD_STAGE --> ARTIFACT[ZIP Artifact\nimmutable drop]
    ARTIFACT --> FEED[Azure Artifacts Feed\nversion 1.2.3]

    FEED -->|download| DEV_DEPLOY[Deploy Dev\nSlot: dev]
    FEED -->|download| QA_DEPLOY[Deploy QA\nSlot: qa]
    FEED -->|download| PROD_DEPLOY[Deploy Prod\nSlot: staging → swap]

    subgraph Azure Artifacts Views
        FEED --> LOCAL[View: Local\nall versions]
        FEED --> PRERELEASE[View: Prerelease\ntested versions]
        FEED --> RELEASE[View: Release\nprod-validated versions]
    end

YAML Pipelines — Complete Reference

Triggers

# CI trigger: push on specific branches
trigger:
  branches:
    include:
      - main
      - release/*
      - feature/*
    exclude:
      - experimental/*
  tags:
    include:
      - v*           # Triggers on version tags (v1.0, v2.3.1)
  paths:
    include:
      - src/**       # Only if src/ changes
    exclude:
      - docs/**
      - '*.md'

# PR trigger
pr:
  branches:
    include:
      - main
      - release/*
  paths:
    include:
      - src/**

# Scheduled trigger (cron)
schedules:
  - cron: "0 0 * * *"      # Midnight every day
    displayName: Daily Build
    branches:
      include:
        - main
    always: true            # Force even without changes

# Upstream pipeline trigger
resources:
  pipelines:
    - pipeline: infra-pipeline
      source: 'Infrastructure Pipeline'
      trigger:
        branches:
          include:
            - main

Variables and Variable Groups

variables:
  # Simple inline variable
  - name: buildConfiguration
    value: 'Release'
  
  # Secret variable (masked in logs)
  - name: apiKey
    value: $(API_KEY)
    
  # Shared Variable Group
  - group: Production-Secrets
  
  # Conditions on variables
  - ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
    - name: environmentName
      value: 'production'
  - ${{ else }}:
    - name: environmentName
      value: 'development'

# Usage in steps
steps:
  - script: |
      echo "Configuration: $(buildConfiguration)"
      echo "Environment: $(environmentName)"
    displayName: Print Variables

Matrix Strategy — Parallel Tests

strategy:
  matrix:
    dotnet8_ubuntu:
      vmImage: 'ubuntu-latest'
      dotnetVersion: '8.0.x'
    dotnet8_windows:
      vmImage: 'windows-latest'
      dotnetVersion: '8.0.x'
    dotnet9_ubuntu:
      vmImage: 'ubuntu-latest'
      dotnetVersion: '9.0.x'
  maxParallel: 3    # Limit parallelism

pool:
  vmImage: $(vmImage)

steps:
  - task: UseDotNet@2
    inputs:
      version: $(dotnetVersion)
  - task: DotNetCoreCLI@2
    inputs:
      command: test

Checkout Multiple Repos

resources:
  repositories:
    - repository: CommonTemplates
      type: git
      name: MyOrg/SharedPipelineTemplates
      ref: refs/heads/main
    - repository: InfraCode
      type: github
      name: myorg/infrastructure
      endpoint: GitHubConnection    # Service Connection

steps:
  - checkout: self                  # Pipeline repo
    fetchDepth: 0                   # Full history (for GitVersion)
  
  - checkout: CommonTemplates       # Templates repo
    path: templates
  
  - checkout: InfraCode             # Infra repo
    path: infra

  - template: templates/build-steps.yml@CommonTemplates
    parameters:
      buildConfig: Release

Deployment Jobs with Environments

- stage: DeployToProduction
  displayName: 'Deploy to Production'
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
    - deployment: DeployWebApp
      displayName: 'Deploy Web Application'
      environment:
        name: production
        resourceType: VirtualMachine   # Or: Kubernetes
        tags: 'web-server'             # Filter by tag
      pool:
        vmImage: ubuntu-latest
      strategy:
        rolling:
          maxParallel: 2              # Deploy to 2 VMs at a time
          preDeploy:
            steps:
              - script: echo "Preparing deployment"
          deploy:
            steps:
              - download: current
                artifact: drop
              - task: AzureWebApp@1
                inputs:
                  azureSubscription: 'Production-SC'
                  appName: 'my-app-prod'
                  package: $(Pipeline.Workspace)/drop/*.zip
          routeTraffic:
            steps:
              - script: echo "Routing traffic to new version"
          postRouteTraffic:
            steps:
              - script: |
                  # Post-deployment smoke test
                  response=$(curl -s -o /dev/null -w "%{http_code}" https://my-app-prod.azurewebsites.net/health)
                  if [ "$response" != "200" ]; then
                    echo "Health check failed: $response"
                    exit 1
                  fi
                  echo "Health check passed: $response"
          on:
            failure:
              steps:
                - script: echo "Deployment failed - initiating rollback"
            success:
              steps:
                - script: echo "Deployment successful!"

Advanced Conditions

stages:
  - stage: Build
    jobs:
      - job: Build
        steps: [...]

  - stage: DeployDev
    dependsOn: Build
    # Deploy only if Build succeeded AND branch = main OR feature
    condition: |
      and(
        succeeded('Build'),
        or(
          eq(variables['Build.SourceBranch'], 'refs/heads/main'),
          startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/')
        )
      )
    jobs: [...]

  - stage: DeployProd
    dependsOn: [Build, DeployDev]
    # Deploy only from main, after success of both previous stages
    condition: |
      and(
        succeeded('Build'),
        succeeded('DeployDev'),
        eq(variables['Build.SourceBranch'], 'refs/heads/main'),
        ne(variables['Build.Reason'], 'PullRequest')
      )
    jobs: [...]

  - stage: Notify
    dependsOn: [DeployDev, DeployProd]
    # Always notify, even if deployments failed
    condition: always()
    jobs:
      - job: SendNotification
        steps:
          - script: |
              if [ "$(Agent.JobStatus)" == "Failed" ]; then
                echo "Sending failure notification"
              else
                echo "Sending success notification"
              fi

Tests in Pipelines — Detailed Implementation

Test Pyramid and Strategy

flowchart TD
    subgraph Test Pyramid
        E2E[E2E Tests\nSlower, fewer\nPlaywright / Selenium]
        INT[Integration Tests\nAPI, DB, service tests\nWebApplicationFactory]
        UNIT[Unit Tests\nFast, isolated\nxUnit / NUnit / MSTest]
    end

    subgraph Feedback Loops
        IDE[IDE / Local\nContinuous unit tests]
        PR[Pull Request\nUnit + Integration]
        CI[CI Build\nAll except slow E2E]
        NIGHTLY[Nightly Build\nFull E2E + Load]
    end

    UNIT --> IDE
    UNIT & INT --> PR
    UNIT & INT & E2E --> CI
    UNIT & INT & E2E --> NIGHTLY

Complete Test Implementation in Azure Pipelines

# Complete test pipeline
steps:
  # ─── Unit Tests ───────────────────────────────────────────────────────────
  - task: DotNetCoreCLI@2
    displayName: 'Run Unit Tests'
    inputs:
      command: 'test'
      projects: '**/*.UnitTests.csproj'
      arguments: >
        --configuration Release
        --no-build
        --collect:"XPlat Code Coverage"
        --results-directory $(Agent.TempDirectory)/TestResults
        --logger trx;LogFileName=unit-tests.trx
      publishTestResults: false    # Publish manually for more control

  # ─── Integration Tests ────────────────────────────────────────────────────
  - task: DotNetCoreCLI@2
    displayName: 'Run Integration Tests'
    inputs:
      command: 'test'
      projects: '**/*.IntegrationTests.csproj'
      arguments: >
        --configuration Release
        --no-build
        --results-directory $(Agent.TempDirectory)/TestResults
        --logger trx;LogFileName=integration-tests.trx
      publishTestResults: false

  # ─── Publish results ─────────────────────────────────────────────────────
  - task: PublishTestResults@2
    displayName: 'Publish Test Results'
    condition: always()    # Publish even if tests fail
    inputs:
      testResultsFormat: 'VSTest'
      testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'
      mergeTestResults: true
      failTaskOnFailedTests: true
      testRunTitle: '$(Build.BuildNumber) - Tests'

  # ─── Code coverage ───────────────────────────────────────────────────────
  - task: PublishCodeCoverageResults@1
    displayName: 'Publish Code Coverage'
    inputs:
      codeCoverageTool: 'Cobertura'
      summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
      reportDirectory: '$(Agent.TempDirectory)/CoverageReport'
      failIfCoverageEmpty: true

Azure Load Testing Service — Configuration

# Integrate Azure Load Testing in the pipeline
- task: AzureLoadTesting@1
  displayName: 'Run Load Test'
  inputs:
    azureSubscription: 'AzureServiceConnection'
    loadTestConfigFile: 'loadtest/config.yaml'
    resourceGroup: 'loadtesting-rg'
    loadTestResource: 'my-load-test'
    env: |
      [
        {
          "name": "webapp_url",
          "value": "https://my-app-test.azurewebsites.net"
        }
      ]
    secrets: |
      [
        {
          "name": "api_key",
          "value": "$(API_KEY)"
        }
      ]
    failCriteria: |
      [
        "avg(response_time_ms) > 2000",
        "percentage(error) > 5"
      ]
# config.yaml file for Azure Load Testing
version: v0.1
testplan: scenarios/main.jmx    # JMeter or
# testplan: scenarios/main.js   # k6
engineInstances: 3
description: "Load test for checkout flow"
failureCriteria:
  - "avg(response_time_ms) > 2000"
  - "percentage(error) > 5"
  - "percentage(error) > 10 for GetCheckout"
autoStop:
  errorPercentage: 90
  timeWindow: 60

Infrastructure as Code — Bicep and ARM

Complete Bicep Template — App Service + SQL + Key Vault

// main.bicep - Complete infrastructure for a web application
@description('Environment name (dev, test, prod)')
@allowed(['dev', 'test', 'prod'])
param environment string

@description('Azure region')
param location string = resourceGroup().location

@description('Application name (prefix for resources)')
param appName string

@description('App Service Plan SKU')
param appServiceSku string = environment == 'prod' ? 'S1' : 'F1'

var resourcePrefix = '${appName}-${environment}'
var tags = {
  Environment: environment
  Application: appName
  ManagedBy: 'Bicep'
}

// ─── App Service Plan ──────────────────────────────────────────────────────
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: '${resourcePrefix}-asp'
  location: location
  tags: tags
  sku: {
    name: appServiceSku
  }
  kind: 'linux'
  properties: {
    reserved: true    // For Linux
  }
}

// ─── App Service ───────────────────────────────────────────────────────────
resource appService 'Microsoft.Web/sites@2023-01-01' = {
  name: '${resourcePrefix}-app'
  location: location
  tags: tags
  identity: {
    type: 'SystemAssigned'    // Managed Identity for Key Vault
  }
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|8.0'
      alwaysOn: environment == 'prod'
      minTlsVersion: '1.2'
      ftpsState: 'Disabled'
      healthCheckPath: '/health'
      appSettings: [
        {
          name: 'ASPNETCORE_ENVIRONMENT'
          value: environment == 'prod' ? 'Production' : 'Development'
        }
        {
          name: 'KeyVaultUri'
          value: keyVault.properties.vaultUri
        }
      ]
    }
    httpsOnly: true
  }
}

// ─── SQL Server + Database ─────────────────────────────────────────────────
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
  name: '${resourcePrefix}-sql'
  location: location
  tags: tags
  properties: {
    administratorLogin: 'sqladmin'
    administratorLoginPassword: sqlAdminPassword
    minimalTlsVersion: '1.2'
    publicNetworkAccess: environment == 'prod' ? 'Disabled' : 'Enabled'
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
  parent: sqlServer
  name: '${resourcePrefix}-db'
  location: location
  tags: tags
  sku: {
    name: environment == 'prod' ? 'S2' : 'Basic'
    tier: environment == 'prod' ? 'Standard' : 'Basic'
  }
}

// ─── Key Vault ─────────────────────────────────────────────────────────────
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: '${resourcePrefix}-kv'
  location: location
  tags: tags
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
    enableRbacAuthorization: true    // RBAC instead of Access Policies
    publicNetworkAccess: 'Enabled'
  }
}

// Grant App Service access to Key Vault via RBAC
resource kvAccessApp 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(keyVault.id, appService.id, '4633458b-17de-408a-b874-0445c86b69e6')
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')    // Key Vault Secrets User
    principalId: appService.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

// ─── Outputs ───────────────────────────────────────────────────────────────
output appServiceUrl string = 'https://${appService.properties.defaultHostName}'
output keyVaultUri string = keyVault.properties.vaultUri
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName

Deploy Bicep with Azure Pipelines

stages:
  - stage: ValidateInfra
    displayName: 'Validate Infrastructure'
    jobs:
      - job: WhatIf
        steps:
          - task: AzureCLI@2
            displayName: 'Bicep What-If'
            inputs:
              azureSubscription: 'AzureServiceConnection'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                # Syntax validation
                az bicep build --file infra/main.bicep
                
                # What-if: see changes without deploying
                az deployment group what-if \
                  --resource-group "myapp-$(environmentName)-rg" \
                  --template-file infra/main.bicep \
                  --parameters \
                    environment=$(environmentName) \
                    appName=myapp \
                    sqlAdminPassword=$(SQL_ADMIN_PASSWORD) \
                  --result-format FullResourcePayloads

  - stage: DeployInfra
    displayName: 'Deploy Infrastructure'
    dependsOn: ValidateInfra
    condition: succeeded()
    jobs:
      - deployment: DeployBicep
        environment: production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureResourceManagerTemplateDeployment@3
                  displayName: 'Deploy Bicep Template'
                  inputs:
                    deploymentScope: 'Resource Group'
                    azureResourceManagerConnection: 'AzureServiceConnection'
                    subscriptionId: '$(SUBSCRIPTION_ID)'
                    action: 'Create Or Update Resource Group'
                    resourceGroupName: 'myapp-prod-rg'
                    location: 'eastus'
                    templateLocation: 'Linked artifact'
                    csmFile: '$(Build.SourcesDirectory)/infra/main.bicep'
                    csmParametersFile: '$(Build.SourcesDirectory)/infra/parameters.prod.json'
                    overrideParameters: '-sqlAdminPassword $(SQL_ADMIN_PASSWORD)'
                    deploymentMode: 'Incremental'
                    deploymentOutputs: 'infraOutputs'
                
                # Extract ARM outputs for subsequent steps
                - task: PowerShell@2
                  displayName: 'Extract Infra Outputs'
                  inputs:
                    targetType: 'inline'
                    script: |
                      $outputs = '$(infraOutputs)' | ConvertFrom-Json
                      Write-Host "App URL: $($outputs.appServiceUrl.value)"
                      Write-Host "##vso[task.setvariable variable=appServiceUrl;isOutput=true]$($outputs.appServiceUrl.value)"

Azure Policy — Detailed Examples

// Policy: Deny resources outside allowed regions
{
  "mode": "All",
  "policyRule": {
    "if": {
      "not": {
        "field": "location",
        "in": [
          "eastus",
          "westus2",
          "northeurope",
          "westeurope"
        ]
      }
    },
    "then": {
      "effect": "deny"
    }
  }
}
// Policy: Require Environment tag on all resources
{
  "mode": "Indexed",
  "policyRule": {
    "if": {
      "field": "tags['Environment']",
      "exists": "false"
    },
    "then": {
      "effect": "modify",
      "details": {
        "operations": [
          {
            "operation": "addOrReplace",
            "field": "tags['Environment']",
            "value": "[resourceGroup().tags['Environment']]"
          }
        ],
        "roleDefinitionIds": [
          "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
        ]
      }
    }
  }
}
# Deploy an Azure policy via CLI
az policy definition create \
  --name "require-env-tag" \
  --display-name "Require Environment Tag" \
  --description "Requires all resources to have an Environment tag" \
  --rules policy-rules.json \
  --mode Indexed

# Assign the policy to a resource group
az policy assignment create \
  --name "require-env-tag-assignment" \
  --policy "require-env-tag" \
  --scope "/subscriptions/{sub}/resourceGroups/myRG" \
  --display-name "Require Environment Tag in MyRG"

# Check compliance
az policy state list \
  --resource-group "myRG" \
  --query "[?complianceState=='NonCompliant'].{Policy:policyDefinitionName,Resource:resourceId}" \
  --output table

GitHub Actions — Complete Guide

Terminology Comparison: Azure Pipelines vs GitHub Actions

Azure PipelinesGitHub ActionsNotes
PipelineWorkflowYAML file
Triggeron:Trigger
StageDoes not exist in GHA
JobJobSimilar
Step / TaskStepSimilar
Agent Poolruns-on:Machine type
Service ConnectionSecret + OIDCAzure authentication
Variable GroupSecret / Env Variables
TemplateReusable workflowReuse
ArtifactArtifactOutputs between jobs

Complete GitHub Actions Workflow — CI/CD to Azure

name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 0'    # Sunday at midnight (UTC)

env:
  AZURE_WEBAPP_NAME: my-app-prod
  DOTNET_VERSION: '8.0.x'

jobs:
  # ─── Build and Test ───────────────────────────────────────────────────────
  build-and-test:
    name: Build and Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0    # Full history for GitVersion

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      # Cache NuGet packages
      - name: Cache NuGet packages
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

      - name: Restore dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore --configuration Release

      - name: Run tests
        run: |
          dotnet test --no-build --configuration Release \
            --collect:"XPlat Code Coverage" \
            --results-directory ./TestResults \
            --logger trx

      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: '.NET Tests'
          path: 'TestResults/**/*.trx'
          reporter: 'dotnet-trx'

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: '**/coverage.cobertura.xml'

      # Publish the application
      - name: Publish application
        run: dotnet publish --configuration Release --output ./publish

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-artifact
          path: ./publish
          retention-days: 5

  # ─── Security Scan ────────────────────────────────────────────────────────
  security-scan:
    name: Security Scan
    needs: build-and-test
    runs-on: ubuntu-latest

    permissions:
      security-events: write    # Required for GitHub Advanced Security

    steps:
      - uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: csharp

      - name: Build for CodeQL
        run: dotnet build

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2

  # ─── Deploy to Dev ────────────────────────────────────────────────────────
  deploy-dev:
    name: Deploy to Development
    needs: [build-and-test, security-scan]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: development
      url: https://my-app-dev.azurewebsites.net

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: app-artifact
          path: ./artifact

      - name: Azure Login (OIDC)
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v2
        with:
          app-name: my-app-dev
          package: ./artifact

  # ─── Deploy to Production ─────────────────────────────────────────────────
  deploy-prod:
    name: Deploy to Production
    needs: deploy-dev
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://my-app-prod.azurewebsites.net
    # The 'production' environment has reviewers configured in GitHub Settings

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: app-artifact
          path: ./artifact

      - name: Azure Login (OIDC)
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Staging Slot
        uses: azure/webapps-deploy@v2
        with:
          app-name: my-app-prod
          slot-name: staging
          package: ./artifact

      - name: Smoke Test on Staging
        run: |
          sleep 30    # Wait for app to start
          status=$(curl -s -o /dev/null -w "%{http_code}" https://my-app-prod-staging.azurewebsites.net/health)
          if [ "$status" != "200" ]; then
            echo "Health check failed: $status"
            exit 1
          fi
          echo "Health check passed!"

      - name: Swap Staging to Production
        uses: azure/cli@v1
        with:
          inlineScript: |
            az webapp deployment slot swap \
              --resource-group my-rg \
              --name my-app-prod \
              --slot staging \
              --target-slot production

OIDC Authentication — Connecting to Azure Without Credentials

# Configure Azure for OIDC from GitHub Actions
# Create an App Registration
APP_ID=$(az ad app create --display-name "GitHub-Actions-OIDC" --query appId -o tsv)

# Create the service principal
az ad sp create --id $APP_ID

# Assign a role
az role assignment create \
  --role "Contributor" \
  --assignee $APP_ID \
  --scope "/subscriptions/$(az account show --query id -o tsv)"

# Configure federated credentials (OIDC)
az ad app federated-credential create \
  --id $APP_ID \
  --parameters '{
    "name": "github-main-branch",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:myorg/myrepo:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'
# In GitHub Actions workflow: OIDC usage
permissions:
  id-token: write    # REQUIRED for OIDC
  contents: read

steps:
  - name: Azure Login
    uses: azure/login@v1
    with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}        # App ID
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}        # Directory ID
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      # No client_secret! OIDC = zero stored credentials

Advanced Package Management

Creating and Publishing a NuGet Package

// MyLibrary.csproj — Configuration for NuGet publishing
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PackageId>MyOrg.MyLibrary</PackageId>
    <Version>$(GitVersion_SemVer)</Version>    <!-- Automatic GitVersion -->
    <Authors>Our Team</Authors>
    <Description>Reusable library for business calculations</Description>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RepositoryUrl>https://github.com/myorg/myrepo</RepositoryUrl>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
</Project>
# Pipeline to publish to Azure Artifacts
- stage: PublishPackage
  jobs:
    - job: Publish
      steps:
        - task: gitversion/setup@0
          inputs:
            versionSpec: '5.x'

        - task: gitversion/execute@0
          displayName: 'Determine Version'

        - script: |
            echo "Building version: $(GitVersion.SemVer)"
          displayName: 'Print Version'

        - task: DotNetCoreCLI@2
          displayName: 'Pack NuGet Package'
          inputs:
            command: 'pack'
            packagesToPack: '**/MyLibrary.csproj'
            versioningScheme: 'byEnvVar'
            versionEnvVar: 'GitVersion.SemVer'
            packDirectory: '$(Build.ArtifactStagingDirectory)/packages'

        - task: NuGetAuthenticate@1
          displayName: 'Authenticate to Azure Artifacts'

        - task: DotNetCoreCLI@2
          displayName: 'Push to Azure Artifacts'
          inputs:
            command: 'push'
            packagesToPush: '$(Build.ArtifactStagingDirectory)/packages/*.nupkg'
            nuGetFeedType: 'internal'
            publishVstsFeed: 'MyOrg/my-packages-feed'
            allowPackageConflicts: false    # Fail if version already exists

Dependency Scanning with GitHub Advanced Security

# GitHub Actions: enable Dependency Review
name: Dependency Review

on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Dependency Review
        uses: actions/dependency-review-action@v3
        with:
          fail-on-severity: moderate
          deny-licenses: GPL-3.0, AGPL-3.0    # Block copyleft licenses
          allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause
          comment-summary-in-pr: true
# Trivy: vulnerability scanning in containers
- name: Build Container Image
  run: docker build -t my-app:latest .

- name: Scan Container with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'my-app:latest'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'    # Fail if critical/high vulnerabilities found

- name: Upload Trivy Results to GitHub Security
  uses: github/codeql-action/upload-sarif@v2
  with:
    sarif_file: 'trivy-results.sarif'

Review Questions — Build and Release Pipelines

Q1 — YAML vs Classic Pipelines

Question: Your team wants pipeline changes to go through the same code review process as application changes. Which approach do you use?

  • A. Classic Pipelines with versioned Task Groups
  • B. YAML Pipelines versioned in the same repository as the code
  • C. Classic Pipelines with JSON export stored in Git
  • D. YAML Pipelines in a separate repository

Explanation: YAML Pipelines store the pipeline definition in a .yml file in the code repository. This file follows the same versioning process (branches, PRs, code reviews) as the rest of the code. Classic Pipelines are stored in Azure DevOps and cannot be reviewed via PR.


Q2 — Agents

Question: Your pipeline deploys applications to servers in a private network not accessible from the Internet. Which agent type do you use?

  • A. Microsoft-hosted agent with VPN Gateway
  • B. Self-hosted agent installed in the private network
  • C. Microsoft-hosted agent with Private Endpoints
  • D. Managed DevOps Pool with VNet peering

Explanation: Self-hosted agents are installed on machines in your private network. The agent contacts Azure DevOps (outbound HTTPS connection), then receives pipeline instructions. This does not require opening inbound ports. Microsoft-hosted agents are in public Microsoft networks and cannot directly access private resources without complex network configuration.


Q3 — Approvals and Gates

Question: You want to prevent deployment to production if Azure Monitor alerts are active on the test environment. In which scope do you configure this check in a YAML pipeline?

  • A. In the production deployment stage, condition: section
  • B. In the production deployment stage, pre-deployment approvals section
  • C. On the service connection or environment in Azure DevOps settings
  • D. In an agentless job with a verification task

Explanation: In YAML pipelines, Approvals and Checks are configured on protected resources (environments, service connections, variable groups, agent pools), not in the pipeline YAML. The resource owner determines the conditions of use. This enables centralized control independent of individual pipelines.


Q4 — Package Views

Question: Your team publishes NuGet packages to Azure Artifacts. You want only production-approved packages to be consumed by deployment pipelines. Which Azure Artifacts feature do you use?

  • A. Upstream sources with version filtering
  • B. Separate feeds for each environment
  • C. Views (Local, Prerelease, Release) with manual promotion
  • D. Package locking in configuration files

Explanation: Azure Artifacts Views control which package versions are visible based on their maturity level. You first publish to the Local view (default), then promote tested versions to Prerelease, then to Release when validated. Production pipelines configure their feed to use @release, which only includes promoted packages.


Q5 — Deployment Strategies

Question: Your critical application must be deployed with the ability to roll back immediately (in under 1 minute) if issues are detected. Zero downtime is mandatory. Which strategy do you choose?

  • A. Rolling deployment on 2 instances
  • B. Canary deployment with progressive routing
  • C. Blue/Green deployment with App Service deployment slots
  • D. Recreate deployment with maintenance window

Explanation: Blue/Green deployment with App Service deployment slots allows:

  1. Deploy the new version to the staging slot (Green)
  2. Test on staging
  3. Instant swap (< 30 seconds) to production
  4. If issues arise: immediate re-swap to old version

Zero downtime guaranteed because the swap redirects traffic at the infrastructure level, with no service interruption.


Q6 — GitOps

Question: You use Azure Kubernetes Service to host your microservices. Your security team requires that CI/CD pipelines must not have write-access credentials to the Kubernetes cluster. Which approach do you recommend?

  • A. Use a Managed Identity for the pipeline with limited access
  • B. Store kubeconfig in Azure Key Vault and load on demand
  • C. Implement GitOps with Flux or ArgoCD
  • D. Create a minimal Kubernetes Service Account for the pipeline

Explanation: GitOps reverses the deployment model: instead of the pipeline pushing to the cluster (requires write credentials), an operator in the cluster pulls manifests from a Git repository. The pipeline only needs read access to the cluster for validation, never write access. The operator (Flux/ArgoCD) in the cluster automatically manages deployments when Git changes.


Q7 — DACPAC vs BACPAC

Question: You want to deploy only schema changes to an Azure SQL database (new tables, indexes, stored procedures) to production, without modifying existing data. Which format do you use?

  • A. BACPAC with the --schema-only option
  • B. DACPAC with SqlPackage.exe /Action:Publish
  • C. Entity Framework dotnet ef database update
  • D. Manually generated SQL Script

Explanation: A DACPAC (Data Application Package) contains only the schema. SqlPackage.exe /Action:Publish compares the DACPAC with the target database and generates the differential migration script to align schemas. It never affects existing data. A BACPAC contains schema + data and is used for full backups.


Q8 — Flaky Tests

Question: Your pipeline reports show that a test fails in about 20% of runs without any code changes. How do you handle this test?

  • A. Increase the test timeout
  • B. Mark the test as [Ignored] to stop running it
  • C. Identify the cause (random data, concurrency, external dependency) and fix or remove
  • D. Move the test to a separate nightly pipeline

Explanation: A flaky test creates false positives. The recommended policy is “fix or delete”:

  1. Identify the cause: poorly generated random data, timing dependencies, concurrency
  2. Fix if possible
  3. If the fix is not immediate: remove the test rather than keep it flaky

A flaky test is worse than no test because it conditions the team to ignore failures, allowing real bugs to go unnoticed.


Glossary — Build and Release Pipelines

TermDefinition
AgentMachine running pipeline jobs
Agentless JobJob running in Azure DevOps infrastructure (polling, delay, API call)
ArtifactImmutable pipeline output (binary, package, zip)
BACPACSQL package containing schema + data (full backup)
DACPACSQL package containing schema only
CanaryDeployment to a subset of servers/users for validation
Deployment SlotParallel environment for an App Service (staging, canary…)
Flaky TestTest with non-deterministic result (passes/fails for no reason)
GateAutomatic check before/after a deployment
GitOpsPull-based deployment model from Git (Flux, ArgoCD)
OIDCOpen ID Connect — Azure authentication without storing a secret
PATPersonal Access Token — non-interactive authentication
Pipeline DecoratorExtension injecting mandatory tasks into all pipelines
Release GateAutomatic condition to progress to the next stage (Classic)
Self-hosted AgentAgent on infrastructure managed by the organization
Service ConnectionEncapsulated credentials for connecting to external systems
Task GroupSet of reusable, versionable Classic pipeline tasks
YAML TemplateReusable YAML file (steps, jobs, stages) across pipelines

Search Terms

az-400 · release · pipelines · azure · devops · iac · microsoft · github · pipeline · yaml · actions · classic · testing · bicep · ci/cd · deployment · package · service · test · tests · artifacts · configuring · deployments · environments

Interested in this course?

Contact us to book it or get a custom training plan for your team.