Introduction
In my previous post, I wrote about how to deploy a web app from Visual Studio to IIS using Web Deploy tool. One problem you might have noticed, is the downtime that occurs during deployment.
So in this post, I'll write about how to use Blue-Green Deployment in IIS to achieve zero downtime deployment.
We will need at least IIS 7, in order to install Application Request Routing (ARR).
Table of Contents
Basic Idea
We will create two server, blue and green. One of them will be accessible through public network for our clients, and the other only through the private network for testing purposes.
Deployment is done in these steps:
- We will deploy our new web app to the private network server.
- Make sure it is working as intended.
- Make the private network server accessible to public network, now both servers can receive clients requests.
- Gracefully shutdown the original public network server; now it can't receive new requests but will finish its current requests.
- Make the original public network server accessible only to private network, then turn it on.
- Now the green and blue servers are switched, and in each deployment they will get switched again.
Steps
1. Download and Install Web Platform Installer (WebPI)
It's recommended to use WebPI instead of directly downloading tools; because WebPI also gets the dependencies and configures these tools depending on the current system settings.
2. Get Application Request Routing (ARR)
- Open WebPI directly (start menu for example), not through IIS Manager.
- Search for ARR, then install.
3. Create the Blue and Green Servers
Make three websites, one on port 80 that will receive client requests, we will call it server-farm. The other two are bound to other ports, say 8001 and 8002, and in this example they are called blue and green respectively.
The two websites contain one file content.html
which either has blue
or green
written in it.
We will also add those websites to the hosts file and make them point to localhost.
In Windows, the hosts file is in: C:\Windows\System32\drivers\etc\hosts
Let's test it.
4. Configure the Server Farm
Create new server farm, and add two servers to it with the port numbers we just assigned to the blue and green websites.
You will get a prompt suggesting to add rewrite rules, select no; we will add our own rewrite rules later.
5. Configure Health Test
The health test will decide which server receives the clients requests.
First create an html file is-up.html
in each website, beside the content.html
file. One of them will contain 1
, and the other 0
.
Let's test it.
Go to our server farm, and configure the health test:
- URL will point to the
is-up.html
file. - Interval will check for response every second.
- Response Match is what we consider healthy—i.e., which server receives the requests.
Verify.
Check Monitor and Management in the server farm.
6. Logging Problem
The health test makes a request to each server every second, and these requests are logged, which is not desirable.
To solve this problem, we will need to add an entry excluding the is-up.html
file from the logs in the applicationHost.config
file of the IIS. As far as I know, this is not possible through the IIS Manager.
In Windows, the applicationHost.config
file is located in: C:\Windows\System32\inetsrv\config\applicationHost.config
Add this code to the end of the applicationHost.config
file.
<location path="blue/is-up.html">
<system.webServer>
<httpLogging dontLog="true" />
</system.webServer>
</location>
<location path="green/is-up.html">
<system.webServer>
<httpLogging dontLog="true" />
</system.webServer>
</location>
This is how the end of the applicationHost.config
file should look like.
7. Add URL Rewrite Rules
URL Rewrite is used to change the request URL according to the rules you specify.
We will use this module to route requests to our server farm.
If you installed ARR using WebPI, URL Rewrite also gets installed.
From the main node in the IIS Manager, select URL Rewrite, then add a Blank rule.
The pattern will match any URL: .*
Add a condition for the port to be 80
The Action type will be: Route to Server Farm
At the end, things will look like this:
If we switch the 0
and 1
in the is-up.html
files
Now thigs are set up. Next, we will write some scripts to automate the deployment process.
Scripts
We can use PowerShell to automate some processes.
Get Server Farm Object
function get-webfarm {
param($webFarmName)
$assembly = [System.Reflection.Assembly]::LoadFrom("$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll")
$mgr = new-object Microsoft.Web.Administration.ServerManager "$env:systemroot\system32\inetsrv\config\applicationhost.config"
$conf = $mgr.GetApplicationHostConfiguration()
$section = $conf.GetSection("webFarms")
$webFarms = $section.GetCollection()
$webFarms | Where {
$_.GetAttributeValue("name") -eq $webFarmName
}
}
To check which server is unhealthy
$webFarmName = "server-farm"
$webFarm = get-webfarm $webFarmName
$unhealthyServer = $webFarm.GetCollection() | where {
!$_.GetChildElement("applicationRequestRouting").GetChildElement("counters").GetAttributeValue("isHealthy")
}
$unhealthyServer.GetAttributeValue("address")
Switch Blue and Green
$webFarmName = "server-farm"
$addressBlue = "blue"
$addressGreen = "green"
$siteBlue = "http://blue:8001/"
$siteGreen = "http://green:8002/"
$pathBlue = "C:\inetpub\sites\blue"
$pathGreen = "C:\inetpub\sites\green"
$warmUpRequestURL = "content.html"
$healthCheckFileName = "is-up.html"
$healthyResponse = "1"
$unhealthyResponse = "0"
$webFarm = get-webfarm $webFarmName
# Get health check interval
$healthCheckTimeoutS = $webFarm.GetChildElement("applicationRequestRouting").GetChildElement("healthCheck").GetAttributeValue("interval").TotalSeconds
$healthyServer = $webFarm.GetCollection() | where {
$_.GetChildElement("applicationRequestRouting").GetChildElement("counters").GetAttributeValue("isHealthy")
}
$siteToWarm = $siteBlue
$serverAddressToBringDown = $addressGreen
$pathToBringDown = $pathGreen
$pathToBringUp = $pathBlue
if($healthyServer.GetAttributeValue("address") -eq $addressBlue) {
$siteToWarm = $siteGreen
$serverAddressToBringDown = $addressBlue
$pathToBringUp = $pathGreen
$pathToBringDown = $pathBlue
}
# Initializing unhealthy server, and making sure it works as intended
Write-Host "Warming up $($siteToWarm)"
Do {
$time = Measure-Command {
$res = Invoke-WebRequest "$($siteToWarm)$($warmUpRequestURL)"
}
$ms = $time.TotalMilliSeconds
If ($ms -ge 400) {
Write-Host "$($res.StatusCode) from $($siteToWarm) in $($ms)ms"
}
} While ($ms -ge 400)
Write-Host "$($res.StatusCode) from $($siteToWarm) in $($ms)ms"
# If the unhealthy server is fine, start the switching operation
if ($res.StatusCode -eq 200) {
Write-Host "Bringing $($pathToBringUp) up"
(Get-Content $pathToBringUp\$healthCheckFileName).replace($unhealthyResponse, $healthyResponse) | Set-Content $pathToBringUp\$healthCheckFileName
# Wait for health check to mark the server healthy, afterwards we can bring down the other server
Write-Host "Waiting for health check to pass in $($healthCheckTimeoutS) seconds..."
Start-Sleep -s $healthCheckTimeoutS
Write-Host "Draining $($pathToBringDown)"
$serverToBringDown = $webFarm.GetCollection() | Where {
$_.GetAttributeValue("address") -eq $serverAddressToBringDown
}
$arrToBringDown = $serverToBringDown.GetChildElement("applicationRequestRouting")
$method = $arrToBringDown.Methods["SetState"]
$methodInstance = $method.CreateInstance()
# 0 = Available
# 1 = Drain
# 2 = Unavailable
# 3 = Unavailable Gracefully
$methodInstance.Input.Attributes[0].Value = 1
$methodInstance.Execute()
# loop till there is no requests, then bring the server down
$currentRequests = $arrToBringDown.GetChildElement("counters").GetAttributeValue("currentRequests")
While($currentRequests -gt 0) {
Start-Sleep -s 1
$currentRequests = $arrToBringDown.GetChildElement("counters").GetAttributeValue("currentRequests")
}
Write-Host "Bringing $($pathToBringDown) down"
(Get-Content $pathToBringDown\$healthCheckFileName).replace($healthyResponse, $unhealthyResponse) | Set-Content $pathToBringDown\$healthCheckFileName
$methodInstance.Input.Attributes[0].Value = 0
$methodInstance.Execute()
} else {
Write-Host "Cannot warm up $($siteToWarm)"
}
Remote Access
To run these scripts remotely, we will use Windows Remote Management (WinRM).
To configure WinRM:
- Make sure port
5985
is open through the network. - Run
winrm qc
at remote server, selecty
when prompted. - Make sure WinRM service is running locally.
- Run
winrm set winrm/config/client '@{TrustedHosts="your.server.IP"}'
locally.
Now you can run scripts remotely using Invoke-Command
Invoke-Command -ComputerName your.server.ip -ScriptBlock {
$webFarmName = "server-farm"
$assembly = [System.Reflection.Assembly]::LoadFrom("$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll")
$mgr = new-object Microsoft.Web.Administration.ServerManager "$env:systemroot\system32\inetsrv\config\applicationhost.config"
$conf = $mgr.GetApplicationHostConfiguration()
$section = $conf.GetSection("webFarms")
$webFarms = $section.GetCollection()
$webFarm = $webFarms | Where {
$_.GetAttributeValue("name") -eq $webFarmName
}
$unhealthyServer = $webFarm.GetCollection() | where {
!$_.GetChildElement("applicationRequestRouting").GetChildElement("counters").GetAttributeValue("isHealthy")
}
$unhealthyServer.GetAttributeValue("address")
} -Credential (New-Object System.Management.Automation.PSCredential ("user", (ConvertTo-SecureString 'password' -AsPlainText -Force)))
This is simple authentication, for more options check this link.
Integration with Visual Studio
This builds on my previous post.
We will use Targets in MSBuild to control which server we will deploy to, and to switch Blue and Green servers after deployment.
Add the PowerShell scripts in the project directory:
\Properties\Scripts\
In the publish profile, add these lines of code in the project tag:
<Target Name="DetermineUnhealthy" BeforeTargets="BeforePublish">
<Exec Command="powershell -executionpolicy unrestricted "& ""$(MSBuildProjectDirectory)\Properties\Scripts\GetUnhealthy.ps1""""
ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DeployIisAppPath"/>
</Exec>
</Target>
This code determines the unhealthy server before the start of the deployment and assigns the value to DeployIisAppPath
property.
In case of an application under the site in IIS:
<Target Name="DetermineUnhealthy" BeforeTargets="BeforePublish">
<Exec Command="powershell -executionpolicy unrestricted "& ""$(MSBuildProjectDirectory)\Properties\Scripts\GetUnhealthy.ps1""""
ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="UnhealthySite"/>
</Exec>
<CreateProperty Value="$(UnhealthySite)/AppName">
<Output TaskParameter="Value" PropertyName="DeployIisAppPath"/>
</CreateProperty>
</Target>
If you want to switch directly after deployment, add this code:
<Target Name="SwitchBlueGreen" AfterTargets="AfterPublish">
<Exec Command="powershell -executionpolicy unrestricted "& ""$(MSBuildProjectDirectory)\Properties\Scripts\SwitchBlueGreen.ps1"""" />
</Target>
In my case, I have two profiles: one with switching, and another without.
Now you can deploy to IIS with zero downtime.
See Also
How to Deploy Anything in IIS with Zero Downtime on a Single Server
Taught me most of the stuff I wrote here.
Top comments (0)