Using Linked Templates and Conditional Logic in ARM

Microsoft have now added first class conditions to the language so this workaround is no longer required. See this article on how to use this feature.**

ARM Templates are a great tool for deploying complex sets of resources in Azure, however as it currently stands there is no concept of an “If” statement in a template. This can make it much more difficult to support the re-use of code and to avoid duplication in your templates, if you have to create a whole new set of templates which are 95% the same but with one section being different. Some resources do allow you to provide conditions through parameters, for example with a public IP it is possible to provide a value for privateIPAllocationMethod which can be either dynamic or static, this can be passed that in through a parameter at run time and all works fine. However, if I wanted to be able to pass a parameter that determined whether a network card got a public IP or not, then that’s not quite so easy.

There’s no simple solution to this, unless Microsoft decide to add conditional logic to the ARM language, but there is a way to make life a bit easier and add a sort of conditional logic to your templates, while at the same time support code re-use, through the use of linked templates.

Linked Templates

ARM templates support the use of linked templates, that is having one template call another. This is great for allowing you to modularise your templates and define commonly used templates in a separate file that can be used by multiple projects.

Using Linked Templates is a really good idea even if you don’t need the conditional logic we talk about later. It’s a great way to modularise your code and support re-use and debugging.

Lets take creating a network card as an example. You wouldn’t usually have a linked template just to create a NIC, but it’s a simple example.
The first thing we would do is create the linked template that will create the NIC, this is no different to how you would normally do this, except you create it in it’s own file just for the NIC creation. In this example we will create a NIC with only a private IP. You do need to remember that this script will be called by your top level script, not directly, so any parameters you need will need to come from the top level script. For organisation I am placing my linked templates in a sub-folder called linkedtemplates

linkedtemplates/CreateNicPrivate.json:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "NICName": {
            "type": "string",
            "metadata": {
                "description": "Name of the network card"
            },
            "virtualNetworkName": {
                "type": "string",
                "metadata": {
                    "description": "Name of Virtual Network for NIC"
                }
            },
            "subnetName": {
                "type": "string",
                "metadata": {
                    "description": "Name of subnet for NIC"
                }
            },
            "IPAllocationMethod": {
                "type": "string",
                "defaultValue": "Dynamic",
                "allowedValues": [
                    "Dynamic",
                    "Static"
                ]
            }
        }
    },
    "variables": {},
    "resources": [
        {
            "apiVersion": "2015-06-15",
            "type": "Microsoft.Network/networkInterfaces",
            "name": "[parameters('NICName')]",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "parameters('NICName')"
            },
            "dependsOn": [],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAllocationMethod": "[Parameters('IPAllocationMethod')]",
                            "subnet": {
                                "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '/subnets/',parameters('subnetName'))]"
                            }
                        }
                    }
                ]
            }
        }
    ],
    "outputs": {}
}

The linked template is pretty standard if your used to ARM templates. Where it does get a little different is in our top level template where we will call the linked template from. How this works is that we supply the top level template with a URL where it can find the top level template, and any parameters that it needs to pass to it. The top level template will then go to that URL, download the linked template and run it with those parameters. The first thing you need is to make sure you have a URL where you can store these files which will be accessible from where you plan to run the deployment, this could be an Azure storage account, Github, Dropbox, or anywhere else you can access a JSON file over a URL. If your templates contain sensitive information, or you don’t want anyone else getting to them, make sure you protect access to them. In this example we are using Azure Blob storage with a SAS token to protect it.

The top level template is defined just like you would any other template, but the magic part is the resource with type “Microsoft.Resources/deployments”, this is the resource to call other templates. In the example below we create a virtual network and subnets in the top level file, and then we call the createNIC template to create the NIC. Note we pass in the URL and SAS token for the linked template location as parameters.

DeployResources.json:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "artifactsLocation": {
            "type": "string",
            "metadata": {
                "description": "URL to location of linked templates"
            }
        },
        "artifactsLocationSasToken": {
            "type": "securestring",
            "metadata": {
                "description": "SAS Token to access linked templates"
            }
        }
    },
    "variables": {
        "NetworkName": "lambda-vnet",
        "Subnet1Name": "lambda-subnet1",
        "Subnet2Name": "lambda-subnet2"
    },
    "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"
                        }
                    }
                ]
            }
        },
        {
            "name": "CreateNIC",
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2015-01-01",
            "tags": {
                "displayName": "CreateNIC"
            },
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "[concat(parameters('artifactsLocation'), '/linkedTemplates/CreateNICPublic.json', parameters('artifactsLocationSasToken'))]",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "NICName": "LambdaNIC1",
                    "virtualNetworkName": "[variables('NetworkName')]",
                    "subnetName": "[variables('Subnet1Name')]",
                    "IPAllocationMethod": "Dynamic"
                }
            }
        }
    ],
    "outputs": {}
}

We’re now ready to run this deployment, we would call the top level file using New-AzureRmResourceGroupDeployment or your preferred method, passing the required parameters. The template will then take care of calling the linked template and passing parmaters to it. At the end of the deployment we end up with a virtual network, some subnets and a single network card.

Conditional Logic

OK, so we’ve got a linked template, but how does this help us with conditional logic? Because we are defining the URL for our linked template in our top level template, and this is just a string, we can alter this URL using parameters and so we can determine which linked template we call at run time.

To see this in action, let’s go back to the NIC example. My original linked template was for a NIC with only a Private IP but in some scenarios I am going to want to have a NIC with a public IP as well. So we create a second linked template called CreateNicPublic.json and we amend this to add a public IP and assign this to the NIC.

linkedtemplates/CreateNicPublic.json:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "NICName": {
            "type": "string",
            "metadata": {
                "description": "Name of the network card"
            },
            "virtualNetworkName": {
                "type": "string",
                "metadata": {
                    "description": "Name of Virtual Network for NIC"
                }
            },
            "subnetName": {
                "type": "string",
                "metadata": {
                    "description": "Name of subnet for NIC"
                }
            },
            "IPAllocationMethod": {
                "type": "string",
                "defaultValue": "Dynamic",
                "allowedValues": [
                    "Dynamic",
                    "Static"
                ]
            }
        }
    },
    "variables": {},
    "resources": [
        {
            "apiVersion": "2015-06-15",
            "type": "Microsoft.Network/publicIPAddresses",
            "name": "[Concat(parameters('NICName'),'-PIP')]",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "[Concat(parameters('NICName'),'-PIP')]"
            },
            "properties": {
                "publicIPAllocationMethod": "[parameters('IPAllocationMethod')]",
                "dnsSettings": {
                    "domainNameLabel": "[Concat(parameters('NICName'),'-PIP')]"
                }
            }
        } {
            "apiVersion": "2015-06-15",
            "type": "Microsoft.Network/networkInterfaces",
            "name": "[parameters('NICName')]",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "parameters('NICName')"
            },
            "dependsOn": [
                "[concat('Microsoft.Network/publicIPAddresses/', parameters('NICName'),'-PIP')]",
            ],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAllocationMethod": "[parameters('IPAllocationMethod')]",
                            "subnet": {
                                "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '/subnets/',parameters('subnetName'))]"
                            },
                            "publicIPAddress": {
                                "id": "[resourceId('Microsoft.Network/publicIPAddresses',vConcat(parameters('NICName'),'-PIP'))]"
                            },
                        }
                    }
                ]
            }
        }
    ],
    "outputs": {}
}

 

Now we’ve got our two linked templates, we need to set up the top level tempalte to allow us to choose which one to use in our deployments.

1. Add a parmeter

To allow us to select which file we use we will setup a parameter in the top level template. We will use this parameter to create the URL for the linked template, so in this simple method the values of this parameter must be the part of the name of the linked template that varies . So in this example my files are CreateNicPrivate.json and CreateNicPublic.json, so my paramter will accept only two values, Public or Private, set your “Allowed Values” section to make sure that only these options are allowed:

"NetworkInterfaceType": { "type": "string", "metadata": { "description": "Whether to have a public or private NIC" }, "allowedValues": [ "Public", "Private" ] }

2. Use the Parameter in our URL

As you’ve probably guessed, we are now going to use this parameter in our “deployments” resource to define the URL, so this is going to look like:

"templateLink": {
    "uri": "[concat(parameters('artifactsLocation'), '/linkedTemplates/CreateNIC',parameters('NetworkInterfaceType'),'.json', parameters('artifactsLocationSasToken'))]",
    "contentVersion": "1.0.0.0"
},

so once that all gets stitched together our URL will look something like:

“https:///linkedTemplates/CreateNICPublic.json”

or

“https:///linkedTemplates/CreateNICPrivate.json”

 

This then allows us to determine which linked template to run at run time, giving us a sort of conditional logic. Here’s the full top level template:

DeployResources.json:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "artifactsLocation": {
            "type": "string",
            "metadata": {
                "description": "URL to location of linked templates"
            }
        },
        "artifactsLocationSasToken": {
            "type": "securestring",
            "metadata": {
                "description": "SAS Token to access linked templates"
            }
        },
        "NetworkInterfaceType": {
            "type": "string",
            "metadata": {
                "description": "Whether to have a public or private NIC"
            },
            "allowedValues": [
                "Public",
                "Private"
            ]
        }
    },
    "variables": {
        "NetworkName": "lambda-vnet",
        "Subnet1Name": "lambda-subnet1",
        "Subnet2Name": "lambda-subnet2"
    },
    "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"
                        }
                    }
                ]
            }
        },
        {
            "name": "CreateNIC",
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2015-01-01",
            "tags": {
                "displayName": "CreateNIC"
            },
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "[concat(parameters('artifactsLocation'), '/linkedTemplates/CreateNIC',parameters('NetworkInterfaceType'),'.json', parameters('artifactsLocationSasToken'))]",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "NICName": "LambdaNIC1",
                    "virtualNetworkName": "[variables('NetworkName')]",
                    "subnetName": "[variables('Subnet1Name')]",
                    "IPAllocationMethod": "Dynamic"
                }
            }
        }
    ],
    "outputs": {}
}

Limitations

Using this method we do still end up duplicating some code in our NIC templates, there’s not really any way around this. But, by modularising the parts of our templates that need to be more dynamic and using this method we do at least minimise the amount of code we have to duplicate.

Even if you don’t need conditional selection right now, using linked templates is a good idea anyway as it allows you to modularise your templates and re-use code much more easily, and if you do need to add conditions later, it’s simple.

Advanced Techniques

In the example above, the parameter we fed in to vary the template was directly linked to the URL of the linked template. If you wanted to de-couple this link, or have a single parameter trigger multiple variations, you can do this using multi level variables, or t-shirt sizes as some people refer to them. In our example, lets say as well as selecting whether to have a public or private IP, we also want machines with private IP’s to have a static IP. In our top level template we would keep the parameter as-is, with two options public or private, but then we would define some variables:

"variables": {
    "NetworkInterfaceType": "[parameters('NetworkInterfaceType')]",
    "Private": {
        "NICTemplateName": "CreateNICPrivate",
        "IPAllocationMethod": "Dynamic"
    },
    "Public": {
        "NICTemplateName": "CreateNICPublic",
        "IPAllocationMethod": "Static"
    }
}

 

Then we would change our deployment resource to use these multi level variables both in the URL and the parameters

 

{
    "name": "CreateNIC",
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2015-01-01",
    "tags": {
        "displayName": "CreateNIC"
    },
    "properties": {
        "mode": "Incremental",
        "templateLink": {
            "uri": "[concat(parameters('artifactsLocation'), '/linkedTemplates/',variables('NetworkInterfaceType').NICTemplateName,'.json', parameters('artifactsLocationSasToken'))]",
            "contentVersion": "1.0.0.0"
        },
        "parameters": {
            "NICName": "LambdaNIC1",
            "virtualNetworkName": "[variables('NetworkName')]",
            "subnetName": "[variables('Subnet1Name')]",
            "IPAllocationMethod": "[variables('NetworkInterfaceType').IPAllocationMethod]"
        }
    }
}

 

By using this method, you can firstly select multiple options with a single parameter, but secondly you de-couple your interface (the parameters) from your implementation. If you later want to add more options to the NIC selection, you can add them to your variable without changing the way you call your template.

Additional Reading

Using linked templates when deploying Azure resources

Best practices for creating Azure Resource Manager templates