Configuring external startup tasks on an Azure cloud service

PaaSTaskWindows Azure cloud services support startup tasks to perform operations before a role starts, like installing a component, enabling a Windows feature, register a COM component, etc. by using .cmd files or just running Powershell scripts. The documentation for running startup tasks in Windows Azure is available on MSDN.

They are really powerful to setup things needed by your role. Since the startup tasks are packaged in your cloud service, the problem comes when you want to use the same cloud service for hundreds of deployments, and you need to customize at least some of these startup tasks per deployment. Or you just simply want to customize the startup tasks without rebuilding and/or redeploying your cloud service.

After thinking on different approaches to implement this scenario without adding more complexity to the application lifecycle management, a good way of doing it would be to have the opportunity to specify startup tasks on a location outside the cloud service package. In summary, to have a startup task that downloads a set of external startup tasks from an Uri and executes them in order.

Let’s see how this can be achieved.

You can download the example code from this Url: http://sdrv.ms/1dY86lt

Adding a startup task to download external startup tasks

ExternalStartupTasksIn the attached example, there is a solution containing a worker role and a cloud service project. The main aspects of this implementations are in the files:

  • ServiceDefinition.csdef
  • ServiceConfiguration.*.cscfg
  • WorkerRole.cs
  • scripts/SetupExternalTasks.cmd
  • scripts/SetupExternalTasks.ps1

Let’s check one by one to fully understand how this works.

ServiceDefinition.csdef

In the service definition file (ServiceDefinition.csdef), what we are going to do is to specify a new startup task as follows:

    <Startup>
      <Task executionContext="elevated" commandLine="scriptsSetupExternalTasks.cmd" taskType="simple">
        <Environment>
          <Variable name="EMULATED">
            <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" />
          </Variable>
          <Variable name="EXTERNALTASKURL">
            <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/ConfigurationSettings/ConfigurationSetting[@name='Startup.ExternalTasksUrl']/@value" />
          </Variable>
        </Environment>
      </Task>
    </Startup>

The startup task executes a script located on “scripts/SetupExternalTasks.cmd” in “simple” mode (the worker role OnStart event will not occur until the task completes, but you can change this behavior by modifying this attribute or by adding another task with the desired taskType). Two variables are passed:

  • EMULATED: to check if the task is being executed on the emulator or on Azure;
  • EXTERNALTASKURL: the Url of a zip file containing the external startup tasks;

ServiceConfiguration.cscfg

This Url is configured in the ServiceConfiguration.*.cscfg files as any other setting:

Settings

WorkerRole.cs

As the startup tasks are executed before the role starts, any change on this setting would recycle the role to execute them again. The following code just do this:

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // For information on handling configuration changes
            // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.

            // Setup OnChanging event
            RoleEnvironment.Changing += RoleEnvironmentOnChanging;

            return base.OnStart();
        }

        /// <summary>
        /// This event is called after configuration changes have been submited to Windows Azure but before they have been applied in this instance
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="RoleEnvironmentChangingEventArgs" /> instance containing the event data.</param>
        private void RoleEnvironmentOnChanging(object sender, RoleEnvironmentChangingEventArgs e)
        {
            // Implements the changes after restarting the role instance
            foreach (RoleEnvironmentConfigurationSettingChange settingChange in e.Changes.Where(x => x is RoleEnvironmentConfigurationSettingChange))
            {
                switch (settingChange.ConfigurationSettingName)
                {
                    case "Startup.ExternalTasksUrl":
                        Trace.TraceWarning("The specified configuration changes can't be made on a running instance. Recycling...");
                        e.Cancel = true;
                        return;
                }
            }
        }

scripts/SetupExternalTasks.cmd

Now let’s take a look to the task that is executed at the beginning. The variables are checked to skip this setup if you are running on a development environment –you may want to remove/comment the first line- or if the Url setting is empty:

if "%EMULATED%"=="true" goto SKIP
if "%EXTERNALTASKURL%"=="" goto SKIP

cd %ROLEROOT%approotscripts
md %ROLEROOT%approotscriptsexternal

reg add HKLMSoftwareMicrosoftPowerShell1ShellIdsMicrosoft.PowerShell /v ExecutionPolicy /d Unrestricted /f
powershell .SetupExternalTasks.ps1 -tasksUrl "%EXTERNALTASKURL%" >> ExternalTasks.log 2>> ExternalTasks_err.log

:SKIP
EXIT /B 0

Then a folder “external” is created to store all the downloaded stuff inside, to don’t interfere with other scripts in the solution while downloading. Finally the powershell script is called with the Url as parameter. Both standard and error outputs are redirected to log files.

scripts/SetupExternalTasks.ps1

Finally, the powershell script that does all the work is called. Note that the script supports three types of external files: a “.cmd” file; a “.ps1” file; or a “.zip” file that can contain one or more startup tasks:

 param (
    [string]$tasksUrl = $(throw "-taskUrl is required."),
    [string]$localFolder = ""
 )

# Function to unzip file contents
function Expand-ZIPFile($file, $destination)
{
    $shell = new-object -com shell.application
    $zip = $shell.NameSpace($file)
    foreach($item in $zip.items())
    {
        # Unzip the file with 0x14 (overwrite silently)
        $shell.Namespace($destination).copyhere($item, 0x14)
    }
}

# Function to write a log
function Write-Log($message) {
    $date = get-date -Format "yyyy-MM-dd HH:mm:ss"
    $content = "`n$date - $message"
    Add-Content $localfolderSetupExternalTasks.log $content
}

 if ($tasksUrl -eq "") {
    exit
 }

if ($localFolder -eq "") {
    $localFolder = "$pwdExternal"
}

# Create folder if does not exist
Write-Log "Creating folder $localFolder"
New-Item -ItemType Directory -Force -Path $localFolder
cd $localFolder


$file = "$localFolderExternalTasks.cmd"

 if ($tasksUrl.ToLower().EndsWith(".zip")) {
    $file = "$localFolderExternalTasks.zip"
 }
 if ($tasksUrl.ToLower().EndsWith(".ps1")) {
    $file = "$localFolderExternalTasks.ps1"
 }

# Download the tasks file
Write-Log "Downloading external file $file"
$webclient = New-Object System.Net.WebClient
$webclient.DownloadFile($tasksUrl,$file)

Write-Log "Download completed"

# If the tasks are zipped, unzip them first
 if ($tasksUrl.ToLower().EndsWith(".zip")) {
    Write-Log "Unzipping $localFolderExternalTasks.zip"
    Expand-ZIPFile -file "$localFolderExternalTasks.zip" -destination $localFolder
    Write-Log "Unzip completed"

    # When a .zip file is specied, only files called "Task???.cmd" and "Task???.ps1" will be executed
    # This allows to include assemblies and other file dependencies in the zip file
    Get-ChildItem $localFolder | Where-Object {$_.Name.ToLower() -match "task[0-9][0-9][0-9].[cmd|ps1]"} | Sort-Object $_.Name | ForEach-Object {
        Write-Log "Executing $localfolder$_"        
        if ($_.Name.ToLower().EndsWith(".ps1")) {
            powershell.exe "$localFolder$_"
        }
        elseif ($_.Name.ToLower().EndsWith(".cmd")) {
            cmd.exe /C "$localFolder$_"
        }
    }
 }
 elseif ($tasksUrl.ToLower().EndsWith(".ps1")) {
    powershell.exe $file
 }
 elseif ($tasksUrl.ToLower().EndsWith(".cmd")) {
    cmd.exe /C $file
 }

 Write-Log "External tasks execution finished"

In the case of a “.zip” file, the scripts expects the startup tasks with the name “Task???.cmd” or “Task???.ps1”, and executes them in order. Note that you can package file dependencies in the zip file such as .msi files, assemblies, other .cmd/.ps1 files, etc. and only the tasks with this pattern will be called by the script.

Seeing it in action

The attached example downloads an external .zip file located at https://davidjrh.blob.core.windows.net/public/code/ExternalTaskExample.zip. This .zip file contains two tasks “Task001.cmd” and “Task002.cmd”. After starting the role, we can verify that in the “scripts” subfolder the following files are created:

FilesCreated

The “ExternalTasks_err.log” is empty (0Kb in size), indicating that the execution was successful. We can open the “ExternalTasks.log” to verify that our tasks executed as expected. In this case, the tasks are simply showing some echoes:

TasksLog

Inside the “external” subfolder we can found all the downloaded .zip file, the unzipped contents and another log about the downloading and task processing steps:

ExternalTasks

SetupTasksLog

What if I need to get a configuration setting value from an external task?

In the case you need to get a service configuration value from a external task, you can use the awesome power of combining Powershell with managed code. The following Powershell function allows you to get a configuration setting value from inside your external startup in a .ps1 file:

[void] [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime")

function Get-ConfigurationSetting($key, $defaultValue = "")
{
    if ([Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::IsAvailable)
    {
        return [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue($key)
    }
    return $defaultValue
}

Once you have defined the function, you can use it with the following syntax. Note that the second parameter for the default value is optional.

$mySetting = Get-ConfigurationSetting "SettingName" "myDefaultValue"

Conclusion

Downloading and executing external startup tasks increase the versatility of the role startup tasks since you don’t need to repackage and upload your cloud services to change the behavior. By simply changing a setting they are automatically downloaded and executed previous starting the worker role.

You can download the example code from this Url: http://sdrv.ms/1dY86lt

Un saludo & Happy Coding!

davidjrh

David Rodriguez, is a happy Spanish guy living and working in Tenerife (Canary Islands, Spain) where he was born. He is one of the lucky ones who has the opportunity to work with cutting edge technologies at Intelequia as CTO. He has more than 20 years development background mostly based on Microsoft technologies, designing and architecting highly scalable systems like reservation systems for airlines companies. He has been working with Microsoft Azure since it was on CTP, migrating on-premise systems to the cloud, co-founding the .NET User Group TenerifeDev as well as the CSV company Intelequia Software Solutions. He is also the author of different DNN-Azure open source projects available on GitHub such as caching providers, analytics and Azure Active Directory.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *