Using IIS Application Request Routing to load balance on Azure

As previously discussed, cloud applications should be written to be stateless, and use the standard Azure round-robin load balancer. However, some applications need a sticky session, in which case, one option you have is to roll your own software load balancer. However the Application Request Routing (a software load balancer) module of IIS presents you with another option. The advantages of using ARR are

  1. It’s standard, well tested code doing the balancing.
  2. There are a number of load balancing strategies available.
  3. Client affinity (sticky sessions) is an option.

The topology of your deployment will look something like this:

The ARR Role will have ARR and the Web Farm Framework installed, and expose a public facing endpoint. It will be responsible for routing traffic to the Web Role instances. The Web Role will have an internal endpoint, but is otherwise unchanged.

Installing ARR

Download ARR and The Web Farm Framework IIS modules:

Create a Web Role project in your solution, copy the MSI files you downloaded into the solution, and set them to ‘copy if newer’. This will ensure they are packaged up with your application.

Add an elevated startup task (see how here) which will install these components. Your Startup.cmd file should contain these lines:

webfarm_amd64_en-US.msi /quiet
requestRouter_amd64_en-US.msi /quiet

Configuring ARR

In your service definition file, set your role to run elevated (see how here).

In your WebRole class (which inherits from RoleEntryPoint) add an override to the ‘Run’ method. The first thing to do is to update the binding configuration of the virtual directory to include the correct host name, and change the physical path of the default site to wwwroot, instead of the web pages packaged with your role. This can be done using the Microsoft.Web.Administration namespace:

using (ServerManager serverManager = new ServerManager())
	bool voteCommit = false;
	Configuration config = serverManager.GetApplicationHostConfiguration();
	ConfigurationSection sitesSection = config.GetSection("system.applicationHost/sites");
	ConfigurationElementCollection sitesCollection = sitesSection.GetCollection();
	ConfigurationElement siteElement = sitesCollection[0];
	ConfigurationElementCollection bindingsCollection = siteElement.GetCollection("bindings");
	ConfigurationElement bindingElement = FindElement(bindingsCollection, "binding", "protocol", @"http");
	if (bindingElement["bindingInformation"] as string != @"*")
		bindingElement["bindingInformation"] = @"*";
	ConfigurationElementCollection siteCollection = siteElement.GetCollection();
	ConfigurationElement applicationElement = FindElement(siteCollection, "application", "path", @"/");
	ConfigurationElementCollection applicationCollection = applicationElement.GetCollection();
	ConfigurationElement virtualDirectoryElement = FindElement(applicationCollection, "virtualDirectory", "path", @"/");
	if (virtualDirectoryElement["physicalPath"] as string != @"%SystemDrive%\inetpub\wwwroot")
		virtualDirectoryElement["physicalPath"] = @"%SystemDrive%\inetpub\wwwroot";

You then need to create a web farm, and add the redirection rule to point incoming requests to your farm. The web farm in this case is configured to use client affinity (sticky sessions).

using (ServerManager serverManager = new ServerManager())
	// create the server farm
	Configuration config = serverManager.GetApplicationHostConfiguration();
	ConfigurationSection webFarmsSection = config.GetSection("webFarms");
	ConfigurationElementCollection webFarmsCollection = webFarmsSection.GetCollection();
	var el = FindElement(webFarmsCollection, "webFarm", "name", farmName);
	if (null != el) return;
	ConfigurationElement webFarmElement = webFarmsCollection.CreateElement("webFarm");
	webFarmElement["name"] = farmName;
	ConfigurationElement applicationRequestRoutingElement = webFarmElement.GetChildElement("applicationRequestRouting");
	ConfigurationElement affinityElement = applicationRequestRoutingElement.GetChildElement("affinity");
	affinityElement["useCookie"] = true;

	// create the rewrite rule
	Configuration config = serverManager.GetApplicationHostConfiguration();
	ConfigurationSection globalRulesSection = config.GetSection("system.webServer/rewrite/globalRules");
	ConfigurationElementCollection globalRulesCollection = globalRulesSection.GetCollection();
	ConfigurationElement ruleElement = globalRulesCollection.CreateElement("rule");
	ruleElement["name"] = serverFarm;
	ruleElement["patternSyntax"] = @"Wildcard";
	ruleElement["stopProcessing"] = true;
	ConfigurationElement matchElement = ruleElement.GetChildElement("match");
	matchElement["url"] = "*";
	matchElement["ignoreCase"] = true;
	ConfigurationElement actionElement = ruleElement.GetChildElement("action");
	actionElement["type"] = @"Rewrite";
	actionElement["url"] = string.Concat(@"http://", serverFarm, @"/{R:0}");

These tasks need only to be run once.

You should then set up a simple loop, which will inspect the instances running, and modify the farm according to changes in the topology. Adding a server is straight forward, and can be done with some code like this:

public static void AddServer(string farmName, string ipAddress, int portNumber)
	using (ServerManager serverManager = new ServerManager())
		Configuration config = serverManager.GetApplicationHostConfiguration();
		ConfigurationSection webFarmsSection = config.GetSection("webFarms");
		ConfigurationElementCollection webFarmsCollection = webFarmsSection.GetCollection();
		ConfigurationElement webFarmElement = FindElement(webFarmsCollection, "webFarm", "name", farmName);
		ConfigurationElementCollection webFarmCollection = webFarmElement.GetCollection();
		var server = FindElement(webFarmCollection, "server", "address", ipAddress);
		if (null != server)
			// server already exists
		ConfigurationElement serverElement = webFarmCollection.CreateElement("server");
		serverElement["address"] = ipAddress;
		ConfigurationElement applicationRequestRoutingElement = serverElement.GetChildElement("applicationRequestRouting");
		applicationRequestRoutingElement["httpPort"] = portNumber;

You would need additional code to for servers being, removed, and IP address changes.