Not so long ago a client asked us to spec up their CI/CD pipeline. They are going through devops transformation and as part of their “speed up delivery” objective they wanted to minimize downtime when they deploy new versions of their software or run maintenance.
🟦🟩-deployments
First thing we wanted to try was to introduce blue-green approach. The application runs on IIS and luckily for us, Microsoft offers a solution there: ARR. There’s heaps documentation online, and most examples seem to point at scaling out by routing traffic to application round robin. In our case the application was not ready for that just yet so we decided to use it for directing all traffic to one backend server only while we deploy the inactive one:
Farming Web Farms
ARR introduces a concept of Web Farms. This basically is a logical grouping of content servers that ARR treats as one site. Each farm comes with settings on how caching should work, or what actual content servers are like. It’s pretty easy to set up when we’ve got one or two of there. But in our case we were looking at approximately 100 farms. Yikes! Overall the process is pretty simple: create farm, add content servers, create URL rewrite rule. Nothing fancy and documentation is plentiful. What we wanted to do however was to automate everything into one script that could later run remotely when triggered by CI/CD pipelines.
PowerShell to the rescue
Our requirements were pretty standard until we realized that there’s no easy way to insert URL rewrite rules into arbitrary positions in the list. So we implemented a set of dummy rules that the script uses as anchors to locate a place where to inject new rule. We also needed node health check to cut off inactive servers, the easiest was a plain text file in website root with words “UP” or “DOWN” so that we can swap ARR slots by simply updating a file. ARR supports a few ways to programmatically manage, but since we’re on Windows we picked PowerShell as our tool of choice and ended up with something like this:
function CheckIfExists($xpath, $name, $remove = $true) {
$existing = Get-WebConfigurationProperty -pspath $psPath "$xpath[@name='$name']" -Name .
if($null -ne $existing) {
if($remove) {
Clear-WebConfiguration -pspath $psPath -Filter "$xpath[@name='$name']"
}
return $true
}
function IndexOfNode($collection, $name) {
$i=0
for ($i=0; $i -lt $existing.Collection.Count; $i++)
{
if ($collection[$i].name -eq $name)
{
return $i
}
}
return $i-1 #found nothing - return position at the end of collection
}
function CreateRule($name, $matchUrl = "*", $atAnchor = "") {
$matchingPatternSyntax = if ($matchUrl -eq "*") {"Wildcard"} else { "ECMAScript" };
$existing = Get-WebConfiguration -pspath $psPath "system.webServer/rewrite/globalRules"
$index = IndexOfNode $existing.Collection $atAnchor
Add-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules" -AtIndex $index -name "." -value @{name=$name;patternSyntax=$matchingPatternSyntax;stopProcessing='True';enabled='True'}
Set-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules/rule[@name='$name']/match" -name "url" -value $matchUrl
}
function CreateRuleCondition($name, $in = "{HTTP_HOST}", $pattern, $negate = $false)
{
$value = @{
input=$in;
pattern=$pattern;
}
if($negate -eq $true) {
$value.Add("negate", "True")
}
Add-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules/rule[@name='$name']/conditions" -name "." -value $value
}
function CreateRewriteAction($name, $url) {
Set-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules/rule[@name='$name']/action" -name "type" -value "Rewrite"
Set-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules/rule[@name='$name']/action" -name "url" -value "$url/{R:0}"
}
function CreateRedirectRule(
$ruleName,
$matchUrl,
$conditionHost,
$farmName,
$recreate = $true,
$atName
)
{
if(CheckIfExists "system.webServer/rewrite/globalRules/rule" $ruleName $recreate) {
if($recreate) {
Write-Host "Removed existing $ruleName before proceeding"
} else {
Write-Host "Skipped existing $ruleName"
return
}
}
# Create a new rule
CreateRule "$ruleName" -matchUrl $matchUrl -atAnchor $atName
Set-WebConfigurationProperty -pspath $psPath -filter "system.webServer/rewrite/globalRules/rule[@name='$farmName']/conditions" -name "logicalGrouping" -value "MatchAny"
$conditionHost | ForEach-Object {
CreateRuleCondition $ruleName -pattern $_
}
CreateRewriteAction $farmName "https://$farmName"
}
function CreateWebFarm(
$farmName,
$healthCheckUrl,
$blueIpAddress,
$greenIpAddress,
$parentSiteHostName,
$Recreate = $true
)
{
return; #debugging
if(CheckIfExists "webFarms/webFarm" $farmName $Recreate) {
if($Recreate) {
Write-Host "Removed existing $farmName before proceeding"
} else {
Write-Host "Skipped existing $farmName"
return
}
}
Add-WebConfigurationProperty -pspath $psPath -filter "webFarms" -name "." -value @{name=$farmName}
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/affinity" -name "useCookie" -value "True"
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/protocol/cache" -name "enabled" -value "False"
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/healthCheck" -name "url" -value $healthCheckUrl
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/healthCheck" -name "interval" -value "00:00:10"
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/healthCheck" -name "timeout" -value "00:00:5"
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/healthCheck" -name "responseMatch" -value "UP"
Add-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']" -name "." -value @{address=$blueIpAddress}
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/server[@address='$blueIpAddress']/applicationRequestRouting" -name "hostName" -value $parentSiteHostName
Add-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']" -name "." -value @{address=$greenIpAddress}
Set-WebConfigurationProperty -pspath $psPath -filter "webFarms/webFarm[@name='$farmName']/server[@address='$greenIpAddress']/applicationRequestRouting" -name "hostName" -value $parentSiteHostName
}
$host
= "newservice.example.com"
$farm = "newservice-farm"
CreateWebFarm $farm "https://$farm/healthcheck.htm" $greenIp $blueIp $host
CreateRedirectRule -ruleName $farm -matchUrl "*" -conditionHost @($host) -farmName $farm -atName "--Inserting rules above this point--"