Passing Mutliple Arguments to Custom Script in an ARM Template

In a lot of scenarios I find myself adding the custom script extension to my Azure Resource Manage (ARM) templates to further configure the virtual machine. In some cases this script can end up with a lot of different parameters that have to be passed in.

The normal way of doing this is to use the concat() string function in the template, but this starts to get very difficult to read very quickly, for example:

{
   "commandToExecute": "[concat('myscript.sh', ' -o install,config -u ', variables('username'), ' -p \"', concat(variables('password'), '\"')]"
}

Not only is this very hard to read, it is also very hard to debug and only gets worse when more arguments are added, especially when the need arises for double quotes where escaping is required (as shown above).

This post shows a way to pass in a Base64 encoded JSON string of all the parameters that need to be passed in. The scripts themselves need to be able to handle this, but it makes it so much easier to edit the parameters and see what is being passed in.

I work with both Windows and Linux in Azure, so the examples will show to accomplish this using both Bash and PowerShell.

ARM Template

All virtual machines, be it Windows or Linux, follow the same pattern when it comes to deployment. The machine is provisioned and then the CustomScript extension is deployed after the machine has completed. It is in this resource that the object to pass to the script is created.

In this simple example three parameters are going to be passed.

  • USERNAME
  • PASSWORD
  • FULLNAME

The thing to note here is that the names of the parameters are not being passed in, rather the names of the variables of the script. This means that the values set in the template are closely coupled with the script, so if a variable name is changed then it will need to be changed in the template as well.

The following snippet shows how a template variable can be set as an object of these values and how the CustomScript extension would consume this.

{
    "parameters": {
        "name": {
            "type": "string",
        },
        "username": {
            "type": "string"
        },
        "password": {
            "type": "securestring"
        },
        "fullname": {
            "type": "string"
        },
        "scriptUrl": {
            "type": "string"
        }
    },
    "variables": {
        "arguments": {
            "USERNAME": "[parameters('username')]",
            "PASSWORD": "[parameters('password')]",
            "FULLNAME": "[parameters('fullname')]"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Compute/virtualMachinesExtensions",
            "name": "[parameters('name')]",
            "location": "[resourceGroup().location]",
            "apiVersion": "2015-06-15",
            "properties": {
                "publisher": "Microsoft.Azure.Extensions",
                "type": "CustomScript",
                "typeHandlerVersion": "2.0",
                "autoUpgradeMinorVersion": true,
                "settings": {
                    "fileUris": [
                        "[parameters('scriptUrl')]"
                    ]
                },
                "commandToExecute": "[concat('script.sh ', base64(string(variables('arguments')))]"
            }
        }
    ]
}

For a Windows deployment the following substituions would need to be made:

Parameter Current Value New Value
publisher Microsoft.Azure.Extensions Microsoft.Compute
type CustomScript CustomScriptExtension
typeHandlerVersion 2.0 1.9

The script will also be a PowerShell script so script.sh would change to script.ps1 in this example.

The template is using two functions:

  1. string - Turn a JSON object into a string (https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-string#string)
  2. base64 - Base64 encode a string (https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-string#base64)

These two functions allow any JSON object to be encoded as a Base64 encoded string.

Bash Script

As stated previously the script needs to be configured to handle the Base64 string that is being passed to it. Bash does not have built-in support for JSON (unlike PowerShell) so the script will need to handle this too.

The following example is designed to run on Debian / Ubuntu machine. However it would not be hard to make it work for other Linux distributions.

# Define the variables that will be set
USERNAME=""
PASSWORD=""
FULLNAME=""

# Ensure that `jq` is installed on the machine
jq=`which jq`
if [ "X$jq" == "X" ]
then
    `apt-get install -y jq`
fi

# Get the base64 encoded string
BASE64_ENCODED=$1

# Decode the encoded string into a JSON string
echo $BASE64_ENCODED | base64 --decode | jq . > args.json

# Read the args file in and set the script variables
VARS=`cat args.json | jq -r '. | keys[] as $k | "\"($k)=\"\(.[$k])\""'`

# Evaluate all the vars
for VAR in "$VARS"
do
    eval "$VAR"
done

echo $USERNAME

With this simple (and very non-production script) it is possible to read in the string generated by the ARM template and write it out to a file. The JSON does not have to be written out a file a file, but it really helps when it comes to debugging what has been sent to the extension and thus the script.

There is potential for abuse here given the fact that eval is being used to turn the JSON object into variables. Be careful on where and how this is exposed. For example, unlike the example above, it would not be prudent to have the actual parameters as the values of the variables. The values should be generated so that there is less chance of malicious code being run.

PowerShell

PowerShell makes life much easier as it has the ability to read a JSON object built-in. This makes the script shorter.

param (
    [string]
    # Base64 encoded string
    $base64_encoded
)

# Decode the base64 encoded string
$json_string = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_encoded))

# Write out the json_string to a file
Set-Content -Name args.json -Value $json_string

# Convert into a object, using the string in memory
$json_object = ConvertFrom-Json -InputObject $json_string
# or read from the file on disk
# $json_object = Get-Content -Path args.json -Raw | ConvertFrom-JSON

# Iterate around the properties of the object and set the variable name and value
$json_object.PSObject.Properties | ForEach-Object {

    # Create the variable
    New-Variable -Name $_.Name -Value $_.Value
}

Write-Output $USERNAME

Conclusion

Using this method has meant that I am able to add new variables to my script and easily update the ARM template to supply those values. Gone are the days that I have to work out where the ending escaped quote is when it is all on one very long line. There are some drawbacks, such as the variables are now exposed outside of the script and in the case of Bash eval is being used and they are valid. However with this pattern I have been able to increase my productivity as I know where I can find out what was sent to the script and I do not loose time debugging concatenations in the ARM template.

Share Comments