Making IIS Configuration Changes in a Web Role Startup Task

The problem

If you're using Azure Cloud Services and you've ever tried to automate advanced IIS configuration changes in a web role, you've likely turned to startup tasks to accomplish this, only to quickly find out the hard way that some IIS configuration changes are actually very tricky to do in a startup task. Most likely you are either hit with strange exceptions when trying to make those changes, or the changes can be performed but seem to not take effect. The reason is not very obvious, and has to do with the sequence of events when a role is initialized: a web role initializes its IIS configuration after startup tasks are executed. This leads to two issues:

  1. It can be impossible to even perform the necessary changes on an IIS that has not yet been configured. For example, when your startup task executes the site object has not yet been created so if you write script logic that goes and looks for the site object, it will fail.
  2. More often than not, any configuration change you can ostensibly perform (such as reconfiguring SSL bindings) will more often than not be subsequently overwritten when IIS is initialized.

I have seen people try to overcome this obstacle in various ways. The most common is to have your startup task kick off some other asynchronous task which contains a delay, and waits a sufficient time for IIS to be configured, and then performs the necessary IIS configuration changes. While this approach may work in some cases, it's deeply flawed because:

  • The time it takes for the web role to initialize IIS is unpredictable and can vary based on a lot of different factors. You either have to make the delay long enough to make sure you give IIS ample time to initialize (in which case you are slowing down your startup performance even though it's seldom needed to wait that long) or make the delay shorter (in which case you are risking a race condition).
  • Since the asynchronous task does not delay role initialization, the role will be considered ready to accept incoming connections while in reality your IIS configuration task may not yet have finished doing its thing.
  • Depending on the nature of your changes, IIS may or may not require a restart. For some things, such as changing SSL binding configurations, not even restarting IIS is enough in some cases - HTTP.sys may need to be restarted which in most cases requires a reboot. After which your startup tasks will execute again, and you have to detect that you've already made the change and not trigger another reboot, and... yeah, you get the idea: things get messy quickly.

Windows Task Scheduler to the rescue

A while ago I came up with a more robust solution to this problem and since I have not yet seen a similar solution documented anywhere yet, I figure it's about time I share it.

Ideally what we really want to do is execute a task to make the IIS configuration changes immediately after the web role has finished initializing IIS. It turns out this is actually possible, because the service runtime is polite enough to log an event to the Windows Event Log upon finalizing the IIS setup. This means we can use this event as a trigger to kick off a scheduled task using the Windows Task Scheduler. This task can in turn execute a script or executable of our choosing, which performs the necessary IIS configuration changes. In other words, we can use a startup task to register a scheduled task to be triggered by a future event to defer the execution of the configuration changes to a later point in time when the IIS setup is finished.

To accomplish this you need to include five things in your role:

  1. The PowerShell script (or any other executable for that matter) that will ultimately make the IIS configuration change. Let's call this one ConfigureIis.ps1.
  2. A scheduled task definition XML file from which the scheduled task will be created. Let's call it ConfigureIisTaskDefinition.xml.
  3. A PowerShell script to register the scheduled task with Windows Task Scheduler. Let's call it RegisterScheduledTasks.ps1.
  4. The usual .cmd file to act as the main entry point for your startup task. Let's call it RunStartupScripts.cmd.
  5. The usual <Task> element in your ServiceDefinition.csdef file to declare the startup task.

Let's walk through each of these steps in more detail.

Please note that I have omitted any housekeeping concerns such as logging and error handling for clarity - for production use you will obviously want to be more robust. Also, remember that any file you include in your web role project for access by startup tasks needs to have its "Build Action" property set to "None" and "Copy to Output Directory" set to "Copy always" for it to be properly included in the publishing and packaging of your role, like so:

Adding ConfigureIis.ps1

The ConfigureIis.ps1 file could contain anything you want to do to your IIS. I won't go into specifics of what particular IIS configuration changes you might want to perform - that's up to you and beyond the scope of this post. This post is only about how to schedule an arbitrary script in such a way that it is able to make such changes at the opportune time in a robust way. I do have a couple of different examples of thing this method be used for, but I'll leave those for subsequent posts.

In any case, whatever logic this file contains, you will need to add it to your web role project.

Adding ConfigureIisTaskDefinition.xml

In newer versions of Windows the Windows Task Scheduler supports triggering tasks not only based on time but also on various other conditions, such as events being logged to the Windows Event Log. As mentioned above we'd like to schedule a task to be triggered by such an event that gets logged by the service runtime after it finishes setting up IIS. This event has event ID 10004 and gets logged in the "Windows Azure" log by the source "Windows Azure Runtime 2.4.0.0" (assuming you are using Azure SDK 2.4 - you will need to update the this version number whenever migrating to newer versions of the Azure SDK).

So how does one best automate the creation of such a scheduled task?

Well, the Windows Task Schedule includes a nifty feature allowing it to export and import scheduled task definitions to and from XML. We can take advantage of this capability to simplify the creation of the scheduled task by crafting an XML file for Windows Task Scheduler to import. You can either do this from scratch by using the Windows Task Scheduler UI to create the scheduled task interactively once just the way you want it and then export it to XML for inclusion in your role, OR you can use the below XML that I already created as your starting point. Either way you will end up with something like the following:

<?xml version="1.0" encoding="utf-16" ?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
 <RegistrationInfo>
  <Date>2013-03-29T17:07:06.5309093</Date>
  <Author>Daniel Stolt</Author>
 </RegistrationInfo>
 <Triggers>
  <EventTrigger>
   <Enabled>true</Enabled>
   <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Windows Azure"&gt;&lt;Select Path="Windows Azure"&gt;*[System[Provider[@Name='Windows Azure Runtime 2.4.0.0'] and EventID=10004]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
  </EventTrigger>
 </Triggers>
 <Principals>
  <Principal id="Author">
   <UserId>S-1-5-18</UserId>
   <RunLevel>HighestAvailable</RunLevel>
  </Principal>
 </Principals>
 <Settings>
  <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
  <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
  <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
  <AllowHardTerminate>true</AllowHardTerminate>
  <StartWhenAvailable>false</StartWhenAvailable>
  <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
  <IdleSettings>
   <StopOnIdleEnd>true</StopOnIdleEnd>
   <RestartOnIdle>false</RestartOnIdle>
  </IdleSettings>
  <AllowStartOnDemand>true</AllowStartOnDemand>
  <Enabled>true</Enabled>
  <Hidden>false</Hidden>
  <RunOnlyIfIdle>false</RunOnlyIfIdle>
  <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
  <UseUnifiedSchedulingEngine>false</UseUnifiedSchedulingEngine>
  <WakeToRun>false</WakeToRun>
  <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
  <Priority>7</Priority>
 </Settings>
 <Actions Context="Author">
  <Exec>
   <Command>PowerShell</Command>
   <Arguments>-Version 2.0 E:\approot\bin\ConfigureIis.ps1</Arguments>
  </Exec>
 </Actions>
</Task>

Things to note about this XML:

  1. The <EventTrigger> element is what specifies that this task should be triggered by an event. The <Subscription> element text is the actual query that specifies which particular event to listen for.
  2. The <Principal> element defines a principal named "Author" which maps to the SID "S-1-5-18" which resolves to the built-in SYSTEM user account.
  3. The <Actions> element specifies that the task should be executed in the context of this principal.
  4. The <Exec> element and its children specify the actual task to execute. Note that in this case we execute the "PowerShell" command and specify the arguments such that our ConfigureIis.ps1 script gets executed.

You will need to include this XML file in your web role project.

Adding RegisterScheduledTasks.ps1

This PowerShell script is the one that will be called by our startup task to interact with the Windows Task Scheduler to register the scheduled task using the above XML file. The script does this by means of the Register-ScheduledTask cmdlet which ships with PowerShell right out of the box. The script should end up looking something like this:

$rootPath = (Get-Location).Path
$taskName = "Make some IIS configuration changes"
$task = Get-ScheduledTask -TaskName $taskName  -ErrorAction SilentlyContinue
if (!$task)
{
    Register-ScheduledTask -TaskName $taskName -Xml (Get-Content $rootPath\ConfigureIisTaskDefinition.xml | Out-String) -TaskPath \MyTasks
}

Things to note about this script:

  1. I kept the task name generic; obviously you should choose a more descriptive name for the change you are actually making
  2. If a scheduled task already exists with the same name, no attempt is made to register it again. This makes the startup task resilient to reboots.
  3. The -TaskPath \MyTasks parameter creates a logical folder in the Windows Task Scheduler and places the scheduled task there. You will want to use a more appropriate folder name, such as your organization name. It has no functional effect, but it is common practice to group any custom scheduled tasks this way.

You will need to include this PowerShell file in your web role project.

Adding RunStartupScripts.cmd

This CMD file should also be added to your web role project, and constitutes the main entry point of the startup task. It should contain something like the following:

PowerShell -Version 2.0 -Command "Set-Executionpolicy Unrestricted"
PowerShell -Version 2.0 RegisterScheduledTasks.ps1

This CMD script does only two things:

  1. Sets the PowerShell execution policy to "Unrestricted" to allow for the execution of unsigned script files.
  2. Executes the RegisterScheduledTasks1.ps1 script.

Adding the <Task> element in your ServiceDefinition.csdef

This last step is no different from any other startup tasks you may have created before. In the ServiceDefinition.csdef file in your cloud service project, simply add configuration such as the following:

<Startup>
 <Task commandLine="RunStartupScripts.cmd" executionContext="elevated" taskType="simple" />
</Startup>

This schedules the RunStartupScripts.cmd file you added in the previou step for execution during role initialization. Please note that the executionContext attribute must be set to "elevated" for the startup task to have sufficient permissions to register a scheduled task.

Wrapping up

That's it. The service runtime should now execute your startup task during role initialization, your startup task should register a scheduled task with the Windows Task Scheduler, and that scheduled task should get executed immediately after IIS is set up in your web role, allowing you to perform any necessary advanced configuration changes to IIS at the appropriate time.

In subsequent posts, I'll be building on this approach to show you how to perform some specific tasks, such as disabling client certificate revocation checks and granting full file system permissions on your web application folder to your web code, both of which have to be performed after IIS initialization using this approach to be effective.

4 comments

  • ... Posted by russ Posted 10/13/2015 03:32 AM

    Daniel,

    Great post and I just about have it working but I am not comfortable with the full path: E:\approot\bin\ConfigureIis.ps In: ConfigureIisTaskDefinition.xml

    My drive letters constantly change from E to F etc... so I cant hard code the full path.

    What you think?

    thanks Russ

  • ... Posted by Russ Posted 10/13/2015 03:46 AM

    More: I suppose what I am asking is, can I pass this in: <Actions Context="Author"> <Exec> <Command>PowerShell</Command> <Arguments>-Version 2.0 E:\approot\bin\ConfigureIis.ps1</Arguments> </Exec> </Actions>

    As a parameter so I have control over the path.

    thanks Russ

  • ... Posted by Daniel Stolt Posted 01/09/2016 03:59 AM (Author)

    Hi Russ!

    Sorry for the late reply here, for some reason our notifications haven't been working.

    Don't know if this is still an open issue for you, but yes, it should be fairly easy to make the path variable, by adding a parameter to the ConfigureIis.ps1 script file and then using the built-in XML modification functionality of PowerShell to modify the value of the Task/Actions/Exec/Arguments element in the ConfigureIisTaskDefinition.xml file before you pass it on to the Windows Task Scheduler.

    That said, I have never seen the root drive changing between F: and E: ever in my own deployments - must be something relatively new.

  • ... Posted by Dustin Metzgar Posted 12/29/2016 01:13 AM [http://mode19.net]

    Thanks for this article. The switching between E: and F: happens when you update an existing cloud service VM. Daniel has a good solution for this, which I think would be implemented like this:

    $rootPath = (Get-Location).Path
    
    $xml = [xml](Get-Content -Path $rootPath\ConfigureSslTaskDef.xml)
    $xml.Task.Actions.Exec.Arguments.value = 'set config "' + $env:RoleInstanceID + '_MySite" -section:access -sslFlags:Ssl -commit:APPHOST'
    $xml.Save($rootPath\ConfigureSslTaskDef.xml)
    
    $taskName = "Configure SSL ignore client certificates"
    $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
    if (!$task)
    {
        Register-ScheduledTask -TaskName $taskName -Xml (Get-Content $rootPath\ConfigureSslTaskDef.xml | Out-String) -TaskPath \Workflow
    }
    

    I haven't tested this out yet, but I'll let you know if it works. This rewrites the arguments in the task definition XML to get the name of the site in IIS that I want to apply the SSL configuration to. I'm using an environment variable called RoleInstanceId that is only available to tasks started under the WindowsAzureGuestAgent process, like the startup tasks. My task definition XML is executing %windir%\system32\inetsrv\appcmd.exe. I only need the one command, but if you have multiple you may need to create a command file and pass in the values from these environment variables as arguments. The scheduled task does not have access to the same environment variables.

Leave a comment