Deploy Web App to IIS with Zero Downtime in Visual Studio
Tags: tutorial, productivity, devops
Published: 2022-04-17 09:26:01

## Introduction

In my previous [post](https://dev.to/mark0960/publish-web-app-from-visual-studio-to-iis-using-web-deploy-56m6), 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](https://en.wikipedia.org/wiki/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](#bi)
* [Steps](#steps)
* [Scripts](#scripts)
* [Remote Access](#remote)
* [Integration with Visual Studio](#vs)

## Basic Idea <a name="bi"></a>

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 <a name="steps"></a>

### 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](https://www.microsoft.com/web/downloads/platform.aspx)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lswqi2ofdkf1yk70kqpu.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kplk8urekc6iry9ncem8.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dst2qc4mhtd4vutcrn7s.png)

Let's test it.

![Test Website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11pxot83jn52tn9nvg2z.png)

### 4. Configure the Server Farm

Create new [server farm](https://en.wikipedia.org/wiki/Server_farm), and add two servers to it with the port numbers we just assigned to the blue and green websites.

![Create Server Farm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dwf29gczytr3d0qinf38.png)

![Add Two Servers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/key1147b1c1konxmfuzi.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a92zrxbfh8ncxwfjek8w.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/37b240irdvup09bhobmm.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ez84jrycpz3gcsjo8cmi.png)

Verify.

![Verify Health Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iisnvh1lc5fza5jz0yk5.png)

Check Monitor and Management in the server farm.

![Monitor and Management](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h78gqcwgswmz9ud85hea.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3pb3g4i23elmundoynfp.png)

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.

![End of applicationHost.config](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hr21w1acb81xy1s7p89n.png)

### 7. Add URL Rewrite Rules

[URL Rewrite](https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-the-url-rewrite-module) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4z7lpcmpmzvt1mh2cqmr.png)

The pattern will match any URL: `.*`
![Match Any URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/evd6w3qcz3q7gapn18ju.png)

Add a condition for the port to be `80`
![Only Port 80](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cc0gbxc32lyj02ae3fcr.png)

The Action type will be: Route to Server Farm
![Action Type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mkzh3mwbguxdb6hc1etl.png)

At the end, things will look like this:
![URL Rewrite Rule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnzflwlb1l0k7ghd0a63.png)

Let's test it.
![Test Server-Farm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejjzho4u9ejczmkmu570.png)

If we switch the `0` and `1` in the `is-up.html` files
![Switch 0 and 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/scz0p2z1e52093cruyzd.png)

Now thigs are set up. Next, we will write some scripts to automate the deployment process.

## Scripts <a name="scripts"></a>

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 <a name="remote"></a>

To run these scripts remotely, we will use [Windows Remote Management (WinRM)](https://docs.microsoft.com/en-us/windows/win32/winrm/portal).

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](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-5.1)
```
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](https://docs.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections).

## Integration with Visual Studio <a name="vs"></a>

This builds on my previous [post](https://dev.to/mark0960/publish-web-app-from-visual-studio-to-iis-using-web-deploy-56m6).

We will use [Targets](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-targets?view=vs-2022) 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>
```
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>
```

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

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](https://kevinareed.com/2015/11/07/how-to-deploy-anything-in-iis-with-zero-downtime-on-a-single-server/)
Taught me most of the stuff I wrote here.

* [MSBuild](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild)