Do More With ARM Templates using Functions

If you are writing ARM templates to deploy your Azure Infrastructure, then it's more than likely you are utilising some of the functions provided by the ARM language. Functions allow you to perform simple operations inside your template to transform or creation values that you use in your deployment. Some of the most common ones you'll see include:

  • Concat - for joining strings, regularly used to join parameters, variables and constants together to form resource names, app settings, connection strings and so on.
  • ResourceID - to obtain a full resource ID from a resources name
  • Add, Sub, Mul and Div - to perform numerical operations

The full list of functions available in the ARM language can be found here. For the rest of this article, we are going to take a look at some of the less well known, but handy functions that might help you improve or simplify your templates.

I'd love to hear if you are doing anything new or unique with ARM functions, so please leave a comment.

String Functions

toLower & toUpper

These are relatively simple functions that convert a string to upper or lower case, but they can be handy we need to conform to some of the naming conventions in Azure. For example, storage account names can only be lowercase so we can use toLower to ensure that regardless of what is passed into the template, the name is always lower case.

{
    "type": "Microsoft.Storage/storageAccounts",
    "name": "[toLower(parameters('storageAccountName'))]",
    "location": "[parameters('location')]",
    "apiVersion": "2018-07-01",
    "sku": {
        "name": "[parameters('storageAccountType')]"
    },
    "kind": "StorageV2",
    "properties": {}
}

UniqueString

Still on the topic of names, some resources in Azure require you to have globally unique names, such as storage accounts. It can be a pain to ensure your name is unique, especially if you're trying to automate your deployments. The UniqueString function helps you to generate a unique name.

The UniqueString function isn't random, but it generates a name based on a hash function. You can pass this function any string to influence the uniqueness. For example, it is common to use the name or ID of the subscription, resource group or deployment which will create a string which is unique for those parameters. The hash is deterministic, so assuming you pass the same values to the function you will always get the same result back, which is excellent if you need to reference the string later.

In the example below, we are going to create a name for the storage account that is unique for the subscription.

{
    "type": "Microsoft.Storage/storageAccounts",
    "name": "[concat(parameters('storageAccountName'),uniqueString(subscription().subscriptionId))]",
    "location": "[parameters('location')]",
    "apiVersion": "2018-07-01",
    "sku": {
        "name": "[parameters('storageAccountType')]"
    },
    "kind": "StorageV2",
    "properties": {}
}

PadLeft

If your looking to ensure a standard naming convention or name length for your resources then PadLeft can be helpful to ensure that your numbering suffixes always have the same amount of digits, the PadLeft function accepts a string to pad, the desired length for the string, and the character to use for the padding.

In the example below, we want to ensure the network cards we are creating always have identifiers that are 3 digits long and using 0 for padding.

    {
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[concat('nic-', padLeft(copyindex(),3,'0'))]",
        "apiVersion": "2016-03-30",
        "location": "[parameters('location')]",
        "copy": {
            "name": "nicLoop",
            "count": "[parameters('numberOfInstances')]"
        },
        "dependsOn": [
            "[variables('virtualNetworkName')]"
        ],
        "properties": {
            "ipConfigurations": [
                {
                    "name": "ipconfig1",
                    "properties": {
                        "privateIPAllocationMethod": "Dynamic",
                        "subnet": {
                            "id": "[variables('subnet1Ref')]"
                        }
                    }
                }
            ]
        }
    }

Numeric Functions

CopyIndex

You may already have used CopyIndex in resources where you are using a loop. It returns the current iteration of the loop. What you may not know is that you can pass an additional offset to the function to add to the Index. This is useful because CopyIndex loops are 0 based if you are using this add a suffix to a resource name you will often want this to start at 1. We can do this by adding an offset:

copyindex(1)

If we amend our NIC example above to start number at 1, it looks like this:

    {
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[concat('nic-', padLeft(copyindex(1),3,'0'))]",
        "apiVersion": "2016-03-30",
        "location": "[parameters('location')]",
        "copy": {
            "name": "nicLoop",
            "count": "[parameters('numberOfInstances')]"
        },
        "dependsOn": [
            "[variables('virtualNetworkName')]"
        ],
        "properties": {
            "ipConfigurations": [
                {
                    "name": "ipconfig1",
                    "properties": {
                        "privateIPAllocationMethod": "Dynamic",
                        "subnet": {
                            "id": "[variables('subnet1Ref')]"
                        }
                    }
                }
            ]
        }
    }

Resource Functions

List*

List* can be used to list the details of any resource that supports the list function. The most common of these are:

  • ListKeys - To list the keys for resources like storage accounts and service bus
  • ListSecrets - To list secrets from a resource, such as functions trigger URLs
  • listAccountSas - List SaS tokens for a storage account

The list* function takes the name of the resource you want to list values from, and the API version of the resource, i.e.

 listKeys(variables('storageAccountid'),'2015-05-01-preview')

We can then further filter the list to get specific values.

( listKeys(variables('storageAccountid'),'2015-05-01-preview')).key1

Using the List* function can be very useful to pass values from on object to another. In the example below, we are taking the storage key from a storage account and passing it to a function to be stored as an app setting.

{
    "apiVersion": "2015-08-01",
    "type": "Microsoft.Web/sites",
    "name": "[variables('functionAppName')]",
    "location": "[resourceGroup().location]",
    "kind": "functionapp",            
    "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    ],
    "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
        "siteConfig": {
            "appSettings": [
                {
                    "name": "AzureWebJobsDashboard",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
                },
                {
                    "name": "AzureWebJobsStorage",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
                },
                {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
                },
                {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('functionAppName'))]"
                },
                {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~1"
                }
            ]
        }
    }
}

ResourceGroup

The ResourceGroup allows you to retrieve the object for the ResourceGroup you are currently deploying to and use its values in the template. The best use for this is to get the location of the resource group. This allows you to deploy resources to the same location as the resource group, rather than having to pass in a location to the template.

   {
      "apiVersion": "2016-08-01",
      "type": "Microsoft.Web/sites",
      "name": "[parameters('siteName')]",
      "location": "[resourceGroup().location]",
      ...
   }

ResourceID

Some resources will require you to provide references to other resources using the full resource ID rather than just the name. For example, if you want to assign a public IP to a network card you need to find the resource ID for the public IP in this format:

/subscriptions/<subscriptionID>/resourceGroups/<resourceGroupName>/providers/Microsoft.Network/publicIPAddresses/<PIPName>

You can work this out manually, but the easier way is to use the ResourceID function which will generate this for you. If the resource is in the same subscription and resource group as the deployment then all you need to provide is the resource type and the resource name:

"[resourceId('Microsoft.Network/publicIPAddresses','PIPName')]"

If the resource you're looking for is in a different resource group or subscription then you can also supply this to the function:

"[resourceId('subscriptionId','resourgoupName','Microsoft.Network/publicIPAddresses','PIPName')]"

The example below shows using resourceID on a NIC for both the Public IP and the Subnet.

{
    "apiVersion": "2015-06-15",
    "type": "Microsoft.Network/networkInterfaces",
    "name": "[variables('nicName')]",
    "location": "[parameters('location')]",
    "dependsOn": [
      "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]",
      "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
    ],
    "properties": {
      "ipConfigurations": [
        {
          "name": "ipconfig1",
          "properties": {
            "publicIPAddress": {
              "id": "[resourceId ('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
            },
            "privateIPAllocationMethod": "Dynamic",
            "subnet": {
              "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('subnetName'))]"
            }
          }
        }
      ]
    }
  }

Deployment Functions

Deployment

The Deployment function allows you to retrieve details of the current deployment. The object returned contains all the details such as resources, parameters, variables etc. There are a few uses for this:

  • Returning parameter or variable values as part of the template output
  • Getting the URL of the deployment template, you can then use this to reference other templates stored in the same location, for example, if you are using nested templates.
  • Getting the deployment mode of the template (incremental or full), which you could use to do different steps based on the deployment type

The example below will output all the parameters sent to the template as part of the output (except secrets):

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "resources": [],
    "outputs": {
        "subscriptionOutput": {
            "value": "[deployment().properties.template.parameters]",
            "type" : "object"
        }
    }
}

Logical Functions

If

The If function allows you to use conditional logic in your resources to control how things are deployed. I wrote a full article on how to use If within your templates here. The If function accepts a condition to look for (which uses one of the Boolean functions) and then what to do if this is true or false. A simple example of using If is determining whether to attach a public IP to a network card based on a parameter. We check to see if a parameter called "NetworkInterfaceType" is set to "public" or "private" and add a Public IP (or not) based on this:

[if(equals(parameters('NetworkInterfaceType'),'Public'), variables('publicIP1'), json('null'))]

json('null') is another function which returns the JSON version of null.

The full example can be seen below:

    {
           "apiVersion": "2017-04-01",
           "type": "Microsoft.Network/networkInterfaces",
           "name": "[variables('NICName')]",
           "location": "[resourceGroup().location]",
           "tags": {
               "displayName": "parameters('NICName')"
           },
           "dependsOn": [
               "[concat('Microsoft.Network/publicIPAddresses/', variables('NICName'),'-pip')]"
           ],
           "properties": {
               "ipConfigurations": [
                   {
                       "name": "ipconfig1",
                       "properties": {
                           "privateIPAllocationMethod": "[parameters('IPAllocationMethod')]",
                           "subnet": {
                               "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', variables('NetworkName')), '/subnets/',variables('Subnet1Name'))]"
                           },
                           "publicIPAddress": "[if(equals(parameters('NetworkInterfaceType'),'Public'), variables('publicIP1'), json('null'))]"
                       }
                   }
               ]
           }
       }