Dynamic ARM Templates with Inline Logic Operators

A while back I wrote an article talking about the new “Condition” option in Azure Resource Manger (ARM) templates. This was the first step into conditional logic in ARM templates and worked great where you needed to apply a  condition at the resource level. Where it fell down was where you needed a condition inside a resource, this resulted in you having to duplicate objects with different settings and work around issues like duplicate naming. In our example we looked at whether or not a network card should have a public IP. The condition worked great for determining whether or not to create the Public IP (PIP) object, but when it came to deciding whether or not to assign the PIP to the network card we ended up having to create two network card objects, one with one without, and use the condition to select which one to use, a bit painful!

I’m happy to say a new update to the ARM template specification adds more options for conditional logic in your ARM templates. In particular, alongside conditions we now have IF statements which you can use inline in your code to conditionally make choices at run time, which can be really powerful. The syntax of this is pretty straightforward:

[if(condition, true value, false value)]

These if statements can be applied to parameters, variables and most importantly resource properties.

At the present time these new features haven’t rolled out to all regions, but will do over the next few days. If you want to try it out it is definitely in the West Central US region.

Example

Let’s look at an example, if we take our previous template for deploying a network card and deciding whether to use a public IP, we can now use an IF statement in our network card properties section to decide whether to try and assign the public IP to the VM:

"publicIPAddress": "[if(equals(parameters('NetworkInterfaceType'),'Public'),variables('publicIP1'),'-pip')), json('null'))]"

Were still doing using the PublicIPAddress property, but instead of just providing the resource ID for the Public IP resource, we are wrapping it in an if statement. Were check whether our “NetworkInterfaceType” is equal to “Public” (and you can use any of the other comparison functions here). If it is set to “public” then we tell it to add the public IP, if not we set it to Null. Because the public IP object is fairly complex, we are not including it inline, instead we reference a variable that contains it, this is what the variable looks like:

"publicIP1": {
    "id": "[resourceId('Microsoft.Network/publicIPAddresses',Concat(variables('NICName'),'-pip'))]"
}

Note that the name of this variable does not need to be “publicIPAddress”, all we are defining here is a variable to hold the public IP configuration that we will substitute in later. If the true condition of our if statement was a simple string we could have included it inline, like:

"[if(equals('a', 'a'), 'yes', 'no')]"

So now, we can throw away or duplicated network card and have a single network card with an inline condition. At the same time, we are still making use of the condition statement in our PIP resource to determine whether that resource exists at all. Here is the full template that creates the PIP (or not) and the NIC, as you can see it is much simpler and easier to understand:

{
   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
   "contentVersion": "1.0.0.0",
   "parameters": {
       "NetworkInterfaceType": {
           "type": "string",
           "metadata": {
               "description": "Whether to have a public or private NIC"
           },
           "allowedValues": [
               "Public",
               "Private"
           ]
       },
       "IPAllocationMethod": {
           "type": "string",
           "defaultValue": "Dynamic",
           "allowedValues": [
               "Dynamic",
               "Static"
           ]
       },
       "PIPCount": {
           "type": "int",
           "defaultValue": 1
       }
   },
   "variables": {
       "NetworkName": "lambda-vnet",
       "Subnet1Name": "lambda-subnet1",
       "Subnet2Name": "lambda-subnet2",
       "NICName": "lambdanic1",
       "PublicIPName": "[concat(variables('NICName'),'-pip')]",
       "publicIP1": {
           "id": "[resourceId('Microsoft.Network/publicIPAddresses',Concat(variables('NICName'),'-pip'))]"
       }
   },
   "resources": [
       {
           "apiVersion": "2015-06-15",
           "type": "Microsoft.Network/virtualNetworks",
           "name": "[variables('NetworkName')]",
           "location": "[resourceGroup().location]",
           "tags": {
               "displayName": "[variables('NetworkName')]"
           },
           "properties": {
               "addressSpace": {
                   "addressPrefixes": [
                       "10.0.0.0/16"
                   ]
               },
               "subnets": [
                   {
                       "name": "[variables('Subnet1Name')]",
                       "properties": {
                           "addressPrefix": "10.0.0.0/24"
                       }
                   },
                   {
                       "name": "[variables('Subnet2Name')]",
                       "properties": {
                           "addressPrefix": "10.0.1.0/24"
                       }
                   }
               ]
           }
       },
       {
           "apiVersion": "2017-04-01",
           "condition": "[equals(parameters('NetworkInterfaceType'),'Public')]",
           "type": "Microsoft.Network/publicIPAddresses",
           "name": "[variables('PublicIPName')]",
           "location": "[resourceGroup().location]",
           "tags": {
               "displayName": "[Concat(variables('NICName'),'-pip')]"
           },
           "properties": {
               "publicIPAllocationMethod": "[parameters('IPAllocationMethod')]",
               "dnsSettings": {
                   "domainNameLabel": "[Concat(variables('NICName'),'-pip')]"
               }
           }
       },
       {
           "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'))]"
                       }
                   }
               ]
           }
       }
   ],
   "outputs": {}
}

The full templates for this example are also on Github

Other Logical Operators

In addition to IF statements, logic operators for AND, OR and NOT have been added to the language specification

[and(value1, value2)] [or(value1value2)] [not(value1)]

Combining these with Conditions or IF statements we can use multiple criteria for defining whether something is deployed or not. If we take the Network card example, if we introduce a new parameter that indicates the number of public IP’s to create, and only assign a public IP to a NIC if NetworkInterfaceType is set to “Public” and PIPCount is greater than 0:

"publicIPAddress": "[if(and(equals(parameters('NetworkInterfaceType'),'Public'),greater(parameters('PIPCount'),0)), resourceId('Microsoft.Network/publicIPAddresses',Concat(variables('NICName'),'-pip')), json('null'))]"

This works, but it can be a bit confusing to read, so what you might want to do is split your condition out into a variable and then use that in your resource, e.g.:

"variables": { ... "requirePublicIP": "[and(equals(parameters('NetworkInterfaceType'),'Public'),greater(parameters('PIPCount'),0))]",
    "publicIP1": {
        "id": "[resourceId('Microsoft.Network/publicIPAddresses',Concat(variables('NICName'),'-pip'))]"
    }
},

Finally, we also now have the bool() function which can transform a parameter into a boolean, useful to use in our logical operators.

 "trueString": { "value": "[bool('true')]", "type" : "bool" },

Summary

This new set of language features is a great addition to the toolbox for building more dynamic and re-usable templates. It’s not perfect, if your using IFs with complex JSON objects you end up having to store this as variables and end up growing that out very quickly, especially if you want to use an IF statement to decide between two different complex objects. Additionally, if you want to do something like if, if then, else, you end up having to next multiple IFs which can get pretty complicated to read. All of these compromises however, are still better than having to duplicate whole objects like we did before and we now have a pretty powerful set of tools to be able to control what we deploy dynamically at run time, and most importantly help us make scripts that are generic enough to share around.

It should be noted that If statements don’t do away with the need for Conditions, but more likely you will use these two in combination like we have here. Where you need to determine whether an entire resource is deployed or not, use conditions, where you need to alter inline elements use an if statement.

Further Reading

Logical functions for Azure Resource Manager templates

Example Code on Github