Understanding Azure Custom Script Extension

At Build 2014 conference, Microsoft launched the Azure VM Custom Script Extension. This is a very useful and straightforward extension. However, understanding it in detail helps make better use of it. Before we get started, there are a few prerequisites we need to take care of. The Azure VM agent is the first requirement and if your VMs don’t have this already installed, you can follow the procedure on the Microsoft Azure team blog. We can enable VM extensions in an Azure VM using the Azure PowerShell cmdlets. In fact, you need to update the existing Azure PowerShell module to version 0.8.0 or later.

Once we have all these prerequisites met and we have authenticated to Azure, let us just run the Get-AzureVMAvailableExtension cmdlet to see what extensions are available to us.


Now that we know what extensions are available in general, we can use the Azure VM extension cmdlets to manage them.

The Get-AzureVMExtension cmdlet tells us what extensions are already installed in our Azure VM.

$Vm = Get-AzureVM -ServiceName "DSCDemo" -Name "DSCPull"
Get-AzureVMExtension -VM $Vm | Select ExtensionName, Publisher, Version


So, I have only the BGInfo Extension installed in my Azure VM. We can use the Set-AzureVMExtension cmdlet to install the Custom Script Extension.

Set-AzureVMExtension -ExtensionName CustomScriptExtension -VM $vm -Publisher Microsoft.Compute | Update-AzureVM -Verbose

4Now that we have the Custom Script Extension enabled, let’s take a look at the Set-AzureVMCustomScriptExtension cmdlet. This cmdlet helps us run a script available in a Azure storage account or at an Internet URI. Both methods have distinct use cases. You can find the binaries and log files related to this extension at C:\Packages\Plugins and C:\WindowsAzure\Logs\Plugins. If you think the script execution did not work as you expected, you can look at these locations to find what could have gone wrong. There is also PowerShell way of doing that. We will see that in detail in this article.

Executing scripts from your storage account

The -ContainerName parameter of the Set-AzureVMCustomScriptExtension cmdlet can be used to specify the Azure storage container where the scripts are stored. By default, the Custom Script Extension tries to find this container in the default Azure storage account. So, if you have multiple storage accounts, you will have to specify the -StorageAccountName parameter with the storage account name too. If this is not your own storage account, you need to specify the -StorageAccountKey parameter.

$Vm = Get-AzureVM -ServiceName "DSCDemo" -Name "DSCPull"
Set-AzureVMCustomScriptExtension -ContainerName scripts -StorageAccountName psmag -FileName user.ps1 -Run user.ps1 -VM $vm | Update-AzureVM -Verbose

The -FileName parameter can be used to specify all script files that need to be downloaded from the storage container. The -Run parameter specifies the script that we want to execute once the script files are downloaded. In my example above, I am using only one script and that is user.ps1. Since the -FileName parameter has only one script file, there is no need to explicitly specify -Run parameter. In the absence of -Run parameter, the Custom Script Extension will execute the first script in the list of file names provided as an input to the -FileName parameter.

When I run the above command, the user.ps1 gets downloaded to C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1 folder in the Azure VM. Once the script file is downloaded, it gets executed by CustomScriptHandler.exe process using the following PowerShell.exe commandline.

powershell -ExecutionPolicy Unrestricted -file user.ps1

5The contents of my user.ps1 script are simple. I wanted to find out what credentials does the Azure VM Custom Script Extension use when running the PowerShell scripts.

#Contents of user.ps1

Since I have already executed the script using the Custom Script Extension, I can look at the output generated by script by looking at the VM properties. We need to refresh the VM object by using the Get-AzureVM cmdlet.

$vm = Get-AzureVM -ServiceName DSCDemo -Name DSCPull

The ExtensionSettingStatus property under ResourceExtensionStatusList property of the Azure VM object contains the result of our script execution.


The Code property specifies the status code from the CustomScriptHandler.exe execution. The SubStatusList property contains the Standard Output (StdOut) and Standard Error (StdErr) streams from the script execution. So, in our script output, we should be able to see the output of ‘whoami‘ command.

$Vm.ResourceExtensionStatusList.ExtensionSettingStatus.SubStatusList | Select Name, @{"Label"="Message";Expression = {$_.FormattedMessage.Message }}


As we see here, the CustomScriptHandler.exe process runs as the System account. This means using the Azure VM Custom Script Extension we can run any sort of code even if it requires highest system privileges. This might sound scary but given the fact that using this extension requires access to your publish settings or Azure account, this can be assumed safe.

Output from a script execution

As you see in the above example, there are only two output streams available from the script execution – the standard output and the standard error. So, what happens if your script execution returns a PowerShell object or some type of an object other than string/text output? Let us see an example.

#Contents of process.ps1

To understand the script output behavior, I created another script called process.ps1 and copied it to the same Azure container as the above example. As you see, the script has just one command that is Get-Process which returns a collection of process objects. I will use the method we saw above to execute the script and collect the output.


As we see, the output from the Get-Process cmdlet gets converted to text.

Using the FileUri parameter

When using the -FileUri parameter, you need to specify the complete URI to the script file you want to execute inside the Azure VM. The user.ps1 and process.ps1 are available in a public Azure container and therefore if we know the storage account and container names, we can build the URI to access the script file. In my example, the storage account name is psmag and the container name is scripts. So, the URI for the user.ps1 script will be http://psmag.blob.core.windows.net/scripts/user.ps1.

Set-AzureVMCustomScriptExtension -FileUri http://psmag.blob.core.windows.net/scripts/user.ps1 -VM $vm -Verbose | Update-AzureVM -Verbose

Using the -FileUri parameter, the process of executing the script inside the Azure VM isn’t different. The CustomScriptHandler.exe process still downloads the file and then uses PowerShell.exe to execute that.

Passing arguments to scripts

You may want to pass arguments to the scripts that you plan to execute in the Azure VM using the Custom Script Extension. This is where the -Argument parameter comes handy. An example will explain this better.

Set-AzureVMCustomScriptExtension -FileUri http://psmag.blob.core.windows.net/scripts/hello.ps1 -Run hello.ps1 -Argument "-fname Windows -lname PowerShell" -VM $Vm | Update-AzureVM -Verbose


One caveat with the -Argument parameter is that it takes only string values as arguments. So, for the above example, if I want to specify the arguments without the named parameters, we need to specify the arguments for those named parameters as a single string.

Set-AzureVMCustomScriptExtension -FileUri http://psmag.blob.core.windows.net/scripts/hello.ps1 -Run hello.ps1 -Argument "Windows PowerShell" -VM $Vm | Update-AzureVM -Verbose

This results is the same output as the other example using named parameters.

Updating scripts downloaded to the Azure VM

Like I’d mentioned earlier, the scripts that we execute using the Custom Script Extension get downloaded to C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1 folder in the Azure VM. These script files do not get deleted after the script execution. And, the scripts get downloaded every time we run the Set-AzureVMCustomScriptExtension cmdlet. IMHO, this is not a very good design.

However, here is a caveat. If you use -FileUri to specify the path to a script file and you repeat the same command for the second time, you can see in the log files that the script file does not get downloaded again. This is true even if the script is updated. Here is the excerpt from the log file.

2014-04-29T17:59:33.9920457Z [Info]: Starting IaaS ScriptHandler Extension v1
2014-04-29T17:59:33.9920457Z [Info]: HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1", ConfigFolder: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\RuntimeSettings", StatusFolder: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\Status", HeartbeatFile: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\Status\HeartBeat.Json"]
2014-04-29T17:59:33.9920457Z [Info]: Enabling Handler
2014-04-29T17:59:33.9920457Z [Info]: Handler successfully enabled
2014-04-29T17:59:34.0076663Z [Info]: Loading configuration for sequence number 24
2014-04-29T17:59:34.0545913Z [Info]: HandlerSettings = ProtectedSettingsCertThumbprint: , ProtectedSettings: {}, PublicSettings: {FileUris: [https://psmag.blob.core.windows.net/scripts/process.ps1], CommandToExecute: powershell -ExecutionPolicy Unrestricted -file process.ps1 }
2014-04-29T17:59:34.0545913Z [Info]: Downloading files specified in configuration...
2014-04-29T17:59:34.1014232Z [Info]: DownloadFiles: fileUri = "https://psmag.blob.core.windows.net/scripts/process.ps1", baseUri = "https://psmag.blob.core.windows.net/"
2014-04-29T17:59:35.6763364Z [Info]: Files downloaded. Asynchronously executing command: 'powershell -ExecutionPolicy Unrestricted -file process.ps1 '
2014-04-29T17:59:35.6953042Z [Info]: Command execution task started. Awaiting completion...
2014-04-29T17:59:36.5209451Z [Info]: Command execution finished. Command exited with code: 0
2014-04-29T18:01:04.4843341Z [Info]: Starting IaaS ScriptHandler Extension v1
2014-04-29T18:01:04.4843341Z [Info]: HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1", ConfigFolder: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\RuntimeSettings", StatusFolder: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\Status", HeartbeatFile: "C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.0.1\Status\HeartBeat.Json"]
2014-04-29T18:01:04.4843341Z [Info]: Enabling Handler
2014-04-29T18:01:04.4843341Z [Info]: Handler successfully enabled
2014-04-29T18:01:04.4982194Z [Info]: Loading configuration for sequence number 24
2014-04-29T18:01:04.5294705Z [Info]: HandlerSettings = ProtectedSettingsCertThumbprint: , ProtectedSettings: {}, PublicSettings: {FileUris: [https://psmag.blob.core.windows.net/scripts/process.ps1], CommandToExecute: powershell -ExecutionPolicy Unrestricted -file process.ps1 }
2014-04-29T18:01:04.5450936Z [Warn]: Current sequence number, 24, is not greater than the sequence number of the most recently executed configuration. Exiting...

As you see towards the end of the above log, it finds that the sequence number generated for the execution is not greater than the previous run. Looking at the code for CustomScriptHandler.exe, I feel that this is a bug. The only workaround is to execute some other script and then try the Uri method. This downloads the script from storage container again.

So, this brings us to the end of our exploration of the new Azure VM Custom Script Extension. There are a couple of other cmdlets that can help manage this extension.

The Get-AzureVMCustomScriptExtension cmdlet gives you the details about the last script that was executed and the Remove-AzureVMCustomScriptExtension cmdlet removes the Custom Script Extension from the Azure VM.

Filed in: Articles, Azure, Online Only Tags: ,

11 Responses to "Understanding Azure Custom Script Extension"

  1. Gaurav Tyagi says:

    We are using custom script extension to remotely download and execute azure PowerShell script on azure VM. It was running but since yesterday evening we are getting below exception while running Set-AzureVMCustomScriptExtension on our build server(perhaps after deploying a cloud service from that server) :

    Get-AzureVM -ServiceName $AzureServiceName -Name $AzureSubServiceName | Set-AzureVMCustomScriptExtension -StorageAccountName $AzureStorageAccount -ContainerName $AzureStorageContainer -FileName $ZipFileName, $DeployScriptFileName, $WebServiceProfileName -Run $DeployScriptFileName -Argument “$ZipFileName $WebSiteRootPath $WebServiceProfileName $PowershellLogPath $UnZipLocationOnVM” |


    Getting below exception while trying to execute red highlighted line :

    Caught an exception while downloading the files

    Exception Type: System.Management.Automation.CmdletInvocationException

    Exception Message: A cmdlet named ‘Add-AzureCacheWorkerRole’ already exists. Cmdlets must have unique names.

    Could you please share inputs if any on this issue

    Thanks & Regards


  2. Gaurav Tyagi says:

    Hi Ravi,

    It is working for me now, it was to do with Azure configuration:(

    As the same script was working for me from other machines, I compared the azure files in both machines and found an extra folder(Scaffolding) at below path:

    C:\Program Files (x86)\Microsoft SDKs\Windows Azure\PowerShell\AZURE

    Removed this folder from above path and it resolved my exception.

    Thanks & Regards
    Gaurav Tyagi

  3. Gaurav Tyagi says:

    Hi Ravi,

    We tried above commands to get output while executing Powershell script on a vm using custom script extension.We are not able to get it despite executing above ve command :
    The SubStatusList property contains the Standard Output (StdOut) and Standard Error (StdErr) streams from the script execution.
    Could you please share more details on getting execution log of powershell script that has been executed on the VM

    Thanks & Regards
    Gaurav Tyagi

  4. Rajeev says:

    Hi Ravi,

    I am hoping to find an equivalent of “$Vm.ResourceExtensionStatusList.ExtensionSettingStatus.SubStatusList” for ARM VM. I am able to successfully run scripts using customscriptextension in ARM VMs but have not been able to figure out how to retrieve the result of my script.

  5. John says:


    I’m utilizing the following script block
    $azureVMInfo = Get-AzureVM -Name $AzureVMName -ServiceName $AzureCloudService
    $runCustomScript = $azureVMInfo | Set-AzureVMCustomScriptExtension -StorageAccountName $StorageAccountName -StorageAccountKey $storageKey -ContainerName $StorageContainerName -FileName $TestScript -Run $TestScript
    $updateAzureVM = Update-AzureVM -ServiceName $AzureCloudService -Name $AzureVMName -VM $azureVMInfo

    and keep getting this error –

    Set-AzureVMCustomScriptExtension : The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
    At C:\CustomScriptExtensionTest.ps1
    + $runCustomScript = $azureVMInfo | Set-AzureVMCustomScriptExtension -StorageAccou …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Set-AzureVMCustomScriptExtension], FormatException
    + FullyQualifiedErrorId : System.FormatException,Microsoft.WindowsAzure.Commands.ServiceManagement.IaaS.Extensions.SetAzureVMCustomScriptExtensionCommand

    Update-AzureVM : Cannot bind parameter ‘VM’. Cannot convert the “Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVMRoleContext” value of type
    “Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVMRoleContext” to type “Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVM”.
    At C:\CustomScriptExtensionTest.ps1:63 char:88
    + … zureVMName -VM $azureVMInfo
    + ~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Update-AzureVM], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.WindowsAzure.Commands.ServiceManagement.IaaS.UpdateAzureVMCommand

    I’m passing the VM object to Set-AzureVMCustomScriptExtension, why is it returning that it is not a valid Base-64 string? Then when I try to update the VM, even if I’m passing the same VM object, which does return information when I run Get-AzureVM, I’m not able to pass the VM object to Update-AzureVM as well?

  6. SomeGuy says:

    Running custom extension code as SYSTEM has its drawbacks.

    Write Custom Extensions to:
    Schedule a task to run as some other user (You CAN schedule it, but Access Denied when task tries to run)
    Starting a process with impersonation. (Access Denied)

    when SYSTEM tries to do these things – you get ‘Access Denied’

Leave a Reply

Submit Comment

© 2016 PowerShell Magazine. All rights reserved. XHTML / CSS Valid.
Proudly designed by Theme Junkie.
%d bloggers like this: