DEV Community

Cover image for Deploy Web App to IIS with Zero Downtime in Visual Studio
Ahmed Hesham
Ahmed Hesham

Posted on

Deploy Web App to IIS with Zero Downtime in Visual Studio

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:

  1. We will deploy our new web app to the private network server.
  2. Make sure it is working as intended.
  3. Make the private network server accessible to public network, now both servers can receive clients requests.
  4. Gracefully shutdown the original public network server; now it can't receive new requests but will finish its current requests.
  5. Make the original public network server accessible only to private network, then turn it on.
  6. 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.

Web Platform Installer

2. Get Application Request Routing (ARR)

  1. Open WebPI directly (start menu for example), not through IIS Manager.
  2. Search for ARR, then install.

Install ARR

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.

Create Websites

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

hosts File

Let's test it.

Test Website

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.

Create Server Farm

Add Two Servers

You will get a prompt suggesting to add rewrite rules, select no; we will add our own rewrite rules later.

Things should look like this:
Server Farm Set Up

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.

Test is-up.html

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.

Add Health Test

Verify.

Verify Health Test

Check Monitor and Management in the server farm.

Monitor and Management

6. Logging Problem

The health test makes a request to each server every second, and these requests are logged, which is not desirable.

Health Test Logs

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>


Enter fullscreen mode Exit fullscreen mode

This is how the end of the applicationHost.config file should look like.

End of applicationHost.config

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.

Add Blank Rule

The pattern will match any URL: .*
Match Any URL

Add a condition for the port to be 80
Only Port 80

The Action type will be: Route to Server Farm
Action Type

At the end, things will look like this:
URL Rewrite Rule

Let's test it.
Test Server-Farm

If we switch the 0 and 1 in the is-up.html files
Switch 0 and 1

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
    }
}


Enter fullscreen mode Exit fullscreen mode

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")


Enter fullscreen mode Exit fullscreen mode

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)"
}


Enter fullscreen mode Exit fullscreen mode

Remote Access

To run these scripts remotely, we will use Windows Remote Management (WinRM).

To configure WinRM:

  1. Make sure port 5985 is open through the network.
  2. Run winrm qc at remote server, select y when prompted.
  3. Make sure WinRM service is running locally.
  4. 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)))


Enter fullscreen mode Exit fullscreen mode

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 &quot;&amp; &quot;&quot;$(MSBuildProjectDirectory)\Properties\Scripts\GetUnhealthy.ps1&quot;&quot;&quot;"
            ConsoleToMSBuild="true">
        <Output TaskParameter="ConsoleOutput" PropertyName="DeployIisAppPath"/>
    </Exec>
</Target>


Enter fullscreen mode Exit fullscreen mode

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 &quot;&amp; &quot;&quot;$(MSBuildProjectDirectory)\Properties\Scripts\GetUnhealthy.ps1&quot;&quot;&quot;"
            ConsoleToMSBuild="true">
        <Output TaskParameter="ConsoleOutput" PropertyName="UnhealthySite"/>
    </Exec>
    <CreateProperty Value="$(UnhealthySite)/AppName">
        <Output TaskParameter="Value" PropertyName="DeployIisAppPath"/>
    </CreateProperty>
</Target>


Enter fullscreen mode Exit fullscreen mode

If you want to switch directly after deployment, add this code:



<Target Name="SwitchBlueGreen" AfterTargets="AfterPublish">
    <Exec Command="powershell -executionpolicy unrestricted &quot;&amp; &quot;&quot;$(MSBuildProjectDirectory)\Properties\Scripts\SwitchBlueGreen.ps1&quot;&quot;&quot;" />
</Target>


Enter fullscreen mode Exit fullscreen mode

In my case, I have two profiles: one with switching, and another without.

Now you can deploy to IIS with zero downtime.

See Also

Top comments (0)