User Defined Functions in ARM Templates

Some new functionality for ARM templates was announced at the recent Build conference, one of these was user-defined functions. What this lets you do is create re-useable functions that you can call inside your template. You’re still limited to using the built-in ARM functions inside your function, but you can use functions to help simplify your templates and reduce errors.

Let’s look at an example, in a lot of my templates I am feeding in a “prefix” string which I then use for naming my resources. The functions I use to create the name can get quite complex, for example for a network card it can look like this:

"name":"[concat(toLower(parameters('prefix')),'-',parameters('WebServerPrefix'),'-nic-',padLeft(copyIndex,2,0))]"

This will turn a prefix of “Client1” into “client1-web-nic-01”, it’s fairly complex and easy to make mistakes with Especially when I need to repeat this for my all my VMs. What I can do instead is create a function that makes this a bit simpler and less prone to error.

Creating Functions

Functions have been created as a new top-level resource in ARM templates, so they sit alongside parameters, variables, resource etc.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "functions": [],
    "resources": [],
    "outputs": {}
}

To avoid clashing with built-in functions we need to create our functions inside a namespace, you can create all your functions in a single namespace, or you can split them over multiple if you wish. Inside the namespace is a “members” object which contains all our functions.

 "functions": [ { "namespace": "lambdaToys", "members": { } } ],

Then we will create our function, each function has two sections, “parameters” for inputs, and “outputs” for processing the data and returning a result. Our parameters will be the prefix, the type of VM this nic will attach to, and the ID we want to use.

"functions": [
    {
        "namespace": "lambdaToys",
        "members": {
            "getNicName": {
                "parameters": [
                    {
                        "name": "prefix",
                        "type": "string"
                    },
                    {
                        "name": "vmType",
                        "type": "string"
                    },
                    {
                        "name": "id",
                        "type": "string"
                    }
                ],
                "output": {
                    "type": "string",
                    "value": "[concat(toLower(parameters('prefix')),'-',parameters('vmType'),'-nic-',padLeft(parameters('id'),2,'0'))]"
                }
            }
        }
    }
],

Now that this function has been created we can reference this in our NIC creation using the syntax “namespace.functionName”.

{
    "apiVersion": "2015-06-15",
    "type": "Microsoft.Network/networkInterfaces",
    "name": "[lambdaToys.getNicName(parameters('prefix'),'web','1')]",
    "location": "[resourceGroup().location]",
    "tags": {
        "displayName": "[lambdaToys.getNicName(parameters('prefix'),'web','1')]"
    },
    "dependsOn": [
        "Microsoft.Network/virtualNetworks/VirtualNetwork1"
    ],
    "properties": {
        "ipConfigurations": [
            {
                "name": "ipconfig1",
                "properties": {
                    "privateIPAllocationMethod": "Dynamic",
                    "subnet": {
                        "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', 'VirtualNetwork1'), '/subnets/',variables('Subnet1Name'))]"
                    }
                }
            }
        ]
    }
}

Once we go ahead and deploy that, we’ll get a NIC with the name defined by our function.

We can now use this function in exactly the same way, every time we want to create a NIC in this template. The full template for this example is available on Github here.

Limitations

There are some limits to what you can do with functions which you should bear in mind:

  • You can’t access variables in your function (although you can pass them in as parameters)
  • You can’s use the “reference” function inside your function
  • You can’t have default values in your function parameters

The main limitation though in my view, is that you are stuck with only being able to use existing built-in functions, inside your function. This means that functions are only really ever going to help you with cleaning up your code and facilitating re-use, they are not going to allow you to do anything truly innovative. I’m also yet to find a way to import functions from a shared location or file, which means you will need to declare your functions in every template, probably only a cut and paste task, but still, it would be really good to be able to just import them instead. If you need to make changes to a function you are going to have to do it in every template you use them in.