Introduction to Blue-Green Deployment on Windows Server 2022
Blue-green deployment is a release technique that maintains two identical production environments — one active (blue), one idle (green). A new version is deployed to the idle environment, validated, and then traffic is switched instantly from blue to green. If anything goes wrong after the switch, traffic is switched back to blue in seconds. This eliminates deployment downtime and provides an immediate rollback path. On Windows Server 2022, blue-green can be implemented using IIS with multiple sites and app pools, Network Load Balancing (NLB), Application Request Routing (ARR), or Windows containers.
Blue-Green Architecture with IIS Sites and App Pools
The simplest IIS blue-green setup runs both versions on the same server using separate application pools and sites on different ports, with a front-end switch at the binding level. Create the two environments:
# Create app pools
New-WebAppPool -Name "MyApp-Blue"
New-WebAppPool -Name "MyApp-Green"
# Set .NET version and managed pipeline (adjust for your app)
Set-ItemProperty "IIS:AppPoolsMyApp-Blue" -Name managedRuntimeVersion -Value "v4.0"
Set-ItemProperty "IIS:AppPoolsMyApp-Green" -Name managedRuntimeVersion -Value "v4.0"
# Create sites on different internal ports
New-Website -Name "MyApp-Blue" -PhysicalPath "C:sitesmyapp-blue" `
-ApplicationPool "MyApp-Blue" -Port 8081 -Force
New-Website -Name "MyApp-Green" -PhysicalPath "C:sitesmyapp-green" `
-ApplicationPool "MyApp-Green" -Port 8082 -Force
# Create the public-facing site (initially pointing to blue)
New-Website -Name "MyApp-Public" -PhysicalPath "C:sitesmyapp-blue" `
-ApplicationPool "MyApp-Blue" -Port 80 -Force
The public site’s binding points to whichever color is currently live. After deploying to the idle environment and validating it, perform the cutover.
Switching Traffic with Set-WebBinding
The switch script updates the public site to point to the new deployment and recycles the app pool. Create a Switch-BlueGreen.ps1 script:
param(
[Parameter(Mandatory)]
[ValidateSet("Blue","Green")]
[string]$Target
)
$siteName = "MyApp-Public"
$bluePath = "C:sitesmyapp-blue"
$greenPath = "C:sitesmyapp-green"
$bluePool = "MyApp-Blue"
$greenPool = "MyApp-Green"
if ($Target -eq "Blue") {
$newPath = $bluePath
$newPool = $bluePool
} else {
$newPath = $greenPath
$newPool = $greenPool
}
# Update physical path and app pool atomically
Set-ItemProperty "IIS:Sites$siteName" -Name physicalPath -Value $newPath
Set-ItemProperty "IIS:Sites$siteName" -Name applicationPool -Value $newPool
# Recycle to pick up new pool
Restart-WebAppPool -Name $newPool
Write-Host "Traffic switched to $Target ($newPath)" -ForegroundColor Green
Usage:
.Switch-BlueGreen.ps1 -Target Green # deploy complete, switch to green
.Switch-BlueGreen.ps1 -Target Blue # rollback to blue
Blue-Green with Application Request Routing (ARR)
ARR is Microsoft’s IIS extension for reverse proxy and load balancing. In a blue-green setup, ARR sits in front and routes to the active upstream. Install ARR and the URL Rewrite module (available from https://www.iis.net/downloads/microsoft/application-request-routing).
Enable the proxy in ARR:
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "system.webServer/proxy" -Name "enabled" -Value $true
Configure a server farm for blue:
$farmName = "BlueGreenFarm"
# Create server farm
Add-WebConfiguration -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms" -Value @{ name = $farmName }
# Add the active server (blue initially)
Add-WebConfiguration -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='$farmName']" `
-Value @{ address = "localhost"; httpPort = 8081 }
To switch to green, update the server farm’s address:
function Switch-ARRFarm {
param([string]$FarmName, [int]$Port)
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='$FarmName']/server[1]" `
-Name "httpPort" -Value $Port
}
# Switch to green (port 8082)
Switch-ARRFarm -FarmName "BlueGreenFarm" -Port 8082
# Switch back to blue
Switch-ARRFarm -FarmName "BlueGreenFarm" -Port 8081
Blue-Green with Windows Network Load Balancing (NLB)
For a multi-server setup, Windows NLB distributes traffic across a cluster of nodes. In blue-green, you maintain a blue cluster and a green cluster. Deploy to the green cluster, drain the blue cluster, then re-route traffic to green by manipulating the NLB virtual IP or DNS.
Install and configure NLB:
Install-WindowsFeature NLB -IncludeManagementTools
# Create NLB cluster on the blue nodes
New-NlbCluster -HostName "web01-blue" -ClusterName "MyApp-Cluster" `
-ClusterPrimaryIP "10.0.1.100" -SubnetMask "255.255.255.0" `
-InterfaceName "Ethernet"
# Add second node
Add-NlbClusterNode -HostName "web02-blue" -NewNodeInterface "Ethernet" `
-NewNodeName "web02-blue"
During a blue-to-green switch, drain connections from blue nodes before bringing green online:
# Step 1: Drain connections on blue nodes (stop accepting new, finish existing)
Get-NlbClusterNode -HostName "web01-blue" | Stop-NlbClusterNode -Drain
Get-NlbClusterNode -HostName "web02-blue" | Stop-NlbClusterNode -Drain
# Step 2: Wait for drain to complete (check connection count goes to 0)
Start-Sleep -Seconds 30
# Step 3: Route VIP to green nodes (via DNS switch or enable green nodes in a second cluster)
# If using a DNS-based switch, update the A record
$zone = "yourdomain.local"
Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject (Get-DnsServerResourceRecord -ZoneName $zone -Name "myapp" -RRType A) `
-NewInputObject (New-DnsServerResourceRecordA -Name "myapp" -ZoneName $zone -IPv4Address "10.0.2.100")
Enabling and Draining NLB Nodes with PowerShell
Full automation of the NLB-based switch:
function Switch-NLBBlueGreen {
param(
[string[]]$BlueNodes,
[string[]]$GreenNodes,
[int]$DrainTimeoutSeconds = 60
)
Write-Host "Draining blue nodes..." -ForegroundColor Yellow
foreach ($node in $BlueNodes) {
Stop-NlbClusterNode -HostName $node -Drain
}
$elapsed = 0
while ($elapsed -lt $DrainTimeoutSeconds) {
Start-Sleep -Seconds 5
$elapsed += 5
Write-Host " Waiting for drain... ${elapsed}s"
}
Write-Host "Suspending blue nodes..." -ForegroundColor Yellow
foreach ($node in $BlueNodes) {
Stop-NlbClusterNode -HostName $node
}
Write-Host "Enabling green nodes..." -ForegroundColor Green
foreach ($node in $GreenNodes) {
Start-NlbClusterNode -HostName $node
}
Write-Host "Switch complete. Green nodes active." -ForegroundColor Green
}
Switch-NLBBlueGreen -BlueNodes @("web01","web02") -GreenNodes @("web03","web04") -DrainTimeoutSeconds 90
Database Migration Strategy with Blue-Green
Database changes are the most challenging aspect of blue-green deployment. Both the blue and green versions of the application must be able to connect to the same database simultaneously during the transition. The recommended pattern is expand-and-contract (also called parallel change):
Phase 1 — Expand (backward-compatible migration): Add new columns or tables with nullable/defaulted values. Both old (blue) and new (green) code can run against the schema.
-- Example: add new column with default (both versions work)
ALTER TABLE Orders ADD ShippingZone NVARCHAR(10) NULL;
Phase 2 — Switch: Route traffic from blue to green. Both versions are momentarily active. The schema is compatible with both.
Phase 3 — Contract: After green is confirmed stable, remove the old column or table that the new version no longer uses:
-- After confirming green is fully live and stable
ALTER TABLE Orders DROP COLUMN OldShippingField;
Run database migrations from the build pipeline before deploying to green, using a migration tool like FluentMigrator or EF Core Migrations:
dotnet ef database update --connection "Server=prod-sql;Database=MyApp;Integrated Security=True;"
Health Check Before Traffic Switch
Never switch traffic before verifying the green environment is healthy. Create a health check script that tests key endpoints and returns a pass/fail:
function Test-GreenHealth {
param([string]$BaseUrl)
$endpoints = @(
"/health",
"/api/status",
"/api/version"
)
$allPassed = $true
foreach ($endpoint in $endpoints) {
try {
$response = Invoke-WebRequest -Uri "$BaseUrl$endpoint" -TimeoutSec 10 -UseBasicParsing
if ($response.StatusCode -ne 200) {
Write-Warning "$endpoint returned HTTP $($response.StatusCode)"
$allPassed = $false
} else {
Write-Host " PASS: $endpoint (HTTP $($response.StatusCode))" -ForegroundColor Green
}
} catch {
Write-Warning " FAIL: $endpoint - $_"
$allPassed = $false
}
}
return $allPassed
}
# Test green on its internal port before switching
if (-not (Test-GreenHealth -BaseUrl "http://localhost:8082")) {
Write-Error "Green health checks failed. Aborting switch."
exit 1
}
Write-Host "Green is healthy. Proceeding with switch." -ForegroundColor Green
.Switch-BlueGreen.ps1 -Target Green
Smoke Tests After the Switch
After switching traffic to green, run smoke tests against the public URL to confirm end-to-end functionality:
function Invoke-SmokeTests {
param([string]$PublicUrl)
$tests = @(
@{ Url = "$PublicUrl/"; ExpectedStatus = 200 },
@{ Url = "$PublicUrl/api/health"; ExpectedStatus = 200 },
@{ Url = "$PublicUrl/api/products"; ExpectedStatus = 200 },
@{ Url = "$PublicUrl/nonexistent-page"; ExpectedStatus = 404 }
)
$failed = 0
foreach ($test in $tests) {
$response = Invoke-WebRequest -Uri $test.Url -UseBasicParsing -SkipHttpErrorCheck
if ($response.StatusCode -eq $test.ExpectedStatus) {
Write-Host " PASS: $($test.Url)" -ForegroundColor Green
} else {
Write-Host " FAIL: $($test.Url) - expected $($test.ExpectedStatus), got $($response.StatusCode)" -ForegroundColor Red
$failed++
}
}
if ($failed -gt 0) {
Write-Warning "$failed smoke tests failed. Consider rolling back."
return $false
}
return $true
}
$smokeResult = Invoke-SmokeTests -PublicUrl "https://myapp.yourdomain.com"
if (-not $smokeResult) {
Write-Warning "Rolling back to blue..."
.Switch-BlueGreen.ps1 -Target Blue
}
Rollback Procedure
The rollback to blue is a single command that reverts the binding switch. The blue environment was never stopped — it was merely idle. Because the database migrations used backward-compatible changes (expand-and-contract), the blue code still works with the current schema. The rollback takes effect within seconds with zero data loss.
# Immediate rollback - switch public traffic back to blue
.Switch-BlueGreen.ps1 -Target Blue
# Verify blue is responding
Test-GreenHealth -BaseUrl "http://localhost:8081"
# Log the rollback event
$logEntry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] ROLLBACK: Switched from green to blue. Reason: smoke test failure."
Add-Content -Path "C:logsdeployment-history.txt" -Value $logEntry
Blue-Green with Windows Containers
Windows Server 2022 supports Windows containers. In a containerized blue-green setup, build a new image (green), run it on a different port, validate it, then update the IIS ARR or reverse proxy upstream to the new container port. Using Docker Compose on a single host:
# Deploy new green container
docker pull myregistry.local/myapp:2.4.1
docker run -d --name myapp-green -p 8082:80 myregistry.local/myapp:2.4.1
# Health check green container
$healthy = $false
for ($i = 0; $i -lt 12; $i++) {
try {
$r = Invoke-WebRequest -Uri "http://localhost:8082/health" -UseBasicParsing
if ($r.StatusCode -eq 200) { $healthy = $true; break }
} catch {}
Start-Sleep -Seconds 5
}
if ($healthy) {
# Switch ARR to green port
Switch-ARRFarm -FarmName "BlueGreenFarm" -Port 8082
# Stop old blue container after drain period
Start-Sleep -Seconds 30
docker stop myapp-blue
docker rm myapp-blue
docker rename myapp-green myapp-blue
} else {
Write-Error "Green container failed health checks"
docker stop myapp-green
docker rm myapp-green
}
Summary
Blue-green deployment on Windows Server 2022 eliminates deployment downtime by keeping two parallel environments and switching traffic atomically. Using IIS site bindings and app pools for single-server deployments, ARR for proxy-level switching, or NLB for multi-server clusters, the switch is reduced to a PowerShell one-liner. The pattern’s power comes from the combination of pre-switch health checks, post-switch smoke tests, and the always-available rollback to the blue environment — a safety net that makes even complex releases predictable and low-risk.