Using Loops and Conditions in ARM Templates

The most recent episode of ARM Template Masterclass covered the use of conditions and If statements in ARM templates. We looked at using this in a template that was deploying a network card and providing a parameter to indicate if the network card is public or private. If it is public, we create a Public IP and attach it to the NIC, if it is private we don’t create the public IP and don’t attach anything. This uses both types of conditional:

  • Conditions - this is used for the Public IP object, as it is a top-level object we can use a condition to determine whether we create it or not
  • If - the assignment of the Public IP to the NIC is a property, so we can’t use conditions, so instead we can use “If” to generate the right json for attaching the PIP or not

If you want to see how this works in detail, please check out the video here.

In the comments of this video, Dennis B asked a question about how you could combine this process with a loop, so creating X number of network cards, where X is provided by the user of the template, and specifying if these network cards should be public or private. Initially, I thought this should be reasonably straight forward, but it actually had some complexities that I thought it was worth writing about. So here is how I adapted the original template to work with a loop.

Adding Count Parameter

The first thing I had to do was add a new property that allowed the user to specify how many NICs they wanted. This is a very straight forward integer property.

"nic_count": {
    "type": "int",
    "metadata": {
        "description": "number of NIC's to create"
    },
    "defaultValue": 1
}

Adding Copy Property

This was the easy part. To be able to loop through as many times as the “nic_count” parameter defines, we need to add a copy property to both the Public IP and the Network Interface. We also need to use this property in the naming of the resources, and the DNS name of the PIP to ensure they are unique.

The Copy property looks like this:

"copy": {
    "name": "pipcopy",
    "count": "[parameters('nic_count')]"
},

This is for the public IP object, we created a similar one on the Network Interface object, with a different name.

To ensure the name was unique, we added the copy index to it. We also did a couple of things to format the name nicely. First, we provided “1” as a parameter to the copyIndex function, this starts the numbering at 1 rather than 0. Second, we used the “padLeft” function to add a leading 0 to our ID. So the name property of the Public IP looks like:

"name": "[concat(variables('PublicIPName'),padleft(copyIndex(1),2,'0'))]",

We made this change for the name of the NIC, and for the DNS name of the Public IP. See the full template example for more details.

At this point, the work on the Public IP is done. Now if the “NetworkInterfaceType” property is set to “Public”, then it will create as many public IPs as specified in the “nic_count” parameter, and if it is set to “Private”, it will create none.

Where this gets more complicated is on the Network Interface.

Public IP Address Property

For the network interface, we need to set the “PublicIPAddress” property to the ID of the public IP if the NIC is “Public” or to null if it is private. Before the changes, we used a variable configured to get the resource ID of the PIP in JSON format in the If statement.

The If statement looked like this:

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

and the “PublicIP1” variable looked like this:

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

This allowed us to define the JSON for the public IP as a variable and not have to deal with escaping the JSON inline, parsing the functions etc. However, this won’t work in this scenario. We have to use the count Index as part of the name, and you can’t reference count index in a variable.

So to make this work, we are going to have to generate the JSON inline using a few functions, and escaping the data where appropriate. This is what it looked like:

json(concat('{\"id\":', '\"',resourceId('Microsoft.Network/publicIPAddresses',Concat(variables('PublicIPName'),padleft(copyIndex(1),2,'0'))),'\"}'))

Let me just explain what is happening here:

  1. The Json() function converts this string into a JSON object
  2. We need to concatenate some escaped strings at the start and end to make this into an adequately formatted JSON string
  3. We use the resourceID function to generate the actual resource ID
  4. Finally, we do what we have done before and use concat to create a string of the PIP name using the prefix, and the countIndex, starting and 1 and padded left

All of this generates a JSON object that looks like this:

 "publicIP1": {
            "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/iftest/providers/Microsoft.Network/publicIPAddresses/lambdanic-pip01"
        }

Custom Function

While this worked, it was a bit messy. So to tidy this up and make it a bit more reusable, I decided to move this into a user-defined function. If your not familiar with user-defined functions, please have a read of this article.

By encapsulating our complex set of functions into this user-defined function, it simplifies the JSON used to create the actual resource and makes this function more reusable.

The custom functions section now looks like this:

"functions": [
    {
        "namespace": "samcogan",
        "members": {
            "get_nic_id": {
                "parameters": [
                    {
                        "name": "namePrefix",
                        "type": "string"
                    },
                    {
                        "name": "count",
                        "type": "int"
                    }
                ],
                "output": {
                    "type": "object",
                    "value": "[json(concat('{\"id\":', '\"',resourceId('Microsoft.Network/publicIPAddresses',Concat(parameters('namePrefix'),padleft(parameters('count'),2,'0'))),'\"}'))]"
                }
            }
        }
    }
],

This function takes two parameters, the prefix used for naming the resource, and the countIndex, and returns a JSON object for the PIP.

In our actual Network Interface object, the PublicIPAddress object now looks like this:

"publicIPAddress": "[if(equals(parameters('NetworkInterfaceType'),'Public'),samcogan.get_nic_id(variables('PublicIPName'),copyIndex(1)) , json('null'))]"

If “NetworkInterfaceType” is “Public” it uses the result of our user-defined function if it is “Private” it sets the value to Null.

The full code for this example is on Github here.