The Azure custom script virtual machine extension (2.0) allows script execution on virtual machines (VMs) during Azure Resource Manager (ARM) template deployment. During script development however, using the extension is slightly annoying due to the way scripts are referenced. I needed a workaround that would let me change script content easily, especially during development.

Executing scripts

There are three ways you can get your script contact into the extension for execution: commandToExecute, script and fileUris.


This is the easiest method if you have something small, like a one or two liner to execute.

"commandToExecute": "apt-get -y update && apt-get install -y apache2"


Once you get past one or two lines, appending all your commands into commandToExecute causes the field to become unweildy - difficult to read and manage. script is base64 encoded (and optionally gzip’ed) which allows you to include line breaks.

Our script

apt-get -y update 
apt-get install -y apache2



Originally this was the approach I was taking. Note that the maximum size of the encoded data is 256 KB - any larger and the script won’t be executed.


Used in combination with the other two, fileUris is an array of of URLs that will be downloaded. For example, you could host a script on, and then in commandToExecute call

This method is kind of what I want, however my script is in a private git repo, so cannot be downloaded by the extension. The only credentials currently supported are storageAccountName and storageAccountKey, but if specified all fileUris must be URLs for Azure Blobs.

What’s my problem?

My script needs to either be publically hosted (or on Azure Blob Storage) so it can be downloaded, or, base64 encoded within the script parameter. I don’t want to store the script in two places, and I don’t want to re-encode & copy & paste every time I change the script back into the ARM template.

The solution

I’ll leave my script in GitHub, add a Personal Access Token (PAT) to my GitHub account, have the extension download the script with curl using the PAT, then execute the downloaded script. I want to securely store the PAT, so I’ll pass it as a parameter to the ARM template.

    "parameters": {
      "githubPat": {
        "type": "securestring"
      "vmScriptPath": {
        "type": "string",
        "defaultValue": ""
    "resources": [
        "type": "Microsoft.Compute/virtualMachines",
                "name": "my-custom-script",
                "type": "extensions",
                "location": "[resourceGroup().location]",
                "apiVersion": "2018-10-01",
                "dependsOn": [
                "tags": {
                  "displayName": "Execute my custom script"
                "properties": {
                  "publisher": "Microsoft.Azure.Extensions",
                  "type": "CustomScript",
                  "typeHandlerVersion": "2.0",
                  "autoUpgradeMinorVersion": true,
                  "protectedSettings": {
                    "commandToExecute": "[concat('curl -o ./ --header ''Authorization: token ', parameters('githubPat'), ''' --header ''Accept: application/vnd.github.v3.raw'' --remote-name --location ', parameters('vmScriptPath'), ' && chmod +x ./ && ./')]"

The action is predominantly in commandToExecute - lets see expand it and see what it does. An example of how to use the curl functionality specifically can be seen here.

# use curl and the GitHub PAT to download the script from the private repo
curl -o ./ \
    --header "Authorization: token parameters('githubPat')" \
    --header "Accept: application/vnd.github.v3.raw" \
    --remote-name \
    --location "parameters('vmScriptPath')"

# mark the script as executable
chmod +x ./

# execute the script

When will it run?

By default the extension will execute the script anytime configuration changes within the extension, but otherwise will only be executed once. This means if you update the referenced script only, and not the ARM config for the extension, the script is not going to run again.

The reference guide states that the easiest config change to re-trigger execution is the timestamp property:

Use this field only to trigger a re-run of the script by changing value of this field. Any integer value is acceptable; it must only be different than the previous value.

You might consider including it as a parameter "timestamp": "parameter('timestamp')" and calling it from your pipeline whenever you feel the need to re-execute.