Functions in ARM Templates

To help build templates quicker, make them more expressive and reusable, there are many built-in functions at your disposal. And even if there isn’t a built-in function for your specific scenario, you can always write one on your own. The use of functions introduces pieces of logic into your templates, which can be used from expressions.

Manning MEAP This blog is an excerpt from the book Azure Infrastructure as Code by Henry Been, Eduard Keilholz, and Erwin Staal. This section is from Chapter 3 which explores ARM Templates and covers, besides functions, parameters, variables, outputs, and resouce declaration. Take 40% off Azure Infrastructure as Code by entering ‘fccbeen’ into the discount code box at checkout at manning.com. https://www.manning.com/books/azure-infrastructure-as-code

Expressions

Using functions, either the built-in ones or user-defined, you can create expressions to extend the default JSON language in ARM templates. Expressions start with [, end with ] and can return a string, int, bool, array, or object. You’ve already seen quite a few examples throughout the previous code snippets, but here is another example that deploys a storage account.

"resources": [
    {
        "type": "Microsoft.Storage/storageAccounts",
        "name": "[parameters('storageName')]",
		…
        "location": "[resourceGroup().location]"
    }
]

Just as with JavaScript, function calls are formatted as functionName(arg1,arg2,arg3). In this example, the function parameters() is used with storageName as the argument to retrieve the value for that parameter. As you can see, passing in string values is done using single quotes. In the location property, you see another expression. Here, the resourceGroup() functions returns an object, and you can use the dot notation to get the value of any of its properties.

Sometimes you find yourself in a situation that you want to assign a null value to a property based on a condition and nothing otherwise. Assigning a null makes sure that the Resource Manager ignores the property while deploying your template. Using the JSON function, you can assign null values. Consider the following example in which you create a virtual network subnet that may or may not get a routing table assigned, depending on whether it has been specified in a parameter.

{
    "$schema": "https://schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "subnet": {
            "type": "object"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Network/virtualNetworks/subnets",
            "location": "West Europe",
            "apiVersion": "2020-05-01",
            "name": "[parameters('subnet').name]",
            "properties": {
                "addressPrefix": "[parameters('subnet').addressPrefix]",
                "routeTable": "[if(contains(parameters('subnet'), 'routeTable'),
                     json(concat('{\"id\": \"',
                         resourceId('Microsoft.Network/routeTables',
                             parameters('subnet').routeTable.name), '\"}')),
                     json('null'))]"
            }
        }
    ]
}

The first thing to notice in the expression on the route table property is the contains() function invocation on an object. This shows that you can use the contains function to check whether an object has a particular property defined or not. In this example, if the routeTable property exists, the json() function is used to create the object and assign it. If it does not exists, json(‘null’) is used to ignore it. Instead of using the json function to create the object in place, you could also create a variable to hold it and reference that as shown in the following example.

{
    "$schema": "https://schema.management.azure.com/schemas/
          2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "subnet": {
            "type": "object"
        }
    },
    "variables": {
        "routeTable": {
            "id": "[resourceId('Microsoft.Network/routeTables',
             parameters('subnet').routeTable.name)]"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Network/virtualNetworks/subnets",
            "location": "West Europe",
            "apiVersion": "2020-05-01",
            "name": "[parameters('subnet').name]",
            "properties": {
                "addressPrefix": "[parameters('subnet').addressPrefix]",
                "routeTable": "[if(contains(parameters('subnet'), 
                 'routeTable'), variables('routeTable'), json('null'))]"
            }
        }
    ]
}

Here you see the route table defined as a variable and used in the routeTable property in the properties object using the variables() function. The benefit of that is that your template is more readable, especially when the object you create in the json function gets larger.

As you saw, the syntax to assign null to an object is json(‘null’). You can use json('[]') when you need to do that same for an array type of just null on a string type.

Built-in functions

The list of built-in functions is quite long and is available at https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions. This book does not cover all of them but shows you the most frequently used functions. Functions are grouped into the following groups:

  • Reference functions
  • Logical functions
  • Array functions
  • Comparison functions
  • Date functions
  • Deployment value functions
  • Numeric functions
  • Object functions
  • Resource functions
  • String functions

The following sections explore reference and logic functions in detail. The other types of functions are less concerned with understanding how to write an ARM template and more with finding values for specific properties. You can look the workings of these functions up in the Microsoft documentation.

Reference functions

First up are reference functions. They allow you to get information on existing resources in Azure. Let’s start with an example that deploys an Azure KeyVault.

"resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2019-09-01",
      "name": "[parameters('keyVaultName')]",
      "location": "[resourceGroup().location]",
      "properties": {
          "sku": {
              "family": "A",
              "name": "standard"
          },
          "accessPolicies": "[parameters('accessPolicies').list]",
          "tenantId": "[subscription().tenantId]",
          "enabledForTemplateDeployment": true
        }
    }
]

This example uses two reference functions. First, it uses the resourceGroup() function to get information on the resource group that the template is deployed into. One of the returned properties is its location. Second, you see the usage of the subscription() function. That function returns information on the subscription you are deploying to, and one of its properties is the tenantId. That is the ID of the Active Directory Tenant used for authenticating requests to the KeyVault.

Now with the KeyVault in place, it is time to store secrets in it. Storing secrets is possible using the Azure Portal, but this is a book on ARM templates – so let’s practice that instead. In the coming examples, you go through the following steps:

  1. Create an Azure Storage Account
  2. Store the Management Key for that Storage Account in Azure KeyVault Let’s start by creating the storage account itself.
"resources": [
    {
        "type": "Microsoft.Storage/storageAccounts",
        "apiVersion": "2019-04-01",
        "name": "myStorageAccount",
        "location": "South Central US",
        "sku": {
            "name": "Standard_LRS"
        },
        "kind": "StorageV2"
    }
]

Before storing the key, you need to retrieve it using the listKeys() function. The following snippet does this.

listKeys(resourceId('Microsoft.Storage/storageAccounts',
     parameters('storageAccountName')), 2019-04-01').key1

The listKeys() functions accepts a reference to a resource as its first input. Here the resourceId() function is used to get that. The identifier returned has the following format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}

The second input parameter to listKeys is the API version of the resource for which you are getting the keys. Once the listKeys function returns, you can use the key1 or key2 property to get one of the access keys to the storage account. With a working snippet for fetching the storage account key, now use the following snippet to store that in the KeyVault.

"resources": [
  {
    "type": "Microsoft.KeyVault/vaults/secrets",
    "apiVersion": "2018-02-14",
    "name": "[concat(parameters('keyVaultName'), '/', parameters('secretName'))]",
    "location": "[parameters('location')]",
    "properties": {
      "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts',
         parameters('storageAccountName')), '2019-04-01').key1]"
    }
  }
]

This snippet adds a secret with the name secretName, as defined in the templates' name property. For its value, the key of the storage account is retrieved using the listKeys function and assigned to the value property in the template’s properties object. Another resource you’ve seen in the scenario for this chapter is Application Insights. Application Insights is a tool used to monitor the API. You can then find the APIs performance metrics, for example. You can also store custom metrics and do reporting on them. Since the API in our scenario processes orders, it makes sense to create a metric that holds each processed order’s value. The following snippet creates this resource.

"resources": [
    {
        "apiVersion": "2018-05-01-preview",
        "name": "[parameters('applicationInsightsName')]",
        "type": "Microsoft.Insights/components",
        "kind": "web",
        "location": "[resourceGroup().location]",
        "properties": {
            "Application_Type": "web"
        }
    }
]

Here you see the creation of an Application Insights resource that uses a parameter named applicationInsightsName for the resource’s name. To connect the API to that instance of Application Insights, you need its InstrumentationKey. One way to pass that to the API is to set an Application Setting on the Azure Web App that runs the API. The following sample shows how to do that.

"resources": [
    {
        "name": "[concat(parameters('webappname'), '/', 'appsettings')]",
        "type": "Microsoft.Web/sites/config",
        "apiVersion": "2018-02-01",
        "location": "[resourceGroup().location]",
        "properties": {
            "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(parameters(
                 'applicationInsightsName'), '2018-05-01-preview')
                 .InstrumentationKey]"
        }
    }
]

This snippet deploys a resource called appsettings within the Azure Web App. Within the properties, the actual configuration, a setting called APPINSIGHTS_INSTRUMENTATIONKEY is added. At runtime, this setting is used by the Application Insights SDK in the API to connect to the correct Application Insights instance. Important here is that you see that the setting gets its value by referencing the just created Application Insights resource using the reference() function, in a way similar to the resourceId() function. In contrast with the resourceId() function, the reference() function does not specify the entire resourceId, but only the name of the resource that is being referenced. This shorter and more readable notation is only usable when the resource you are referencing is defined within the same template. In all other cases, you must use the resourceId function. In both cases, the returned object gives you information on the Application Insights instance, and one of those properties is the InstrumentationKey.

Logical functions

Other often-used functions come from the group of logical functions. Imagine that you deploy virtual machines for your internal customers. Some customers might demand that you deploy those machines in an Availability Set to ensure high availability. You, however, only need to do that for their resources in the production environment, and not in other environments where the same templates are used. To reuse a single resource declaration that allows for both variations, you can use the following snippet.

"variables": {
    "deployAvailabilitySet": "[and(parameters('availabilitySet'), 
         equals(parameters('environment'), 'production'))]",
    "availabilitySet": {
        "id": "[resourceId('Microsoft.Compute/availabilitySets',
             parameters('availabilitySetName'))]"
    }
},
"resources": [
    {
        "name": "[parameters('vmName')]",
        "type": "Microsoft.Compute/virtualMachines",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-03-30",
        "properties": {
            "availabilitySet": "[if(variables('deployAvailabilitySet'),
                 variables('availabilitySet'), json('null'))]",
            ...
        }
    }
]

In this example, you see the equals() function’s usage inside another logical function, the and() function. The first input value to the and() function is a boolean parameter to check if the use of availability sets is required for this customer. The second input value is the outcome of the equals() function, which checks whether you are currently deploying to your production environment and also results a boolean as it’s result.

The outcome of the and() function is saved in the deployAvailabilitySet variable. This variable is used in the template while setting the availabilitySet property of the Virtual Machine. Only when this parameter’s value evaluates to true, the availability set defined in the variable is assigned. In all other cases, the json() function assigns null. ARM templates support most of the logic-functions that you would expect when coming from a programming or scripting background, including not(), and(), or(), equals(), if(), and many more.

Most of the built-in functions are usable at any of the scopes to which you can deploy templates: the tenant, management group, subscription, and resource group. You might not be familiar with these scopes. They will be introduced in the next chapter. For now, it’s enough to know that there are different scopes in Azure and that there exists a hierarchy between them. Where most functions can be deployed at any scope, there are exceptions and they are well documented. As an example, take the subscription() function that gives you information on the subscription you are deploying to. This function is not available when you deploy a template on the management group level, as there is no subscription involved. Subscriptions always reside within a management group and therefor at a lower level.

User-defined functions

Although the list of built-in functions is quite long, you may still find that you miss out on some functionality. In that case, a user-defined function can be what you need. Especially when you have complex expressions that you repeatedly reuse in a template, user-defined functions shine. In the following example, you see a user-defined function in the functions for capitalizing a string.

"functions": [
    {
     "namespace": "demofunction",
     "members": {
        "capitalize": {
            "parameters": [
                {
                    "name": "input",
                    "type": "string"
                }
            ],
            "output": {
                "type": "string",
                "value": "[concat(toUpper(first(parameters('input'))),
                     toLower(skip(parameters('input'), 1)))]"
                }
            }
        }
    }
]

The functions section is an array where you can define more than one custom function. The first property you specify is its namespace to make sure you don’t clash with any of the built-in functions. Then, in the members array, you define one or more functions. A function definition always starts with its name, in this case, capitalize. The parameters array is used for defining input parameters, precisely the same as parameters for a template. One limitation is that you cannot give parameters a default value. Within a user-defined function, you can only use the parameters defined in the function, not the template’s. The output section is where you define the actual logic of your function. You specify the output type and its value.

User-defined functions come with a few limitations. First, the functions cannot access variables, so you must use parameters to pass those values along. Functions cannot call other user-defined functions and cannot use the reference, resourceId, or list functions. A workaround for these two limitations is to use those functions in the parameters while using the function.

"resources": [
    {
        "type": "Microsoft.Sql/servers",
        "name": "[demofunction.capitalize(parameters('sqlServerName'))]",
        "apiVersion": "2019-06-01-preview",
        "location": "[parameters('location')]",
        "properties": {
            "administratorLogin": "[parameters('sqlServerUsername')]",
            "administratorLoginPassword": "[parameters('sqlServerPassword')]"
        }
    }
]

In the example above, the user-defined function is used to create a value for the SQL servers name property. The function is used as demofunction.capitalize() and the parameter sqlServerName is used as input. So, the format to use a user-defined function is .().

If you want to learn more about the book, you can check it out on Manning’s site: https://www.manning.com/books/azure-infrastructure-as-code