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.