Installing and using an HttpModule in Windows Azure

Windows Azure has a lot of great things about it, but one of the difficulties is that you can’t administer your IIS very easily as you don’t have access to the machine.  If you use the Windows Azure 1.3 SDK or later and enable RDP, you can change IIS settings but if the role has to be restarted for some reason, it will lose whatever changes you have made.

So this is where using the OnStart of the WebRole can really help a developer out.  For this example, I am going to use the IIS Application Warm-Up module.  This is a good example because it is a rather complicated module to install and use properly.

Here is how you go about getting this module running in Windows Azure.

Note: You must be using the Windows Azure 1.3 SDK and have your site running using Full IIS for this to work.

  1. Download the IIS Application Warm-Up module on your local machine and install it.  This will get you the actual module: %windir%\system32\inetsrv\appwarmup.dll.

  2. Add the appwarmup.dll module above to your Visual Studio WebRole project.  Add it to the root of the application and be sure under Properties you have Build Action set to Content and Copy to Output Directory set to Copy always.

  3. Add a Reference in the WebRole project to %windir%\system32\inetsrv\Microsoft.Web.Administration.dll.

  4. Under the Properties of the newly added reference, change Copy Local to True.

  5. Open the WebRole.cs file in the WebRole project, Modify it with the following code:

        1: using System;
        2: using System.Collections.Generic;
        3: using System.Linq;
        4: using Microsoft.WindowsAzure;
        5: using Microsoft.WindowsAzure.Diagnostics;
        6: using Microsoft.WindowsAzure.ServiceRuntime;
        7: using Microsoft.Web.Administration;
        8:  
        9: namespace WebRole1
       10: {
       11:     public class WebRole : RoleEntryPoint
       12:     {
       13:         public override bool OnStart()
       14:         {
       15:             // For information on handling configuration changes
       16:             // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357.
       17:  
       18:             try
       19:             {
       20:                 System.Diagnostics.Trace.WriteLine("OnStart");
       21:                 // Before we do anything else, we have to create the schema file and write it to disk
       22:                 string appwarmupSchemaLocation = System.IO.Path.Combine(Environment.GetEnvironmentVariable("windir") + @"\", @"system32\inetsrv\config\schema\appwarmup_schema.xml");
       23:                 if (!System.IO.File.Exists(appwarmupSchemaLocation))
       24:                 {
       25:                     string[] lines = {
       26:                         "<configSchema>",
       27:                         "    <sectionSchema name=\"system.webServer/httpWarmupGlobalSettings\">",
       28:                         "      <attribute name=\"httpWarmupEnabled\" type=\"bool\" defaultValue=\"true\" />",
       29:                         "    </sectionSchema>",
       30:                         "",
       31:                         "    <sectionSchema name=\"system.webServer/httpWarmup\">",
       32:                         "      <element name=\"userContext\">",
       33:                         "        <attribute name=\"authMode\" type=\"enum\" defaultValue=\"SetNull\" >",
       34:                         "          <enum name=\"SetNull\" value=\"0\" />",
       35:                         "          <enum name=\"SetUserOnly\" value=\"1\" />",
       36:                         "          <enum name=\"SetToken\" value=\"2\" />",
       37:                         "        </attribute>",
       38:                         "        <attribute name=\"authType\" type=\"string\" defaultValue=\"\" />",
       39:                         "        <attribute name=\"username\" type=\"string\" defaultValue=\"\" />",
       40:                         "        <attribute name=\"password\" type=\"string\" encrypted=\"true\" caseSensitive=\"true\" defaultValue=\"[enc:AesProvider::enc]\" />",
       41:                         "      </element>",
       42:                         "      <element name=\"requests\" >",
       43:                         "        <collection addElement=\"add\" clearElement=\"clear\" removeElement=\"remove\" mergeAppend=\"false\" >",
       44:                         "          <attribute name=\"requestUrl\" type=\"string\" required=\"true\" isUniqueKey=\"true\" />",
       45:                         "          <attribute name=\"allowedResponseCodes\" type=\"string\" defaultValue=\"200-399\" />",
       46:                         "          <attribute name=\"warmupContext\" type=\"string\" />",
       47:                         "          <attribute name=\"sendMode\" type=\"enum\" defaultValue=\"Asynchronous\" >",
       48:                         "            <enum name=\"Synchronous\" value=\"1\" />",
       49:                         "            <enum name=\"Asynchronous\" value=\"2\" />",
       50:                         "          </attribute>",
       51:                         "        </collection>",
       52:                         "      </element>",
       53:                         "    </sectionSchema>",
       54:                         "",
       55:                         "</configSchema>"};
       56:  
       57:                     System.IO.File.WriteAllLines(appwarmupSchemaLocation, lines);
       58:                 }
       59:  
       60:                 // Create an instance of our ServerManager
       61:                 ServerManager iisManagerFirst = new ServerManager();
       62:  
       63:                 // Get the applicationHost.config file
       64:                 Configuration appHostConfigFirst = iisManagerFirst.GetApplicationHostConfiguration();
       65:  
       66:                 // Create a Section Definition for our new settings under <system.webServer>
       67:                 SectionDefinition definition = RegisterSectionDefinition(appHostConfigFirst, "system.webServer/httpWarmupGlobalSettings");
       68:                 definition.OverrideModeDefault = "Deny";
       69:                 definition.AllowDefinition = "MachineToApplication";
       70:                 SectionDefinition definition2 = RegisterSectionDefinition(appHostConfigFirst, "system.webServer/httpWarmup");
       71:                 definition2.OverrideModeDefault = "Allow";
       72:                 definition2.AllowDefinition = "MachineToApplication";
       73:  
       74:                 // Write these new settings out to the file, this allows us to add them later, otherwise we will get an exception
       75:                 // because the section doesn't have a definition
       76:                 iisManagerFirst.CommitChanges();
       77:  
       78:                 // Get another instance of our ServerManager as the first one is committed so it is read-only
       79:                 ServerManager iisManager = new ServerManager();
       80:  
       81:                 // Find our application
       82:                 Application app = iisManager.Sites[RoleEnvironment.CurrentRoleInstance.Id + "_Web"].Applications[0];
       83:  
       84:                 // Get the applicationHost.config file
       85:                 Configuration appHostConfig = iisManager.GetApplicationHostConfiguration();
       86:  
       87:                 // Get the appPool for this application
       88:                 ConfigurationSection appPoolSection = appHostConfig.GetSection("system.applicationHost/applicationPools");
       89:                 ConfigurationElementCollection poolCollection = appPoolSection.GetCollection();
       90:                 ConfigurationElement appPoolElement = FindElement(poolCollection, "add", "name", app.ApplicationPoolName);
       91:  
       92:                 if (appPoolElement == null) throw new InvalidOperationException("App Pool element not found!");
       93:  
       94:                 // Set the attribute startMode="AlwaysRunning"
       95:                 appPoolElement.SetAttributeValue("startMode", "AlwaysRunning");
       96:  
       97:                 // Add AppWarmupModule to the globalModules section of system.webServer, only if it doesn't already exist
       98:                 ConfigurationSection globalSection = appHostConfig.GetSection("system.webServer/globalModules");
       99:                 ConfigurationElementCollection globalCollection = globalSection.GetCollection();
      100:                 ConfigurationElement globalElement = FindElement(globalCollection, "add", "name", "AppWarmupModule");
      101:  
      102:                 if (globalElement == null)
      103:                 {
      104:                     ConfigurationElement globalElement2 = globalCollection.CreateElement("add");
      105:                     globalElement2.SetAttributeValue("name", "AppWarmupModule");
      106:                     string appwarmupLocation = System.IO.Path.Combine(Environment.GetEnvironmentVariable("RoleRoot") + @"\", @"approot\appwarmup.dll");
      107:                     System.Diagnostics.Trace.WriteLine("Location: " + appwarmupLocation);
      108:                     globalElement2.SetAttributeValue("image", appwarmupLocation);
      109:                     globalCollection.Add(globalElement2);
      110:                 }
      111:  
      112:                 // Add AppWarmupModule to the modules section of system.webServer, only if it doesn't already exist
      113:                 // This is adding to the <location path="" ...> section, which is why we have a second parameter
      114:                 ConfigurationSection modulesSection = appHostConfig.GetSection("system.webServer/modules", "");
      115:                 ConfigurationElementCollection modulesCollection = modulesSection.GetCollection();
      116:                 ConfigurationElement modulesElement = FindElement(modulesCollection, "add", "name", "AppWarmupModule");
      117:  
      118:                 if (modulesElement == null)
      119:                 {
      120:                     System.Diagnostics.Trace.WriteLine("Add AppWarmupModule");
      121:                     ConfigurationElement modulesElement2 = modulesCollection.CreateElement("add");
      122:                     modulesElement2.SetAttributeValue("name", "AppWarmupModule");
      123:                     modulesCollection.Add(modulesElement2);
      124:                 }
      125:  
      126:                 // Add httpWarmupEnabled to the new httpWarmupGlobalSettings section of system.webServer
      127:                 // This is adding to the <location path="OurRoleId" ...> section, which is why we have a second parameter
      128:                 ConfigurationSection warmupSection = appHostConfig.GetSection("system.webServer/httpWarmupGlobalSettings", RoleEnvironment.CurrentRoleInstance.Id + "_Web");
      129:                 System.Diagnostics.Trace.WriteLine("Add httpWarmupEnabled");
      130:                 warmupSection.SetAttributeValue("httpWarmupEnabled", "true");
      131:  
      132:                 // Add our requests to the new httpWarmup section of system.webServer
      133:                 // This is adding to the <location path="OurRoleId" ...> section, which is why we have a second parameter
      134:                 ConfigurationSection warmupSection2 = appHostConfig.GetSection("system.webServer/httpWarmup", RoleEnvironment.CurrentRoleInstance.Id + "_Web");
      135:                 ConfigurationElement requestsElement = warmupSection2.GetChildElement("requests");
      136:                 ConfigurationElementCollection warmupCollection = requestsElement.GetCollection();
      137:  
      138:                 // TODO: change <Warmup File.aspx> to the correct filename we want to browse to warm up the application
      139:                 ConfigurationElement warmupElement = FindElement(warmupCollection, "add", "requestUrl", "<Warmup File.aspx>");
      140:  
      141:                 if (warmupElement == null)
      142:                 {
      143:                     ConfigurationElement warmupElement2 = warmupCollection.CreateElement("add");
      144:  
      145:                     // TODO: change <Warmup File.aspx> to the correct filename we want to browse to warm up the application
      146:                     warmupElement2.SetAttributeValue("requestUrl", "<Warmup File.aspx>");
      147:                     warmupElement2.SetAttributeValue("allowedResponseCodes", "200-399");
      148:                     warmupElement2.SetAttributeValue("warmupContext", "This is a warmup request");
      149:                     warmupElement2.SetAttributeValue("sendMode", "Asynchronous");
      150:                     warmupCollection.Add(warmupElement2);
      151:                 }
      152:  
      153:                 System.Diagnostics.Trace.WriteLine("CommitChanges");
      154:                 iisManager.CommitChanges();
      155:             }
      156:             catch (Exception e)
      157:             {
      158:                 System.Diagnostics.Trace.WriteLine(e.ToString());
      159:             }
      160:  
      161:             return base.OnStart();
      162:         }
      163:  
      164:         /// <summary>
      165:         /// This function will create a SectionDefinition given a configuration and a path, the path can have /'s in it
      166:         /// </summary>
      167:         /// <param name="config"></param>
      168:         /// <param name="sectionPath"></param>
      169:         /// <returns></returns>
      170:         private SectionDefinition RegisterSectionDefinition(Configuration config, string sectionPath)
      171:         {
      172:             string[] paths = sectionPath.Split('/');
      173:  
      174:             SectionGroup group = config.RootSectionGroup;
      175:             for (int i = 0; i < paths.Length - 1; i++)
      176:             {
      177:                 SectionGroup newGroup = group.SectionGroups[paths[i]];
      178:                 if (newGroup == null)
      179:                 {
      180:                     System.Diagnostics.Trace.WriteLine("Creating: " + paths[i]);
      181:                     newGroup = group.SectionGroups.Add(paths[i]);
      182:                 }
      183:  
      184:                 group = newGroup;
      185:             }
      186:  
      187:             SectionDefinition section = group.Sections[paths[paths.Length - 1]];
      188:             if (section == null)
      189:             {
      190:                 System.Diagnostics.Trace.WriteLine("Creating: " + paths[paths.Length - 1]);
      191:                 section = group.Sections.Add(paths[paths.Length - 1]);
      192:             }
      193:  
      194:             return section;
      195:         }
      196:  
      197:         /// <summary>
      198:         /// This function will look for an element with the given name/value pairs and the given elementTagName
      199:         /// </summary>
      200:         /// <param name="collection"></param>
      201:         /// <param name="elementTagName"></param>
      202:         /// <param name="keyValues"></param>
      203:         /// <returns></returns>
      204:         private ConfigurationElement FindElement(ConfigurationElementCollection collection, string elementTagName, params string[] keyValues)
      205:         {
      206:             foreach (ConfigurationElement element in collection)
      207:             {
      208:                 if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase))
      209:                 {
      210:                     bool matches = true;
      211:                     for (int i = 0; i < keyValues.Length; i += 2)
      212:                     {
      213:                         object o = element.GetAttributeValue(keyValues[i]);
      214:                         string value = null;
      215:                         if (o != null)
      216:                             value = o.ToString();
      217:  
      218:                         if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase))
      219:                         {
      220:                             matches = false;
      221:                             break;
      222:                         }
      223:                     }
      224:  
      225:                     if (matches)
      226:                         return element;
      227:                 }
      228:             }
      229:  
      230:             return null;
      231:         }
      232:     }
      233: }
    

    Update (2/23/2011): I have made a few updates above based on some testing.

    Note: On lines 82, 128, 134 this code uses “_Web” as part of the role name.  If you change the name in the ServiceDefinition.csdef file (<Site name=”Web”>) then you will need to adjust that code accordingly.  Also, if you have multiple sites, all 3 of these locations (adding the values there) will need to be done for each site you want to warm up.

  6. In the OnStart function that we just added above, change the two instances of <Warmup File.aspx> (these are on lines 139 and 146 in the above code) to point to the file you want to have browsed for warming up your application.

    Note: You can add additional requests and more advanced things to this, check out the Advanced Scenarios section of Getting Started with the IIS 7.5 Application Warm-Up Module for more information.

  7. Open the ServiceConfiguration.cscfg file and ensure the osFamily attribute of the ServiceConfiguration element is set to “2”.  This will ensure you are using Windows Server 2008 R2 which has IIS 7.5 which is required for the IIS Application Warm-Up module.

  8. Open the ServiceDefinition.csdef file and add the following entry inside the <WebRole > section:

     <Runtime executionContext="elevated" />
    
  9. Build your application and upload it to Windows Azure.

Once you have done these steps, your application should be running now using the Warm-Up module.  So the first request to a page, once the site is ready to run, should be just as fast as any other request.

There is another alternative, instead of doing all of this in the OnStart of your WebRole and forcing your Role to run elevated, you can also create a StartupTask to do the same thing and have it run elevated.  That would basically entail doing this same code in your own exe and then running the StartupTask.  There is a good discussion of them at Azure Startup Tasks and Powershell- Lessons Learned - Jim O'Neil