Create flexible ARM templates using conditions and logical functions

Udgivet den 7 september, 2017

Senior Program Manager, AzureCAT ARM

In this blog post, I will walk you through some of the new capabilities we have in our template language expressions for Azure Resource Manager templates.

Background

A common ask from customers is, “how can we use conditions in our ARM templates, so if a user selects parameter A, then resource A is created. If not, resource B should be created?”.  The only way you could achieve this, would be by using nested templates and have a mainTemplate to manipulate the deployment graph.

A common pattern can be seen below, where the user will select either new or existing.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "newOrExisting": {
            "type": "String",
            "allowedValues": [
                "new",
                "existing"
            ]
        }
    },
    "variables": {
        "templatelink": "[concat('https://raw.githubusercontent.com/krnese/ARM/master/', concat(parameters('newOrExisting'),'StorageAccount.json'))]"
    },
    "resources": [
        {
            "apiVersion": "2017-05-10",
            "name": "nestedTemplate",
            "type": "Microsoft.Resources/deployments",
            "properties": {
                "mode": "incremental",
                "templateLink": {
                    "uri": "[variables('templatelink')]",
                    "contentVersion": "1.0.0.0"
                },
                ...

In the variables declaration, the link to the template is constructed based on the parameter input.

The example shows that the URI to the template will either be ‘https://raw.githubusercontent.com/krnese/ARM/master/newStorageAccount.json’ or ‘https://raw.githubusercontent.com/krnese/ARM/master/existingStorageAccount.json’.  

This approach does work and you will have a successful deployment of the template, regardless whether the user selected new or existing in the parameter selection. However, the approach of having nested templates could indeed lead to many templates over time. Where some of the templates ended up being completely empty just to avoid having a failure in the deployment graph. Further, the complexity would grow as you would normally use other resource types – besides a storage account, and potentially have multiple conditions involved.

Another technique that also was frequently used, was to manipulate complex variables based on input parameters to determine certain properties for a resource. The example below shows how ARM could navigate within the complex variables to either create Linux or Windows virtual machines, based on the parameter platform, which allowed Windows or Linux as input.

        "osType": "[variables(concat('osType',parameters('platform')))]",        
        "osTypeWindows": {
            "imageOffer": "WindowsServer",
            "imageSku": "2016-Datacenter",
            "imagepublisher": "MicrosoftWindowsServer"
        },
        "osTypeLinux": {
            "imageOffer": "UbuntuServer",
            "imageSku": "12.04.5-LTS",
            "imagepublisher": "Canonical"
        },

Needless to say, we have heard from customers that this should be simplified, so that the template language should support and handle conditions more easily.

Introducing support for conditions

As we always appreciate the feedback from our customers, we are glad to remind you (announced at //BUILD this year) we have added support for conditions on resources, as well as many more capabilities. These include logical and comparison functions that can be used in the template language when handling conditions.

I will show a practical example of how the new capabilities can be leveraged in ARM templates. A typical example today, where you want your users to select whether a virtual machine should be Windows or Linux based, would require a complex variable being manipulated as demonstrated earlier in this blog post. In addition, if your user needs to decide if the virtual machine should go into production or not, by having an availability set as an optional resource would require nested templates that either deployed the resource or are empty.

In total, this would require at least 3 templates (remember that 2 of them would be nested templates).

By using conditions and functions, we can now accomplish this with a single template! Not to mention a lot less JSON. Let’s start by walking through some of the parameters we are using in the sample template, and explain the steps we have taken.

    "parameters": {
        "vmNamePrefix": {
            "type": "string",
            "defaultValue": "VM",
            "metadata": {
                "description": "Assign a prefix for the VM you will create."
            }
        },
        "production": {
            "type": "string",
            "allowedValues": [
                "Yes",
                "No"
            ],
            "metadata": {
                "description": "Select whether the VM should be in production or not."
            }
        },
        "platform": {
            "type": "string",
            "allowedValues": [
                "WinSrv",
                "Linux"
            ],
            "metadata": {
                "description": "Select the OS type to deploy."
            }
        },
        "pwdOrssh": {
            "type": "securestring",
            "metadata": {
                "description": "If Windows, specify the password for the OS username. If Linux, provide the SSH."
            }
        },

Besides assigning the virtual machine a prefix, we also have parameters for production and platform.

For production, the user can simply select yes or no. For yes, we want to ensure that the virtual machine being created gets associated with an availability set, since this resource needs to be in place prior to the virtual machine creation process. To support this, we have added the following resource to the template:

        {
            "condition": "[equals(parameters('production'), 'Yes')]",
            "type": "Microsoft.Compute/availabilitySets",
            "apiVersion": "2017-03-30",
            "name": "[variables('availabilitySetName')]",
            "location": "[resourceGroup().location]",
            "properties": {
                "platformFaultDomainCount": 2,
                "platformUpdateDomainCount": 3
            },
            "sku": {
                "name": "Aligned"
            }
        },

Note the condition property. We are using a comparison function, equals(arg1, arg2), which will check whether two values equal each other. In this case, if the parameter production equals yes, ARM will process this resource during runtime. If not true (No being selected), it will not be provisioned.

For the virtual machine resource in our template, we have declared the reference to the availability set based on the condition we introduced.

           "properties": {
                "availabilitySet": "[if(equals(parameters('production'), 'Yes'), variables('availabilitySetId'), json('null'))]",

We’re using if() which is one of our logical functions. This function takes three arguments. The first is the condition (Boolean), which is the value to check whether it is true or false. The second argument will be the true value, followed by the third argument which is false. The net result of this, would be to associate the virtual machine. When the user selects yes for the production parameter, then the virtual machine will get associated with the availability set declared in our template, which has already been created due to the condition. If the user selects no, the availability set won’t be created, hence there won’t be any association from the virtual machine resource.

We also have a parameter platform which decides if we are creating a Windows or Linux virtual machine. To simplify the language expression throughout the template, we added the values for Linux and Windows into our variables section.

        "windowsOffer": "WindowsServer",
        "windowsSku": "2016-Datacenter",
        "windowsPublisher": "MicrosoftWindowsServer",
        "linuxOffer": "UbuntuServer",
        "linuxSku": "12.04.5-LTS",
        "linuxPublisher": "Canonical",

On the virtual machine resource, more specifically within the storageProfile section where we distinguish between the image being used, we are referring to our variables for Windows or Linux.

"storageProfile": {
                    "imageReference": {
                        "publisher": "[if(equals(parameters('platform'), 'WinSrv'), variables('windowsPublisher'), variables('linuxPublisher'))]",
                        "offer": "[if(equals(parameters('platform'), 'WinSrv'), variables('windowsOffer'), variables('linuxOffer'))]",
                        "version": "latest",
                        "sku": "[if(equals(parameters('platform'), 'WinSrv'), variables('windowsSku'), variables('linuxSku'))]"
                    },

If a user selects WinSrv for the platform parameter, we will grab the values for the variables pointing to the Windows image. If not and Linux is selected, we refer to those variables instead. The result would either be a virtual machine using Windows Server 2016 or Ubuntu.

Last but not least, we also have our output section in the template, which will provide the user with some instructions based on their selection.

    "outputs": {
        "vmEndpoint": {
            "type": "string",
            "value": "[reference(concat(variables('pNicName'))).dnsSettings.fqdn]"
        },
        "platform": {
            "type": "string",
            "value": "[parameters('platform')]"
        },
        "connectionInfo": {
            "type": "string",
            "value": "[if(equals(parameters('platform'), 'WinSrv'), 'Use RDP to connect to the VM', 'Use SSH to connect to the VM')]"
        }
    }

If the user deploys a Windows Server, they will see the following output:

image

If the user deploys Linux, they will see this:

image

Summary

We believe, by introducing support for conditions on the resources you declare in your templates and enhancements to the template language itself with logical and comparison functions, we have set the scene for much simpler templates. You can now move away from the workarounds you previously had when implementing conditions, and should hopefully achieve much more flexibility while deploying complex apps, resources, and topologies in Azure.

The full template is available here.