DEV Community

Olivier Miossec
Olivier Miossec

Posted on • Updated on

Infra as Code in Azure, User-defined functions in ARM Templates

Often, when defining an ARM template, you have to apply the same transformation over and over across resources. Take for example resource naming, in a template you may have to use the Concat() function to create the name of the resource. Even if, it only takes a few parameters, it adds complexity and it's often error prone. It’s the same when you need to make complexes calculation in resource sections using complexes comparison and logical operator. At the end it can be a complete nightmare when you have to debug it.

Many times, these complexes manipulations are needed more than once in a template. Updating them can sometimes difficult. Often, At the end it can be a complete nightmare when you have to debug it . If we can centralize these manipulations in one place and call them when we need it will be a great feature.

This feature exists, user-defined functions. It was introduced in 2018 and it’s one of the sections of a template along with Resources or Variables.

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

A function needs to belong to a namespace. It prevents confusion and helps to identify the function.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [],
    "outputs": {
        "FName" : {
            "type": "string",
            "value": "[GetMy.firstName()]"
        },
        "LName" : {
            "type": "string",
            "value": "[GetMy.lastName()]"
        }
    },
    "functions": [
           {
                "namespace": "GetMy",
                "members": {
                "firstName": {
                    "parameters": [
                    ],
                    "output": {
                    "type": "string",
                    "value": "Olivier"
                    }
                }, 
                "lastName": {
                    "parameters": [
                    ],
                    "output": {
                    "type": "string",
                    "value": "Miossec"
                    }
                }
                }
            }
    ]
}
Enter fullscreen mode Exit fullscreen mode

As you see, a namespace can contain several functions and you can have several namespaces in a ARM template file.
The function definition starts with the name of the function. It must be unique within the namespace, but it’s a good practice to make it unique for the whole template to avoid confusion.

Each function has 2 sections, parameters and output.
The parameters sections let you define the input of the function. The function can only use these parameters. A function cannot access variables or template parameters, the only possible inputs are the parameters.
This section is a JSON array. It can contain 0 to many parameters. A parameter contains a type, the JSON data type expected, string, int, bool, object, array, securestring and secureObject and the name of the parameter.
Remember, parameters are the only way to pass data to the function. There is no way to access the template's variables or other value defined in the template.
The parameter order here defines the order in the function call.

The output contains two elements, the data type, like in the parameters section, it must be a valid JSON data type and the value.

The value can hardcode, like in the previous example (but there is no real value to do that) or it can be a more complexes ARM template expression.
There is some limitation here, the ARM reference function is not available, you can not use the list functions or another user-defined function.

You can use parameters in the same way you use it in the template.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [],
    "outputs": {
            "Name" : {
            "type": "string",
            "value": "[GetMy.lastName('Olivier', 'Miossec')]"
        }
    },
    "functions": [
           {
                "namespace": "GetMy",
                "members": {
                "lastName": {
                    "parameters": [
                            {
                                "name": "FistName",
                                "type": "string"
                            },
                            {
                                "name": "LastName",
                                "type": "string"
                            }
                        ],
                        "output": {
                        "type": "string",
                        "value": "[concat(parameters('FistName'), ' ', parameters('LastName'))]"
                        }
                    }      
                }
            }
    ]
}
Enter fullscreen mode Exit fullscreen mode

When to use user-defined functions?
Sometimes you need to create complexes calculations. Imagine a situation where you need to deploy multiple VM from a template. All resources must comply with the naming convention. VM, Disk and nic name must be built with a prefix, a suffix representing the resource name (like -vm) and an increment. Finally, we want all resource names to use lowercase.

When to use user-defined functions? Sometimes you need to create complexes calculations. For example, imagine a situation where you need to deploy multiple VM from a template. All resources must comply with the naming convention. VM, Disk and nic name must be built with a prefix, a suffix representing the resource name (like -vm) and an increment. Finally, and we want all resource names to use lowercase.

Resource Name must look like.
prefix-0X-(vm|osvhd|vhd|nic)

Without user-defined function, you will have to use a Concat() and toLower() functions not only for resource names but also for DependsOn. It will be difficult to update and debug in this case.

But what if we have more than 9 VM to deploy? We need to test the increment if the value is greater than 9 we need to remove the 0.
Let’s take a look at the function.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [],
    "outputs": {
            "Name" : {
            "type": "string",
            "value": "[resourceName.GetName('appli', 12,'vhd')]"
        }
    },
    "functions": [
                   {
                "namespace": "resourceName",
                "members": {
                "GetName": {
                    "parameters": [
                            {
                                "name": "prefix",
                                "type": "string"
                            },
                            {
                                "name": "increment",
                                "type": "int"
                            },
                            {
                                "name": "resourceType",
                                "type": "string"
                            }
                        ],
                        "output": {
                        "type": "string",
                        "value": "[toLower(concat(parameters('prefix'), '-', if( greater(parameters('increment'), 9 ) ,'','0') ,parameters('increment'),'-', parameters('resourceType')))]"
                        }
                    }      
                }
            }
    ]
}
Enter fullscreen mode Exit fullscreen mode

To deal with the 0 when the increment is greater than 9 we need to use the IF logical function with the Greater() comparison function.

The complete template looks like this.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "VMNumber": {
            "type": "int",
            "defaultValue": 3,
            "metadata": {
                "description": "Nunber of VM to deploy"
              }
        }, 
        "Prefix": {
            "type": "string",
            "metadata": {
                "description": "The prefix used to deploy VM"
              }
        },
        "adminUsername": {
            "type": "string",
            "defaultValue": "labadmin",
            "metadata": {
                "description": "VM Administrator Username"
              }
        },
        "adminPassword": {
            "type": "securestring",
            "metadata": {
                "description": "VM Administrator password"
              }
        }
    },
    "variables": {
        "imagePublisher": "MicrosoftWindowsServer",
        "imageOffer": "WindowsServer",
        "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', 'testvnet')]",
        "subnetRef": "[concat(variables('vnetID'),'/subnets/', 'vmapp')]",
        "location": "[resourceGroup().location]",
        "osSku": "2019-Datacenter-smalldisk",
        "vmSize": "Standard_B2s"
    },
    "resources": [
       {
        "apiVersion": "2018-02-01",
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[resourceName.GetName(parameters('Prefix'), copyIndex('NicCopy', 1),'nic')]",  
        "location": "[variables('location')]",
        "properties": {
          "ipConfigurations": [
            {
              "name": "ipconfig1",
              "properties": {
                "privateIPAllocationMethod": "Dynamic",
                "subnet": {
                  "id": "[variables('subnetRef')]"
                }
              }
            }
          ]
        },
        "copy": {
            "name": "NicCopy",
            "count": "[parameters('VMNumber')]"
        }
      },
     {
        "apiVersion": "2018-06-01",
        "type": "Microsoft.Compute/virtualMachines",
        "name": "[resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'vm')]", 
        "location": "[variables('location')]",
        "dependsOn": [
          "[concat('Microsoft.Network/networkInterfaces/', resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'nic'))]"
        ],
        "properties": {
          "hardwareProfile": {
            "vmSize": "[variables('vmSize')]"
          },
          "osProfile": {
            "computername": "[resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'vm')]", 
            "adminUsername": "[parameters('adminUsername')]",
            "adminPassword": "[parameters('adminPassword')]"
          },
          "licenseType": "Windows_Server",
          "storageProfile": {
            "osDisk": {
              "osType": "Windows",
              "name": "[resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'osvhd')]", 
              "caching": "ReadWrite",
              "createOption": "FromImage",

              "managedDisk": {
                "storageAccountType": "Premium_LRS"
              }

            },
          "dataDisks": [
            {
              "name": "[resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'vhd')]", 
              "createOption": "Empty",
              "caching": "None",
              "diskSizeGB": 10,
              "lun": 0,
              "managedDisk": {
                "storageAccountType": "Premium_LRS"
              }
            }
          ],
          "imageReference": {
            "publisher": "[variables('imagePublisher')]",
            "offer": "[variables('imageOffer')]",
            "sku": "[variables('osSku')]",
            "version": "latest"
          }
        },
        "networkProfile": {
            "networkInterfaces": [
              {
                "id": "[resourceId('Microsoft.Network/networkInterfaces', resourceName.GetName(parameters('Prefix'), copyIndex('VmCopy',1),'nic'))]"

              }
            ]
          }
        },
        "copy": {
            "name": "VmCopy",
            "count": "[parameters('VMNumber')]"
        }
      }
    ],
    "outputs": {},
     "functions": [
                   {
                "namespace": "resourceName",
                "members": {
                "GetName": {
                    "parameters": [
                            {
                                "name": "prefix",
                                "type": "string"
                            },
                            {
                                "name": "increment",
                                "type": "int"
                            },
                            {
                                "name": "resourceType",
                                "type": "string"
                            }
                        ],
                        "output": {
                        "type": "string",
                        "value": "[toLower(concat(parameters('prefix'), '-', if( greater(parameters('increment'), 9 ) ,'','0') ,parameters('increment'),'-', parameters('resourceType')))]"
                        }
                    }      
                }
            }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Note that by using Copy to create resources. But by default, the copyIndex() starts by 0. To avoid naming resources with a 00 we need to use an offset.

copyIndex('VmCopy',1)
Enter fullscreen mode Exit fullscreen mode

There are some limitations when you use User Defined Functions in an ARM template. But they can save you time with complexes templates. They take complexity from the resources section and they are reusable.

Oldest comments (0)