June 20, 2014

Automating .NET 3.5 offline installation on Azure roles

We have a .NET 3.5 based web application which runs on Azure. As a part of deployment we runs couple of start-up tasks. One of the start-up task install .NET 3.5 in the roles.

Recently we faced down time for this website. We did some analysis and found start-up tasks together takes around 30 minutes to finish. The reason for down time was azure Fabric Controller will wait 15 minutes for a role in an upgrade domain to start before proceeding to a role in the next upgrade domain. 15 minute limit is about how long the Fabric Controller will wait for a role instance to go to the Ready state during a role update. This cause both of our role instances to be in busy state and stop responding to HTTP requests for long time.

We checked the start-up scripts one by one and noticed that the start-up script that installs .NET 3.5 runs the below command:

PowerShell -Command "Add-WindowsFeature Net-Framework-Core" >> "%TEMP%\StartupLog.txt" 2>&1

This command connect to WSUS (Windows Server Update Service) to install .NET 3.5. Downloading .NET 3.5 files will take some time because NET 3.5 install files are around 250 MB. Note that our roles are running in 'Windows Server 2012 R2' VMs. Only way to automate .NET 3.5 installation in OS starting from 'Windows Server 2008 R2' is via 'Add-WindowsFeature' command.

If you have the .NET 3.5 install files available locally then we can run above command with -Source option:

Add-WindowsFeature Net-Framework-Core –Source "<Path-To-NET35-Install-Files>"

How to get NET35-Install-Files?

If you have Windows Installation media, you can find these files under "\sources\sxs".  You can compress contents of this directory and include it in the azure package or can upload to azure blob storage.

Now we need to modify the start-up task to get this compressed file, extract it and run 'Add-WindowsFeature Net-Framework-Core' with -Source option.

Note that we cannot extract these files to role's %TEMP% directory, because we need more than 250 MB disk space and if we use %TEMP% then we will get 'Not enough space on disk' error. So solution is to use Azure Role local store, allocate 350 MB space for the resource. Let's call this local resource as 'net35resource'

Since we have to un-compress the zip file, include 7z binaries in the azure package. Easy way to get these binaries is to install 7zip in your dev machine, go to 'ProgramFiles(x86)\7-Zip' and copy the files '7z.exe', '7z.dll' and '7-zip.dll'.

Now we will write a powershell script file, lets name it 'installnet35.ps1'.

# Method that returns path to the directory holding 'installnet35.ps1' script. 
function Get-ScriptDirectory
  $Invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $Invocation.MyCommand.Path

# Gets path to the local resource we reserved for manipulating the zip file.
$localStoreRoot = ([Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource("net35resource")).RootPath.TrimEnd('\\')

# .NET 3.5 source (in blob storage)
# Note that you can also include the .NET 3.5 source zip file in the package
$net35Source = "http://<storage-account-name>.blob.core.windows.net/<container>/net35.zip"

# Destination path for the zip file
$net35ZipDestination = Join-Path $localStoreRoot "net35.zip"

# Use WebClient to download the the zip file
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($net35Source, $net35ZipDestination)

# Destination path to hold the extracted files
$net35ExtractDestination = Join-Path $localStoreRoot "net35"
$pathExists = Test-Path $net35ExtractDestination
if (!$pathExists)
  new-item $net35ExtractDestination -itemtype directory

# Build command to unzip
$zipTool = (Join-Path (Get-ScriptDirectory) "\7z.exe")
$unzipCommandArgs = "x " + $net35ZipDestination + " -o$net35ExtractDestination -y"

# Unzip the file
Start-Process $zipTool $unzipCommandArgs -NoNewWindow -Wait
$net35ExtractDestination = Join-Path $net35ExtractDestination "net35"

# Install .NET 3.5 using -Source option
Install-WindowsFeature NET-Framework-Core –Source Join-Path $net35ExtractDestination

Next we need a batch file to run the above powershell script in unrestricted mode. lets name it 'installnet35.cmd'.

PowerShell -ExecutionPolicy Unrestricted "%~dp0..\..\Startup\installnet35.ps1" >> "%TEMP%\StartupLog.txt" 2>&1

We need to include all these files in project's Startup directory as shown below.

Note that for all these files under 'Startup' you need to set 'Build Action' as 'Content' and 'Copy To output Directory' as 'Copy Always'

Last step is to register 'installnet35.cmd' as a startup task via 'ServiceDefinition.config' file.

<Startup priority="-2">
  <Task commandLine=".\Startup\installnet35.cmd" executionContext="elevated" taskType="simple">
      <Variable name="EMULATED">
        <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" />