Conditions in ARM templates

If you are faced with writing an ARM template that deploys services to VMs differently in Dev and Test environment, how do you write the template? You can of cause have two different ARM templates, but that gives you more to maintain. When faced with this problem, I solved it by using the condition function.

Problem

An example of the problem can be describes as this. You have Service X and Y and in the Dev enviroment you need to deploy it to one VM, while in the Test environment they should run on separate VMs. In the Dev env, host A consists of multiple VMs and in the Test env, both host A and B consists of multiple VMs.

In the ARM Template you have the following set of parameters (not complete set). For the Dev environment, you will have values of countHostA = 2 and countHostB = 0, but for the Test environment you will have countHostA = 2 and countHostB = 2.

        "envName": {
            "type": "string",
            "allowedValues": [
                "dev",
                "test",
                "prod"
              ],
              "defaultValue": "dev",
              "metadata": {
                "description": "The environment name"   
              }         
        },
        "countHostA": {
            "type": "int",
            "defaultvalue":1
        },
        "countHostB": {
            "type": "int",
            "defaultvalue":0
        }

Solution

The first problem is that since we are going to use a copy-loop in the ARM template to create NICs and VMs, the copy-count doesn’t allow the value of 0 (you’ll get an error saying it must be a positive integer). We like the countHostB parameter to pass in a value of 0 as it represents how many VMs we will have in Dev environment, but we then must make the template not fail validations.

The solution is to change the value of 0 to 1 and then have a condition set for Host B only to provision if the environment is Test. Passing in a value of 1 for countHostB and getting 0 VMs provisioned would be very confusing.

Variables

In the variables section, we do this

        "countHostBVar": "[if(equals(parameters('countHostB'), 0), 1, parameters('countHostB'))]",
        "hostARoles": "[if(equals(parameters('envName'), 'Dev'), 'ServiceX, ServiceY', 'ServiceX') ]"
        "hostBRoles": "ServiceY",

The first line creates an internal variable – countHostBVar – that is set to 1 in case we pass in 0 or else the value passed in (because 0 is invalid in copy-loops). The second variable creates a variable that will have the value ” ServiceX, ServiceY” if it’s the Dev environment or just “ServiceX” in the Test environment. We will use this value to pass to the Custom Script Extension we kick off

Resources

Creating the resources, like the NIC, for Host B should only happen in the Test environment, so we put a condition for that resource. This part would fail if we used a copy-value of 0 but since we bump it up to 1 but block it with the condition, it will not create a NIC in the Dev environment. The same logic is applied to the VM and the Custom Scrip Extension

        {
            "condition": "[equals(parameters('envName'), 'Test')]",
            "name": "[concat('hostb', copyIndex(1), '-nic')]",
            "type": "Microsoft.Network/networkInterfaces",
            "apiVersion": "2018-04-01",
            "location": "[parameters('location')]",
            "dependsOn": [
            ],
            "copy": {
                "name": "nicLoop",
                "count": "[variables('countHostBVar')]"
            },
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "subnet": {
                                "id": "[resourceId( 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName') ) ]"
                            }
                        }
                    }
                ]
            },
            "tags": {}
        },

The installation of the services is performed using Custom Script Extensions in this example. For Host A, we pass variable hostARoles as aparameter to the installer script and it will contain either “ServiceX,ServiceY” or just “ServiceX”. The script is assumed to handle the different installation tasks.

For Host B we do the same type of passing a parameter, but the whole resource is conditioned on the environment being Test.

        {
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat('hosta', copyIndex(1), '/installscript')]",
            "apiVersion": "2018-06-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachines/', 'hosta', copyIndex(1) )]"
            ],
            "copy": {
                "name": "vmExtLoop",
                "count": "[variables('countHostBVar')]"
            },
            "properties": {
                "publisher": "Microsoft.OSTCExtensions",
                "type": "CustomScriptForLinux",
                "typeHandlerVersion": "1.5",
                "settings": {
                    "fileUris": [
                        "[variables('scriptUrl')]"
                    ],
                    "commandToExecute": "[concat('bash installer.sh', ' ', variables('hostARoles'))]"
                }
            }
        },        

        {
            "condition": "[equals(parameters('envName'), 'Test')]",
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat('hostb', copyIndex(1), '/installscript')]",
            "apiVersion": "2018-06-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachines/', 'hostb', copyIndex(1) )]"
            ],
            "copy": {
                "name": "vmExtLoop",
                "count": "[variables('countHostBVar')]"
            },
            "properties": {
                "publisher": "Microsoft.OSTCExtensions",
                "type": "CustomScriptForLinux",
                "typeHandlerVersion": "1.5",
                "settings": {
                    "fileUris": [
                        "[variables('scriptUrl')]"
                    ],
                    "commandToExecute": "[concat('bash installer.sh', ' ', variables('hostBRoles'))]"
                }
            }
        },        

Conclusion

The purpose of this blog post is to show you that by using the condition function, you can make an ARM template provision resources differently depending on something like an environment variable. Why you would do that is probably because the development environment is smaller and should run at with lower cost while the Test environment (or Prod) needs to be bigger in size.