Day: July 16, 2025
Building Production-Ready Release Pipelines in Azure: A Step-by-Step Guide using Arm Templates
Creating enterprise-grade release pipelines in Azure requires a comprehensive understanding of Azure DevOps services, proper configuration, and adherence to production best practices. This detailed guide will walk you through building a robust CI/CD pipeline that deploys applications to Azure App Services with slot-based deployments for zero-downtime releases.
Architecture Overview
Our production pipeline will deploy a .NET web application to Azure App Service using deployment slots for blue/green deployments. The pipeline includes multiple environments (development, staging, production) with automated testing, security scanning, and manual approval gates.
Azure Repos → Build Pipeline (Azure Pipelines) → Dev Deployment → Automated Tests → Staging Deployment → Security Scan → Manual Approval → Production Deployment (Slot Swap) → Post-Deployment Monitoring
Prerequisites
Before starting, ensure you have:
- Azure subscription with sufficient permissions
- Azure DevOps organization and project
- .NET application in Azure Repos (or GitHub)
- Understanding of Azure Resource Manager (ARM) templates
- Azure CLI installed locally
Understanding Azure Deployment Slots
Before diving into infrastructure setup, it’s crucial to understand Azure deployment slots – a key feature that enables zero-downtime deployments and advanced deployment strategies.
What Are Deployment Slots?
Azure App Service deployment slots are live instances of your web application with their own hostnames. Think of them as separate environments that share the same App Service plan but can run different versions of your application.
- Production slot: Your main application (e.g.,
myapp.azurewebsites.net) - Staging slot: A separate instance (e.g.,
myapp-staging.azurewebsites.net) - Additional slots: Canary, testing, or feature-specific environments
Why Use Deployment Slots?
1. Deploy new version to staging slot
2. Test the staging slot thoroughly
3. Swap staging and production slots instantly
4. If issues arise, swap back immediately (rollback)
Key Benefits:
- Zero-downtime deployments: Instant traffic switching
- Blue/green deployments: Run two versions simultaneously
- A/B testing: Route percentage of traffic to different versions
- Warm-up validation: Test in production environment before going live
- Quick rollbacks: Instant revert if problems occur
Step 1: Infrastructure Setup – Choose Your Approach
Azure offers two primary Infrastructure as Code (IaC) approaches for managing resources including deployment slots:
- ARM Templates/Bicep: Azure’s native IaC solution
- Terraform: Multi-cloud infrastructure management tool
Option A: ARM Templates/Bicep (Recommended for Azure-only environments)
Create an ARM template (infrastructure/main.bicep) for your infrastructure:
param location string = resourceGroup().location
param environmentName string
param appServicePlanSku string = 'S1'
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: 'asp-myapp-${environmentName}'
location: location
sku: {
name: appServicePlanSku
tier: 'Standard'
}
properties: {
reserved: false
}
}
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
name: 'app-myapp-${environmentName}'
location: location
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
netFrameworkVersion: 'v6.0'
defaultDocuments: [
'Default.htm'
'Default.html'
'index.html'
]
httpLoggingEnabled: true
logsDirectorySizeLimit: 35
detailedErrorLoggingEnabled: true
appSettings: [
{
name: 'ASPNETCORE_ENVIRONMENT'
value: environmentName
}
{
name: 'ApplicationInsights__ConnectionString'
value: applicationInsights.properties.ConnectionString
}
]
}
}
}
// Create staging slot for production environment
resource stagingSlot 'Microsoft.Web/sites/slots@2022-03-01' = if (environmentName == 'prod') {
parent: webApp
name: 'staging'
location: location
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
netFrameworkVersion: 'v6.0'
appSettings: [
{
name: 'ASPNETCORE_ENVIRONMENT'
value: 'Staging'
}
{
name: 'ApplicationInsights__ConnectionString'
value: applicationInsights.properties.ConnectionString
}
]
}
}
}
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: 'ai-myapp-${environmentName}'
location: location
kind: 'web'
properties: {
Application_Type: 'web'
Request_Source: 'rest'
RetentionInDays: 90
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: 'log-myapp-${environmentName}'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
name: 'kv-myapp-${environmentName}-${uniqueString(resourceGroup().id)}'
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: webApp.identity.principalId
permissions: {
secrets: [
'get'
'list'
]
}
}
]
enableRbacAuthorization: false
enableSoftDelete: true
softDeleteRetentionInDays: 7
}
}
output webAppName string = webApp.name
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output keyVaultName string = keyVault.name
output applicationInsightsKey string = applicationInsights.properties.InstrumentationKey
Deploy Infrastructure
Create infrastructure deployment pipeline (infrastructure/azure-pipelines.yml):
trigger: none
variables:
azureSubscription: 'MyAzureSubscription'
resourceGroupPrefix: 'rg-myapp'
location: 'East US'
stages:
- stage: DeployInfrastructure
displayName: 'Deploy Infrastructure'
jobs:
- job: DeployDev
displayName: 'Deploy Development Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Development Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-dev'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "dev"
-appServicePlanSku "F1"
deploymentMode: 'Incremental'
- job: DeployStaging
displayName: 'Deploy Staging Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Staging Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-staging'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "staging"
-appServicePlanSku "S1"
deploymentMode: 'Incremental'
- job: DeployProduction
displayName: 'Deploy Production Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Production Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-prod'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "prod"
-appServicePlanSku "P1V2"
deploymentMode: 'Incremental'
Option B: Terraform (Recommended for multi-cloud or Terraform-experienced teams)
Alternatively, you can use Terraform to manage the same infrastructure. Here’s the equivalent Terraform configuration:
main.tf:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
# Resource Group
resource "azurerm_resource_group" "main" {
name = "rg-myapp-${var.environment_name}"
location = var.location
}
# App Service Plan
resource "azurerm_service_plan" "main" {
name = "asp-myapp-${var.environment_name}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Windows"
sku_name = var.app_service_plan_sku
}
# Main Web App (Production Slot)
resource "azurerm_windows_web_app" "main" {
name = "app-myapp-${var.environment_name}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_service_plan.main.location
service_plan_id = azurerm_service_plan.main.id
site_config {
always_on = true
application_stack {
dotnet_framework_version = "v6.0"
}
}
app_settings = {
"ASPNETCORE_ENVIRONMENT" = title(var.environment_name)
"ApplicationInsights__ConnectionString" = azurerm_application_insights.main.connection_string
}
identity {
type = "SystemAssigned"
}
https_only = true
}
# Staging Deployment Slot (only for production environment)
resource "azurerm_windows_web_app_slot" "staging" {
count = var.environment_name == "prod" ? 1 : 0
name = "staging"
app_service_id = azurerm_windows_web_app.main.id
site_config {
always_on = true
application_stack {
dotnet_framework_version = "v6.0"
}
}
app_settings = {
"ASPNETCORE_ENVIRONMENT" = "Staging"
"ApplicationInsights__ConnectionString" = azurerm_application_insights.main.connection_string
}
identity {
type = "SystemAssigned"
}
https_only = true
}
# Application Insights
resource "azurerm_application_insights" "main" {
name = "ai-myapp-${var.environment_name}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
application_type = "web"
retention_in_days = 90
workspace_id = azurerm_log_analytics_workspace.main.id
}
# Log Analytics Workspace
resource "azurerm_log_analytics_workspace" "main" {
name = "log-myapp-${var.environment_name}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
sku = "PerGB2018"
retention_in_days = 30
}
# Key Vault for secrets
resource "azurerm_key_vault" "main" {
name = "kv-myapp-${var.environment_name}-${random_string.suffix.result}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# Grant access to the web app's managed identity
access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_windows_web_app.main.identity[0].principal_id
secret_permissions = [
"Get",
"List",
]
}
# Grant access to staging slot if it exists
dynamic "access_policy" {
for_each = var.environment_name == "prod" ? [1] : []
content {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_windows_web_app_slot.staging[0].identity[0].principal_id
secret_permissions = [
"Get",
"List",
]
}
}
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
data "azurerm_client_config" "current" {}
variables.tf:
variable "environment_name" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment_name)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "location" {
description = "Azure region"
type = string
default = "East US"
}
variable "app_service_plan_sku" {
description = "App Service Plan SKU"
type = string
default = "S1"
}
terraform.tfvars (for different environments):
# terraform.tfvars.prod environment_name = "prod" location = "East US" app_service_plan_sku = "P1V2" # Production tier supports deployment slots # terraform.tfvars.staging environment_name = "staging" location = "East US" app_service_plan_sku = "S1" # No slots needed for staging environment # terraform.tfvars.dev environment_name = "dev" location = "East US" app_service_plan_sku = "F1" # Free tier, no slots available
Deploy with Terraform:
# Initialize Terraform terraform init # Plan deployment terraform plan -var-file="terraform.tfvars.prod" # Apply infrastructure terraform apply -var-file="terraform.tfvars.prod" -auto-approve
ARM vs Terraform: Which Should You Choose?
Choose ARM Templates/Bicep if:
- You’re working in a pure Azure environment
- Your team is Azure-focused
- You want native Azure tooling integration
- You need immediate access to new Azure features
Choose Terraform if:
- You have multi-cloud infrastructure
- Your team has Terraform expertise
- You want vendor-neutral infrastructure code
- You need to manage non-Azure resources (DNS, monitoring tools, etc.)
Deploy Infrastructure
If using ARM/Bicep, create infrastructure deployment pipeline (infrastructure/azure-pipelines.yml):
trigger: none
variables:
azureSubscription: 'MyAzureSubscription'
resourceGroupPrefix: 'rg-myapp'
location: 'East US'
stages:
- stage: DeployInfrastructure
displayName: 'Deploy Infrastructure'
jobs:
- job: DeployDev
displayName: 'Deploy Development Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Development Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-dev'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "dev"
-appServicePlanSku "F1"
deploymentMode: 'Incremental'
- job: DeployStaging
displayName: 'Deploy Staging Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Staging Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-staging'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "staging"
-appServicePlanSku "S1"
deploymentMode: 'Incremental'
- job: DeployProduction
displayName: 'Deploy Production Infrastructure'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Production Resources'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: '$(azureSubscription)'
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupPrefix)-prod'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: 'infrastructure/main.bicep'
overrideParameters: |
-environmentName "prod"
-appServicePlanSku "P1V2"
Application Settings
Create environment-specific configuration files:
appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "@Microsoft.KeyVault(SecretUri=https://kv-myapp-dev.vault.azure.net/secrets/DatabaseConnectionString/)"
},
"ApplicationInsights": {
"ConnectionString": ""
}
}
appsettings.Staging.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "@Microsoft.KeyVault(SecretUri=https://kv-myapp-staging.vault.azure.net/secrets/DatabaseConnectionString/)"
},
"ApplicationInsights": {
"ConnectionString": ""
}
}
appsettings.Production.json:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "@Microsoft.KeyVault(SecretUri=https://kv-myapp-prod.vault.azure.net/secrets/DatabaseConnectionString/)"
},
"ApplicationInsights": {
"ConnectionString": ""
}
}
Health Check Configuration
Add health checks to your application (Program.cs):
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
name: "database",
tags: new[] { "db", "sql", "sqlserver" });
var app = builder.Build();
// Configure pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
Step 3: Build Pipeline Configuration
Create the main build pipeline (azure-pipelines.yml):
trigger:
branches:
include:
- main
- develop
paths:
exclude:
- infrastructure/*
- docs/*
- README.md
variables:
buildConfiguration: 'Release'
dotNetFramework: 'net6.0'
dotNetVersion: '6.0.x'
buildPlatform: 'Any CPU'
pool:
vmImage: 'windows-latest'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Build Job'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK $(dotNetVersion)'
inputs:
packageType: 'sdk'
version: '$(dotNetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
feedsToUse: 'select'
- task: DotNetCoreCLI@2
displayName: 'Build application'
inputs:
command: 'build'
projects: '**/*.csproj'
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 $(Common.TestResultsDirectory)'
publishTestResults: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Common.TestResultsDirectory)/**/*.cobertura.xml'
- task: DotNetCoreCLI@2
displayName: 'Publish application'
inputs:
command: 'publish'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/app --no-build'
publishWebProjects: true
zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: 'Publish build artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
- stage: DeployDev
displayName: 'Deploy to Development'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
variables:
environment: 'dev'
resourceGroup: 'rg-myapp-dev'
webAppName: 'app-myapp-dev'
jobs:
- deployment: DeployDev
displayName: 'Deploy to Development'
environment: 'Development'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-steps.yml
parameters:
environment: '$(environment)'
resourceGroup: '$(resourceGroup)'
webAppName: '$(webAppName)'
useSlots: false
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
environment: 'staging'
resourceGroup: 'rg-myapp-staging'
webAppName: 'app-myapp-staging'
jobs:
- deployment: DeployStaging
displayName: 'Deploy to Staging'
environment: 'Staging'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-steps.yml
parameters:
environment: '$(environment)'
resourceGroup: '$(resourceGroup)'
webAppName: '$(webAppName)'
useSlots: false
- job: StagingTests
displayName: 'Run Staging Tests'
dependsOn: DeployStaging
pool:
vmImage: 'windows-latest'
steps:
- task: DotNetCoreCLI@2
displayName: 'Run integration tests'
inputs:
command: 'test'
projects: '**/*IntegrationTests.csproj'
arguments: '--configuration $(buildConfiguration) --logger trx --results-directory $(Common.TestResultsDirectory)'
publishTestResults: true
env:
TEST_BASE_URL: 'https://app-myapp-staging.azurewebsites.net'
- stage: SecurityScan
displayName: 'Security Scanning'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: SecurityScan
displayName: 'Security Scan'
pool:
vmImage: 'windows-latest'
steps:
- task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@20
displayName: 'WhiteSource Bolt'
inputs:
cwd: '$(System.DefaultWorkingDirectory)'
- task: SonarCloudPrepare@1
displayName: 'Prepare SonarCloud analysis'
inputs:
SonarCloud: 'SonarCloud'
organization: 'your-organization'
scannerMode: 'MSBuild'
projectKey: 'myapp'
projectName: 'MyApp'
projectVersion: '$(Build.BuildNumber)'
- task: DotNetCoreCLI@2
displayName: 'Build for SonarCloud'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: SonarCloudAnalyze@1
displayName: 'Run SonarCloud analysis'
- task: SonarCloudPublish@1
displayName: 'Publish SonarCloud results'
inputs:
pollingTimeoutSec: '300'
- stage: ProductionApproval
displayName: 'Production Approval'
dependsOn:
- DeployStaging
- SecurityScan
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: waitForValidation
displayName: 'Wait for external validation'
pool: server
timeoutInMinutes: 4320 # 3 days
steps:
- task: ManualValidation@0
displayName: 'Manual validation'
inputs:
notifyUsers: |
admin@company.com
devops@company.com
instructions: 'Please validate the staging deployment and approve for production'
onTimeout: 'reject'
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: ProductionApproval
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
environment: 'prod'
resourceGroup: 'rg-myapp-prod'
webAppName: 'app-myapp-prod'
jobs:
- deployment: DeployProduction
displayName: 'Deploy to Production'
environment: 'Production'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-steps.yml
parameters:
environment: '$(environment)'
resourceGroup: '$(resourceGroup)'
webAppName: '$(webAppName)'
useSlots: true
Step 4: Deployment Templates
Create reusable deployment templates (templates/deploy-steps.yml):
parameters:
- name: environment
type: string
- name: resourceGroup
type: string
- name: webAppName
type: string
- name: useSlots
type: boolean
default: false
steps:
- download: current
artifact: drop
displayName: 'Download build artifacts'
- task: AzureKeyVault@2
displayName: 'Get secrets from Key Vault'
inputs:
azureSubscription: 'MyAzureSubscription'
KeyVaultName: 'kv-myapp-${{ parameters.environment }}'
SecretsFilter: '*'
RunAsPreJob: false
- ${{ if eq(parameters.useSlots, true) }}:
- task: AzureRmWebAppDeployment@4
displayName: 'Deploy to staging slot'
inputs:
ConnectionType: 'AzureRM'
azureSubscription: 'MyAzureSubscription'
appType: 'webApp'
WebAppName: '${{ parameters.webAppName }}'
deployToSlotOrASE: true
ResourceGroupName: '${{ parameters.resourceGroup }}'
SlotName: 'staging'
packageForLinux: '$(Pipeline.Workspace)/drop/app/*.zip'
AppSettings: |
-ASPNETCORE_ENVIRONMENT "${{ parameters.environment }}"
-ApplicationInsights__ConnectionString "$(ApplicationInsights--ConnectionString)"
-ConnectionStrings__DefaultConnection "$(DatabaseConnectionString)"
- task: AzureAppServiceManage@0
displayName: 'Start staging slot'
inputs:
azureSubscription: 'MyAzureSubscription'
Action: 'Start Azure App Service'
WebAppName: '${{ parameters.webAppName }}'
SpecifySlotOrASE: true
ResourceGroupName: '${{ parameters.resourceGroup }}'
Slot: 'staging'
- task: PowerShell@2
displayName: 'Validate staging slot'
inputs:
targetType: 'inline'
script: |
$url = "https://${{ parameters.webAppName }}-staging.azurewebsites.net/health"
Write-Host "Testing health endpoint: $url"
$maxAttempts = 10
$attempt = 0
$success = $false
while ($attempt -lt $maxAttempts -and -not $success) {
try {
$response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 30
if ($response) {
Write-Host "Health check passed!"
$success = $true
} else {
Write-Host "Health check failed. Attempt $($attempt + 1) of $maxAttempts"
}
} catch {
Write-Host "Error calling health endpoint: $($_.Exception.Message)"
}
if (-not $success) {
Start-Sleep -Seconds 30
$attempt++
}
}
if (-not $success) {
Write-Error "Health check failed after $maxAttempts attempts"
exit 1
}
- task: AzureAppServiceManage@0
displayName: 'Swap staging to production'
inputs:
azureSubscription: 'MyAzureSubscription'
Action: 'Swap Slots'
WebAppName: '${{ parameters.webAppName }}'
ResourceGroupName: '${{ parameters.resourceGroup }}'
SourceSlot: 'staging'
SwapWithProduction: true
- ${{ if eq(parameters.useSlots, false) }}:
- task: AzureRmWebAppDeployment@4
displayName: 'Deploy to App Service'
inputs:
ConnectionType: 'AzureRM'
azureSubscription: 'MyAzureSubscription'
appType: 'webApp'
WebAppName: '${{ parameters.webAppName }}'
ResourceGroupName: '${{ parameters.resourceGroup }}'
packageForLinux: '$(Pipeline.Workspace)/drop/app/*.zip'
AppSettings: |
-ASPNETCORE_ENVIRONMENT "${{ parameters.environment }}"
-ApplicationInsights__ConnectionString "$(ApplicationInsights--ConnectionString)"
-ConnectionStrings__DefaultConnection "$(DatabaseConnectionString)"
- task: PowerShell@2
displayName: 'Post-deployment validation'
inputs:
targetType: 'inline'
script: |
$url = "https://${{ parameters.webAppName }}.azurewebsites.net/health"
Write-Host "Testing production health endpoint: $url"
$maxAttempts = 5
$attempt = 0
$success = $false
while ($attempt -lt $maxAttempts -and -not $success) {
try {
$response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 30
if ($response) {
Write-Host "Production health check passed!"
$success = $true
}
} catch {
Write-Host "Error calling production health endpoint: $($_.Exception.Message)"
}
if (-not $success) {
Start-Sleep -Seconds 15
$attempt++
}
}
if (-not $success) {
Write-Error "Production health check failed after $maxAttempts attempts"
exit 1
}
- task: AzureCLI@2
displayName: 'Configure monitoring alerts'
inputs:
azureSubscription: 'MyAzureSubscription'
scriptType: 'ps'
scriptLocation: 'inlineScript'
inlineScript: |
# Create action group for alerts
az monitor action-group create `
--name "myapp-alerts" `
--resource-group "${{ parameters.resourceGroup }}" `
--short-name "MyAppAlert" `
--email-receivers name="DevOps Team" email="devops@company.com"
# Create availability alert
az monitor metrics alert create `
--name "myapp-availability-alert" `
--resource-group "${{ parameters.resourceGroup }}" `
--scopes "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/${{ parameters.resourceGroup }}/providers/Microsoft.Web/sites/${{ parameters.webAppName }}" `
--condition "avg Availability < 99" `
--description "Alert when availability drops below 99%" `
--evaluation-frequency 1m `
--window-size 5m `
--severity 2 `
--action-groups "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/${{ parameters.resourceGroup }}/providers/microsoft.insights/actionGroups/myapp-alerts"
Step 5: Variable Groups and Environments
Create Variable Groups
In Azure DevOps, create variable groups for each environment:
Development Variables:
Environment: Development DatabaseConnectionString: (linked to Key Vault) ApplicationInsights.ConnectionString: (from deployment output)
Staging Variables:
Environment: Staging DatabaseConnectionString: (linked to Key Vault) ApplicationInsights.ConnectionString: (from deployment output)
Production Variables:
Environment: Production DatabaseConnectionString: (linked to Key Vault) ApplicationInsights.ConnectionString: (from deployment output)
Configure Environments
Create environments in Azure DevOps with appropriate approvals and checks:
- Development: Auto-approval
- Staging: Auto-approval with branch protection (main only)
- Production: Manual approval required with 2-person approval policy
Step 6: Advanced Production Features
Blue/Green Deployment with Traffic Splitting
Add traffic splitting configuration:
- task: AzureAppServiceManage@0
displayName: 'Configure traffic routing (10% to staging)'
inputs:
azureSubscription: 'MyAzureSubscription'
Action: 'Swap Slots'
WebAppName: '${{ parameters.webAppName }}'
ResourceGroupName: '${{ parameters.resourceGroup }}'
SourceSlot: 'staging'
SwapWithProduction: false
PreserveVnet: true
RouteTrafficPercentage: 10
- task: PowerShell@2
displayName: 'Monitor metrics during canary deployment'
inputs:
targetType: 'inline'
script: |
# Monitor for 10 minutes
$endTime = (Get-Date).AddMinutes(10)
while ((Get-Date) -lt $endTime) {
# Check error rate, response time, etc.
$errorRate = # Query Application Insights
if ($errorRate -gt 0.05) { # 5% error threshold
Write-Error "High error rate detected: $errorRate"
exit 1
}
Start-Sleep -Seconds 60
}
- task: AzureAppServiceManage@0
displayName: 'Complete swap to production'
inputs:
azureSubscription: 'MyAzureSubscription'
Action: 'Swap Slots'
WebAppName: '${{ parameters.webAppName }}'
ResourceGroupName: '${{ parameters.resourceGroup }}'
SourceSlot: 'staging'
SwapWithProduction: true
Automated Rollback
Implement automated rollback capabilities:
- task: PowerShell@2
displayName: 'Monitor post-deployment metrics'
inputs:
targetType: 'inline'
script: |
$monitoringDuration = 300 # 5 minutes
$checkInterval = 30 # 30 seconds
$endTime = (Get-Date).AddSeconds($monitoringDuration)
while ((Get-Date) -lt $endTime) {
try {
# Check health endpoint
$healthResponse = Invoke-RestMethod -Uri "https://${{ parameters.webAppName }}.azurewebsites.net/health" -TimeoutSec 10
# Check Application Insights metrics
$errorRate = # Query error rate from App Insights
$responseTime = # Query average response time
if ($errorRate -gt 0.05 -or $responseTime -gt 2000) {
Write-Error "Performance degradation detected. Initiating rollback..."
# Trigger rollback
az webapp deployment slot swap --name "${{ parameters.webAppName }}" --resource-group "${{ parameters.resourceGroup }}" --slot staging --target-slot production
exit 1
}
Write-Host "Metrics within acceptable range. Error rate: $errorRate, Response time: $responseTime ms"
} catch {
Write-Warning "Error checking metrics: $($_.Exception.Message)"
}
Start-Sleep -Seconds $checkInterval
}
Write-Host "Post-deployment monitoring completed successfully"
Database Migration Pipeline
Create a separate pipeline for database migrations:
# database-migration-pipeline.yml
trigger: none
parameters:
- name: environment
displayName: Environment
type: string
default: staging
values:
- staging
- production
variables:
environment: ${{ parameters.environment }}
stages:
- stage: DatabaseMigration
displayName: 'Database Migration - $(environment)'
jobs:
- job: Migration
displayName: 'Run Database Migration'
pool:
vmImage: 'windows-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '6.0.x'
- task: AzureKeyVault@2
inputs:
azureSubscription: 'MyAzureSubscription'
KeyVaultName: 'kv-myapp-$(environment)'
SecretsFilter: 'DatabaseConnectionString'
- task: DotNetCoreCLI@2
displayName: 'Run EF Migrations'
inputs:
command: 'custom'
custom: 'ef'
arguments: 'database update --connection "$(DatabaseConnectionString)" --project MyApp.Data --startup-project MyApp.Web'
env:
ConnectionStrings__DefaultConnection: '$(DatabaseConnectionString)'
- task: PowerShell@2
displayName: 'Verify Migration'
inputs:
targetType: 'inline'
script: |
# Run verification queries to ensure migration succeeded
# This could include checking table structure, data integrity, etc.
Step 7: Monitoring and Observability
Application Insights Integration
Configure detailed monitoring:
// In Program.cs
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});
builder.Services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
// Custom telemetry initializer
public class CustomTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
if (telemetry is RequestTelemetry requestTelemetry)
{
requestTelemetry.Properties["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
requestTelemetry.Properties["Version"] = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
}
}
}
Dashboard Creation
Create Azure Dashboard for monitoring:
{
"properties": {
"lenses": [
{
"order": 0,
"parts": [
{
"position": { "x": 0, "y": 0, "rowSpan": 4, "colSpan": 6 },
"metadata": {
"inputs": [
{
"name": "ComponentId",
"value": "/subscriptions/{subscription-id}/resourceGroups/rg-myapp-prod/providers/microsoft.insights/components/ai-myapp-prod"
}
],
"type": "Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart"
}
},
{
"position": { "x": 6, "y": 0, "rowSpan": 4, "colSpan": 6 },
"metadata": {
"inputs": [
{
"name": "ComponentId",
"value": "/subscriptions/{subscription-id}/resourceGroups/rg-myapp-prod/providers/microsoft.insights/components/ai-myapp-prod"
}
],
"type": "Extension/AppInsightsExtension/PartType/PerformanceNavButtonPart"
}
}
]
}
]
}
}
Step 8: Security and Compliance
Secure Configuration Management
- task: AzureKeyVault@2
displayName: 'Get secrets from Key Vault'
inputs:
azureSubscription: 'MyAzureSubscription'
KeyVaultName: 'kv-myapp-$(environment)'
SecretsFilter: |
DatabaseConnectionString
ApiKey
JwtSecret
RunAsPreJob: true
- task: FileTransform@1
displayName: 'Transform configuration files'
inputs:
folderPath: '$(Pipeline.Workspace)/drop/app'
fileType: 'json'
targetFiles: '**/appsettings.json'
Compliance Scanning
Add compliance checks to your pipeline:
- task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-credscan.CredScan@2
displayName: 'Run Credential Scanner'
inputs:
toolMajorVersion: 'V2'
scanFolder: '$(Build.SourcesDirectory)'
debugMode: false
- task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-binskim.BinSkim@3
displayName: 'Run BinSkim'
inputs:
InputType: 'Basic'
Function: 'analyze'
AnalyzeTarget: '$(Build.ArtifactStagingDirectory)/**/*.dll;$(Build.ArtifactStagingDirectory)/**/*.exe'
- task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-postanalysis.PostAnalysis@1
displayName: 'Post Analysis'
inputs:
AllTools: false
BinSkim: true
CredScan: true
ToolLogsNotFoundAction: 'Standard'
Step 9: Performance Testing
Add performance testing stage:
- stage: PerformanceTesting
displayName: 'Performance Testing'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: LoadTest
displayName: 'Run Load Tests'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureLoadTest@1
displayName: 'Azure Load Testing'
inputs:
azureSubscription: 'MyAzureSubscription'
loadTestConfigFile: 'loadtest/config.yaml'
loadTestResource: 'loadtest-myapp'
resourceGroup: 'rg-myapp-shared'
env: |
[
{
"name": "webapp-url",
"value": "https://app-myapp-staging.azurewebsites.net"
}
]
- task: PublishTestResults@2
displayName: 'Publish load test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(System.DefaultWorkingDirectory)/**/*loadtest-results.xml'
failTaskOnFailedTests: true
Step 10: Disaster Recovery and Backup
Automated Backup Configuration
- task: AzureCLI@2
displayName: 'Configure backup policy'
inputs:
azureSubscription: 'MyAzureSubscription'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Create storage account for backups
az storage account create \
--name "stmyappbackup$(environment)" \
--resource-group "$(resourceGroup)" \
--location "East US" \
--sku "Standard_LRS"
# Configure app service backup
az webapp config backup update \
--resource-group "$(resourceGroup)" \
--webapp-name "$(webAppName)" \
--container-url "$(az storage account show-connection-string --name stmyappbackup$(environment) --resource-group $(resourceGroup) --query connectionString -o tsv)" \
--frequency 24 \
--retain-one true \
--retention-period-in-days 30
Conclusion
This comprehensive Azure DevOps pipeline provides enterprise-grade capabilities including:
- Infrastructure as Code with Bicep templates
- Multi-environment deployments with appropriate gates
- Zero-downtime deployments using slot swaps
- Automated testing at multiple stages
- Security scanning and compliance checks
- Performance testing integration
- Monitoring and alerting setup
- Automated rollback capabilities
- Disaster recovery configurations
The pipeline ensures high availability, security, and maintainability while providing the flexibility to adapt to changing requirements. Regular monitoring and continuous improvement of the pipeline based on operational feedback will help maintain its effectiveness in production environments.
Key benefits of this approach include reduced deployment risk, faster time-to-market, improved application quality, and enhanced operational visibility across the entire deployment lifecycle.
