Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using Linked Templates and Conditional Logic in ARM

DZone's Guide to

Using Linked Templates and Conditional Logic in ARM

With no 'if' statements in ARM templates, your Azure deployments might benefit from linking your templates together to provide some form of conditional flow.

· Cloud Zone
Free Resource

Are you joining the containers revolution? Start leveraging container management using Platform9's ultimate guide to Kubernetes deployment.

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, say if you have to create a whole new set of templates that 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 runtime 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 decides 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 supporting code re-use: through the use of linked templates.

Linked Templates

ARM templates support the use of linked templates (having one template call another). This is great for allowing you to modularize 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.

Let's 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 it, except you create it in its own file just for the NIC creation. In this example, we will create a NIC with only a private IP. 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 organization, 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 you're 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. 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 that 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 parameters 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 runtime.

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 that 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.

Add a Parmeter

To allow us to select which file we use, we will set up a parameter in the top-level template. We will use this paramter 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"
            ]
        }

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, which will 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://<blob storage URL>/linkedTemplates/CreateNICPublic.json<SAS Token>"

or

"https://<blob storage URL>/linkedTemplates/CreateNICPrivate.json<SAS Token>"


And so this then allows us to determine which linked template to run at runtime, 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 modularizing the parts of our templates that need to be more dynamic and using this method, we do at least minimize 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 modularize 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, let's say as well as selecting whether to have a public or private IP, we also want machines with private IPs 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 want to add more options to the NIC selection later, you can add them to your variable without changing the way you call your template.

Additional Reading

Using Containers? Read our Kubernetes Comparison eBook to learn the positives and negatives of Kubernetes, Mesos, Docker Swarm and EC2 Container Services.

Topics:
cloud ,arm templates ,microsoft azure ,conditional logic ,tutorial

Published at DZone with permission of Sam Cogan, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}