Conditional parameters for ARM templates

Whilst creating Azure Resource Manager templates for my IaaS environment, it quickly became apparent that I’d need multiple nested templates to support the various options we have configured on our VM’s.

I’m not keen on duplicating templates several times, so I decided to design a single template to support all the options based on parameters passed to the template.

I assumed this would be fairly simple to do, but the lack of logical and comparison operators available in the Resource Manager Template functions actually make this tricky.

The good news is that we can take advantage of JSON objects and data to achieve a dynamic template for multiple conditional options.  This has allowed me to create a template that can be used for conditionally configuring the following options on a VM:

  • Dynamic or static private IP
  • Public IP address
  • Availability set

Parameters

I’ll start with the parameters that are important of this design, there are a number of parameters in the full template but we can ignore them for this explanation:

    "publicIpAssignment": {
      "type": "string",
      "allowedValues": [ "assigned", "none" ],
      "metadata": {
        "description": "Has this VM been assigned a public IP address or not?" 
      }
    },
    "privateIPAllocationMethod": {
      "type": "string",
      "allowedValues": [ "Dynamic", "Static" ],
      "metadata": {
        "description": "Is this VM using a static or dynamic IP?"
      }
    },
    "privateIPAddress": {
      "type": "string",
      "metadata": {
        "description": "Private IP address of the VM"
      }
    }, 
    "availabilitySetName": {
      "type": "string"
    },
    "availabilitySetStatus": {
      "type": "string",
      "allowedValues": [ "member", "none" ],
      "metadata": {
        "description": "Is this VM a member of an availability set?"
      }
    }

These are fairly self explanatory, so I’ll move onto the variables that use the parameters.

Variables

"publicIpConfig": {
      "assigned": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',toLower(resourceGroup().name),'/providers/Microsoft.Network/publicIPAddresses/',toLower(parameters('name')))]"
      },
      "none": { }
    },
    "publicIpAddressObject": "[variables('publicIpConfig')[parameters('publicIpAssignment')]]",
    "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',parameters('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetID'),'/subnets/', parameters('subnetName'))]",
    "nicIpConfiguration": {
      "Static": {
        "privateIPAllocationMethod": "Static",
        "privateIPAddress": "[parameters('privateIPAddress')]",
        "subnet": {
          "id": "[variables('subnetRef')]"
        },
        "publicIPAddress": "[variables('publicIpAddressObject')]"
      },
      "Dynamic": {
        "privateIPAllocationMethod": "Dynamic",
        "subnet": {
          "id": "[variables('subnetRef')]"
        },
        "publicIPAddress": "[variables('publicIpAddressObject')]"
      }
    },
    "nicIpProperties": "[variables('nicIpConfiguration')[parameters('privateIPAllocationMethod')]]",
    "availabilitySetConfiguration": {
      "member": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',resourceGroup().name,'/providers/Microsoft.Compute/availabilitySets/',parameters('availabilitySetName'))]"
      },
      "none": { }
    },
    "availabilitySetProperties": "[variables('availabilitySetConfiguration')[parameters('availabilitySetStatus')]]"

The first variable, publicIpConfig, is actually a JSON object with two child objects:

  • assigned
  • none

“Assigned” returns an object that is required for assigning a public IP address to a network interface.  You’ll notice that I don’t use resourceId() to get the ID for the public IP address, this is because the entire template is executed at runtime.  This means that if a public IP isn’t meant to be assigned to the network interface, resourceId() will fail.  To get around this, I’m creating the ID string manually using the subscription().subscriptionID and resourceGroup().name properties.

In our environment, the name of a public IP address matches the name of the VM, so we can use the name parameter for the VM, however you can easily change that to suit your needs.

“None” returns an empty object, not much more to say about that!

The publicIpAddressObject variable is where it gets interesting, this references either publicIpConfig.assigned or publicIpConfig.none, depending on the value of the publicIpAssignment parameter.

It’s this reference that allows us to conditionally assign properties to the resource.

Next we move onto nicIpConfiguration variable, which has two child objects:

  • Dynamic
  • Static

These objects contain the necessary information for configuring either a dynamic or static IP address on a network interface.  They also reference the publicIpAddressObject so that we can include a public IP address if required.

the nicIpProperties variable references either nicIpConfiguration.Static or nicIpConfiguration.Dynamic, depending on the value of the privateIPAllocationMethod parameter.

Again, it’s the value of the parameter that is driving the configuration that is assigned to the resource.

Last of all, we have availabilitySetConfiguration, which has two child objects:

  • member
  • none

These objects contain the necessary information for configuring a VM as part of an availability set.

As you’ll notice I’m not using the resourceId() function, instead I’m building the object ID with the subscription().subscriptionId and resourceGroup().name properties.

availabilitySetProperties returns either availabilitySetConfiguration.member or availabilitySetConfiguration.none depending on the availabilitySetStatus parameter.

Resources

We all of the variables configured, we can move onto the resources. The following example uses the nicIpProperties variable to set the properites on a network interface:

{
        "apiVersion": "[variables('apiVersion')]",
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[variables('nicName')]",
        "location": "[resourceGroup().location]",
        "dependsOn": [ ],
        "properties": {
          "ipConfigurations": [
            {
              "name": "ipconfig1",
              "properties": "[variables('nicIpProperties')]"
            }
          ]
        }
      }

The following example uses the availabilitySetProperties variable to configure an availability set on a VM, the value will be an empty object if the VM isn’t part of an availability set:

 {
        "apiVersion": "[variables('apiVersion')]",
        "type": "Microsoft.Compute/virtualMachines",
        "name": "[parameters('name')]",
        "location": "[resourceGroup().location]",
        "dependsOn": [
          "[concat('Microsoft.Network/networkInterfaces/',variables('nicName'))]"
        ],
        "properties": {
          "hardwareProfile": {
            "vmSize": "[parameters('size')]"
          },
          "osProfile": {
            "computerName": "[parameters('name')]",
            "adminUsername": "[variables('adminUsername')]",
            "adminPassword": "[parameters('adminPassword')]"
          },
          "storageProfile": {
            "imageReference": {
              "publisher": "[parameters('imagePublisher')]",
              "offer": "[parameters('imageOffer')]",
              "sku": "[parameters('imageSKU')]",
              "version": "[parameters('imageVersion')]"
            },
            "dataDisks": [],
            "osDisk": {
              "name": "[concat(parameters('name'),'-os')]",
              "vhd": {
                "uri": "[concat(variables('storageUri'),'/vhds/',parameters('name'),'-os.vhd')]"
              },
              "caching": "ReadWrite",
              "createOption": "FromImage"
            }
          },
          "networkProfile": {
            "networkInterfaces": [
              {
                "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
              }
            ]
          },
          "availabilitySet": "[variables('availabilitySetProperties')]",
          "diagnosticsProfile": {
            "bootDiagnostics": {
              "enabled": "true",
              "storageUri": "[variables('storageUri')]"
            }
          }
        }

It took me some time to piece this all together, so I really do hope it helps others save time working this out and reduces the number of templates you need to use! I’m sure there are many other conditional objects that can use this design, feel free to contact me with any questions.

Here’s a copy of the full template:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminPassword": {
      "type": "securestring"
    },
    "name": {
      "type": "string"
    },
    "size": {
      "type": "string"
    },
    "imagePublisher": {
      "type": "string"
    },
    "imageOffer": {
      "type": "string"
    },
    "imageSKU": {
      "type": "string"
    },
    "imageVersion": {
      "type": "string",
      "defaultValue": "latest"
    },
    "publicIpAssignment": {
      "type": "string",
      "allowedValues": [ "assigned", "none" ],
      "metadata": {
        "description": "Has this VM been assigned a public IP address or not?" 
      }
    },
    "privateIPAllocationMethod": {
      "type": "string",
      "allowedValues": [ "Dynamic", "Static" ],
      "metadata": {
        "description": "Is this VM using a static or dynamic IP?"
      }
    },
    "privateIPAddress": {
      "type": "string",
      "metadata": {
        "description": "Private IP address of the VM"
      }
    },
    "virtualNetworkName": {
      "type": "string"
    },
    "subnetName": {
      "type": "string"
    },
    "storageAccountName": {
      "type": "string"
    },
    "availabilitySetName": {
      "type": "string"
    },
    "availabilitySetStatus": {
      "type": "string",
      "allowedValues": [ "member", "none" ],
      "metadata": {
        "description": "Is this VM a member of an availability set?"
      }
    }
  },
  "variables": {
    "adminUsername": "localadmin",
    "apiVersion": "2015-06-15",
    "nicName": "[concat(parameters('name'),'-nic1')]",
    "storageUri": "[concat('https://',parameters('storageAccountName'),'.blob.core.windows.net')]",
    "publicIpConfig": {
      "assigned": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',toLower(resourceGroup().name),'/providers/Microsoft.Network/publicIPAddresses/',toLower(parameters('name')))]"
      },
      "none": { }
    },
    "publicIpAddressObject": "[variables('publicIpConfig')[parameters('publicIpAssignment')]]",
    "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',parameters('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetID'),'/subnets/', parameters('subnetName'))]",
    "nicIpConfiguration": {
      "Static": {
        "privateIPAllocationMethod": "Static",
        "privateIPAddress": "[parameters('privateIPAddress')]",
        "subnet": {
          "id": "[variables('subnetRef')]"
        },
        "publicIPAddress": "[variables('publicIpAddressObject')]"
      },
      "Dynamic": {
        "privateIPAllocationMethod": "Dynamic",
        "subnet": {
          "id": "[variables('subnetRef')]"
        },
        "publicIPAddress": "[variables('publicIpAddressObject')]"
      }
    },
    "nicIpProperties": "[variables('nicIpConfiguration')[parameters('privateIPAllocationMethod')]]",
    "availabilitySetConfiguration": {
      "member": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',resourceGroup().name,'/providers/Microsoft.Compute/availabilitySets/',parameters('availabilitySetName'))]"
      },
      "none": { }
    },
    "availabilitySetProperties": "[variables('availabilitySetConfiguration')[parameters('availabilitySetStatus')]]"
  },
  "resources": [
      {
        "apiVersion": "[variables('apiVersion')]",
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[variables('nicName')]",
        "location": "[resourceGroup().location]",
        "dependsOn": [ ],
        "properties": {
          "ipConfigurations": [
            {
              "name": "ipconfig1",
              "properties": "[variables('nicIpProperties')]"
            }
          ]
        }
      },
      {
        "apiVersion": "[variables('apiVersion')]",
        "type": "Microsoft.Compute/virtualMachines",
        "name": "[parameters('name')]",
        "location": "[resourceGroup().location]",
        "dependsOn": [
          "[concat('Microsoft.Network/networkInterfaces/',variables('nicName'))]"
        ],
        "properties": {
          "hardwareProfile": {
            "vmSize": "[parameters('size')]"
          },
          "osProfile": {
            "computerName": "[parameters('name')]",
            "adminUsername": "[variables('adminUsername')]",
            "adminPassword": "[parameters('adminPassword')]"
          },
          "storageProfile": {
            "imageReference": {
              "publisher": "[parameters('imagePublisher')]",
              "offer": "[parameters('imageOffer')]",
              "sku": "[parameters('imageSKU')]",
              "version": "[parameters('imageVersion')]"
            },
            "dataDisks": [ ],
            "osDisk": {
              "name": "[concat(parameters('name'),'-os')]",
              "vhd": {
                "uri": "[concat(variables('storageUri'),'/vhds/',parameters('name'),'-os.vhd')]"
              },
              "caching": "ReadWrite",
              "createOption": "FromImage"
            }
          },
          "networkProfile": {
            "networkInterfaces": [
              {
                "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
              }
            ]
          },
          "availabilitySet": "[variables('availabilitySetProperties')]",
          "diagnosticsProfile": {
            "bootDiagnostics": {
              "enabled": "true",
              "storageUri": "[variables('storageUri')]"
            }
          }
        }
      }
      ]
    }

Re-connecting to a failed Azure Windows VM

We’ve recently had a couple of issues connecting to Azure Windows VM’s after a reboot, boot diagnostics was showing that VM’s has booted and Windows was waiting at the login screen.
However, the VM wasn’t responding to pings and we couldn’t connect to with RDP or WinRM.
We tried using the Azure (IaaS) diagnostics, but that was trying to use WinRM to connect to the VM so it also failed.
Suspecting the issue was Windows Firewall, we carried out the following:

  1. Deleted the VM (keeping the disks!!!)
  2. Attached the OS disk to another VM running the same OS
  3. Used regedit to load the system hive from the failed VM
  4. Set the following registry keys
    1. CurrentControlSet001\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\EnableFirewall=0 (DWORD data type)
    2. CurrentControlSet001\Services\SharedAccess\Parameters\FirewallPolicy\DomainProfile\EnableFirewall=0 (DWORD data type)
  5. Detach the disk from the second VM
  6. Re-create the original failed VM, using the existing disks
Once the VM booted up, we were able to connect again.
I believe the issue relates to Windows not applying our GPO for disabling the firewall, to get around this we’ll use GPP to set these registry values rather than relying on the GPO settings.