More Fun with ARM Templates

All this time, all I’ve wanted to do with an Azure Resource Manager (ARM) template was create a bunch of identical computers.   The biggest roadblock I’ve faced is figuring out how to make the ARM template not one gigantic document with similarly-named resources in it.

Each VM requires a public IP address, a NIC, and a VM resource.   When I’ve put those into a template, if I named everything to make sense to me (like “Computer1” would have “Computer1-IP” and “Computer1-NIC” attached to it), the document gets really hard to follow.    I can copy and paste the definitions for these three objects, but eventually it gets ugly and confusing, and when something doesn’t work, it’s a pain to work around.

Enter the “count” parameter!   There’s a neat way that you can tell the ARM engine to iterate through a list of resources and create however many you want it to.   The engine breaks down the JSON file and duplicates the guts of it “n” times, assigning a suffix with the number on it at the end of the item names.

Below is my ARM template that creates as many VMs as you want, each with a public IP address and a NIC.

{ "$schema": "", "contentVersion": "", "parameters": { "userImageStorageAccountName": { "type": "string", "metadata": { "description": "This is the name of the your storage account" } }, "userImageStorageContainerName": { "type": "string", "metadata": { "description": "This is the name of the container in your storage account" } }, "userImageVhdName": { "type": "string", "metadata": { "description": "This is the name of the your customized VHD" } }, "adminUserName": { "type": "string", "metadata": { "description": "UserName for the Virtual Machine" } }, "adminPassword": { "type": "securestring", "metadata": { "description": "Password for the Virtual Machine" } }, "osType": { "type": "string", "allowedValues": [ "windows", "linux" ], "metadata": { "description": "This is the OS that your VM will be running" } }, "vmSize": { "type": "string", "metadata": { "description": "This is the size of your VM" } }, "vmNameBase": { "type": "string", "metadata": { "description": "This is the size of your VM" } }, "vmCount":{ "type": "int", "defaultValue": 1, "metadata": { "description": "The number of VMs to build." } } }, "variables": { "location": "[resourceGroup().location]", "virtualNetworkName": "VNetName", "addressPrefix": "", "subnet1Name": "default", "subnet1Prefix": "", "publicIPAddressType": "Dynamic", "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", "subnet1Ref": "[concat(variables('vnetID'),'/subnets/',variables('subnet1Name'))]", "userImageName": "[concat('http://',parameters('userImageStorageAccountName'),'',parameters('userImageStorageContainerName'),'/',parameters('userImageVhdName'))]" }, "resources": [ { "apiVersion": "2015-05-01-preview", "type": "Microsoft.Network/publicIPAddresses", "name": "[concat(parameters('vmNameBase'),copyIndex(),'-PublicIP')]", "copy": { "name": "publicIPAddressCopy", "count": "[parameters('vmCount')]" }, "location": "[variables('location')]", "properties": { "publicIPAllocationMethod": "[variables('publicIPAddressType')]", "dnsSettings": { "domainNameLabel": "[concat(parameters('vmNameBase'),copyIndex())]" } } }, { "apiVersion": "2015-05-01-preview", "type": "Microsoft.Network/networkInterfaces", "name": "[concat(parameters('vmNameBase'),copyIndex(),'-NIC')]", "location": "[variables('location')]", "copy": { "name": "networkInterfacesCopy", "count": "[parameters('vmCount')]" }, "dependsOn": [ "[concat('Microsoft.Network/publicIPAddresses/', parameters('vmNameBase'),copyIndex(),'-PublicIP')]" ], "properties": { "ipConfigurations": [ { "name": "ipconfig1", "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { "id": "[resourceId('Microsoft.Network/publicIPAddresses/',concat(parameters('vmNameBase'),copyIndex(),'-PublicIP'))]" }, "subnet": { "id": "[variables('subnet1Ref')]" } } } ] } }, { "apiVersion": "2015-06-15", "type": "Microsoft.Compute/virtualMachines", "name": "[concat(parameters('vmNameBase'),copyIndex())]", "copy": { "name": "vmCopy", "count": "[parameters('vmCount')]" }, "location": "[variables('location')]", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/',parameters('vmNameBase'),copyIndex(),'-NIC')]", "[concat('Microsoft.Network/publicIPAddresses/', parameters('vmNameBase'),copyIndex(),'-PublicIP')]"
], "properties": { "hardwareProfile": { "vmSize": "[parameters('vmSize')]" }, "osProfile": { "computername": "[concat(parameters('vmNameBase'),copyIndex())]", "adminUsername": "[parameters('adminUsername')]", "adminPassword": "[parameters('adminPassword')]" }, "storageProfile": { "osDisk": { "name": "[concat(parameters('vmNameBase'),copyIndex(),'-osDisk')]", "osType": "[parameters('osType')]", "caching": "ReadWrite", "createOption": "FromImage", "image": { "uri": "[variables('userImageName')]" }, "vhd": { "uri": "[concat('http://',parameters('userImageStorageAccountName'),'',parameters('vmNameBase'), copyIndex(),'-osDisk.vhd')]" } } }, "networkProfile": { "name": "[concat(parameters('vmNameBase'),copyIndex(),'-networkProfile')]", "networkInterfaces": [ { "id": "[resourceId('Microsoft.Network/networkInterfaces/',concat(parameters('vmNameBase'),copyIndex(),'-NIC'))]" } ] }, "diagnosticsProfile": { "bootDiagnostics": { "enabled": "true", "storageUri": "[concat('http://',parameters('userImageStorageAccountName'),'')]" } } } } ] }

The “copyIndex()” command is what tells the ARM engine to use the value from the loop.   Using “DependsOn” with this, means that Computer1 will depend upong Computer1-NIC and Computer1-PublicIP, so nothing will be built if the other parts aren’t ready for it.

This template is setup to be used with a user image, something custom that I’ve put up there that’s already sysprepped and ready-to-go.  I haven’t yet made this add the resources for the extensions or gotten it to join the domain after being built, but at least I have the VMs running now.

Written on January 25, 2016