Close

24th June 2017

Deploying Multiple VMs with ARM Templates or how to use ‘Copy’ and ‘CopyIndex’

Deploying Multiple VMs with ARM Templates or how to use ‘Copy’ and ‘CopyIndex’

I’ll preface with a few of the resources I’ve used so far in my Azure study journey

Tools, Resources and Progress

If you’ve been following this series of posts (can you call two posts a series?), then you will have the tools, resources, tempates and scripts to;

  1. Deploy a new Network and subnet containing an IaaS AD controller into an Azure subscription.
  2. Deploy a new WindowsVM and perform a domain join operation.

That’s great, but what if you want to deploy multiple VMs?  This is where ‘Copy’ and ‘CopyIndex’ come in.

Template

To follow along with this text you can copy the template previously used to deploy a new WindowsVm and perform a domain join operation.  What I’m going to run through are the edits needed to be made to that template to deploy it multiple times.

The template you will recall has the following parameters;

Deploying Multiple VMs with ARM Templates or how to use 'Copy' and 'CopyIndex'

The template has the following resources;

I’m highlighting these and not the variables as we are not going to change any of the variables within the template.

Parameters

We need to add one Parameter to the existing template, we need to add a count parameter.

"vmCount": {
    "type": "int",
    "defaultValue": 1
},

This parameter references the ‘azuredeploy.paramters.json‘ file to read in the number of resources that are to be created.

"vmCount": {
    "value": 2
}

 

Notice that I’ve changed the language here to start talking about resources and not Virtual Machines.  When we’re working within the template and with azure in general, it’s important to remember that the Virtual Machine itself is a resource, its a resource that requires further resources to be created to provide a working environment.  A Virtual Machine resource needs Storage, Networking for example.

Copy

In order to deploy multiple resources a ‘copy‘ element needs to added to the resource types within the template.

"copy": {
    "name": "nodeCopy",
    "count": "[parameters('vmCount')]"
},

This copy element is quite basic in it’s structure.  It requires a name for the loop, which is a string and for reference. It requires that the number of iterations be provided.  ‘Count‘ must be a positive integer and cannot be greater than 800 (at the time of writing).

The other really important thing to realise about the ‘count’ element is that Azure is going to deploy these elements in parallel.  The order the resources are created is not guaranteed.  Which could be a problem.

To work around this a ‘dependsOn‘ element can be added or amended as needed.  These elements already exist in the script we’re modifying, stating that for example the ‘virtualMachines‘ resource is not created before the ‘storageAccounts‘ or ‘networkInterfaces‘.

In order for the template to deploy multiple resources the ‘copy’ element needs to be added to all of the resources.  The only exception to the is the ‘storageAccount‘ resource which doesn’t have a direct one to one mapping with a ‘virtualMachine‘ resource.  Current recommendations are to deploy each VM resource into it’s own storage account, this is for ease of administration…

However, there are some pretty hard limitations on the number of storage accounts a subscription can hold (200 as I write this).  Microsoft have some recommendations around the maximum number of VMs in a storage account, these are all tied back to IOPS.

“For standard storage accounts: A standard storage account has a maximum total request rate of 20,000 IOPS. The total IOPS across all of your virtual machine disks in a standard storage account should not exceed this limit.

You can roughly calculate the number of highly utilized disks supported by a single standard storage account based on the request rate limit. For example, for a Basic Tier VM, the maximum number of highly utilized disks is about 66 (20,000/300 IOPS per disk), and for a Standard Tier VM, it is about 40 (20,000/500 IOPS per disk), as shown in the table below.”

SO you can see for a real deployment you will need to give the storage layout a good deal of thought.  For this test operation, the template is going to use one ‘storageAccount’ resource.

CopyIndex

For each resource deployment that is being looped by the ‘copy‘ element there needs to be a ‘copyIndex()‘ function added to the resource name.  the ‘copyIndex()‘ function is going to return the current iteration of the loop. If we use this in conjunction with the ‘concat‘ function we can add the loop iteration to the name of the resource being created.

copyIndex()‘ is zero based so using the function as written in this sentence, will add a number starting from ‘0‘ to the name of the resource. If you want to change that you can offset that you can specify a value within the brackets.  Therefore ‘copyIndex(1)‘ will add a number starting from ‘1‘.

"name": "[concat(parameters('dnsLabelPrefix'), copyIndex(1))]",

In the above example the name is going to be a concatenation of the parameter ‘dnsLabelPrefix‘ and the current iteration of the loop starting at 1.

Resources including copy and copyIndex

If I’ve managed to explain the theory sufficiently then you should have been able to edit the resources section of the template so that it looks something like the below;

"resources": [{
    "apiVersion": "[variables('apiVersion')]",
    "type": "Microsoft.Network/publicIPAddresses",
    "name": "[concat(parameters('dnsLabelPrefix'),copyIndex(1))]",
    "location": "[resourceGroup().location]",
    "properties": {
        "publicIPAllocationMethod": "Dynamic",
        "dnsSettings": {
            "domainNameLabel": "[concat(parameters('dnsLabelPrefix'),copyIndex(1))]"
        }
    },
    "copy": {
        "name": "publicIpCopy",
        "count": "[parameters('vmCount')]"
    }
}, {
    "apiVersion": "[variables('apiVersion')]",
    "type": "Microsoft.Storage/storageAccounts",
    "name": "[variables('storageAccountName')]",
    "location": "[resourceGroup().location]",
    "properties": {
        "accountType": "Standard_LRS"
    }
}, {
    "apiVersion": "[variables('apiVersion')]",
    "type": "Microsoft.Network/networkInterfaces",
    "name": "[concat(variables('nicName'),copyIndex(1))]",
    "location": "[resourceGroup().location]",
    "dependsOn": ["[concat('Microsoft.Network/publicIPAddresses/', concat(parameters('dnsLabelPrefix'),copyIndex(1)))]"],
    "properties": {
        "ipConfigurations": [{
            "name": "[concat('ipconfig',copyIndex(1))]",
            "properties": {
                "privateIPAllocationMethod": "Dynamic",
                "publicIPAddress": {
                    "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('dnsLabelPrefix'),copyIndex(1)))]"
                },
                "subnet": {
                    "id": "[variables('subnetId')]"
                }
            }
        }]
    },
    "copy": {
        "name": "nicCopy",
        "count": "[parameters('vmCount')]"
    }
}, {
    "apiVersion": "[variables('apiVersion')]",
    "copy": {
        "name": "nodeCopy",
        "count": "[parameters('vmCount')]"
    },
    "type": "Microsoft.Compute/virtualMachines",
    "name": "[concat(parameters('dnsLabelPrefix'), copyIndex(1))]",
    "location": "[resourceGroup().location]",
    "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts',variables('storageAccountName'))]", "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nicName'),copyIndex(1)))]"],
    "properties": {
        "hardwareProfile": {
            "vmSize": "[parameters('vmSize')]"
        },
        "osProfile": {
            "computerName": "[concat(parameters('dnsLabelPrefix'), copyIndex(1))]",
            "adminUsername": "[parameters('vmAdminUsername')]",
            "adminPassword": "[parameters('vmAdminPassword')]"
        },
        "storageProfile": {
            "imageReference": {
                "publisher": "[variables('imagePublisher')]",
                "offer": "[variables('imageOffer')]",
                "sku": "[variables('windowsOSVersion')]",
                "version": "latest"
            },
            "osDisk": {
                "name": "osdisk",
                "vhd": {
                    "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob,'vhds/',concat(parameters('dnsLabelPrefix'), copyIndex(1)),'.vhd')]"
                },
                "caching": "ReadWrite",
                "createOption": "FromImage"
            },
            "dataDisks": [{
                "name": "myvm-data-disk1",
                "vhd": {
                    "Uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob,'vhds/',concat(parameters('dnsLabelPrefix'), copyIndex(1)), 'disk1.vhd')]"
                },
                "caching": "None",
                "createOption": "Empty",
                "diskSizeGB": "1000",
                "lun": 0
            }]
        },
        "networkProfile": {
            "networkInterfaces": [{
                "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nicName'),copyIndex(1)))]"
            }]
        },
        "diagnosticsProfile": {
            "bootDiagnostics": {
                "enabled": "true",
                "storageUri": "[reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob]"
            }
        }
    }
}, {
    "apiVersion": "[variables('apiVersion')]",
    "copy": {
        "name": "DomainJoinCopy",
        "count": "[parameters('vmCount')]"
    },
    "type": "Microsoft.Compute/virtualMachines/extensions",
    "name": "[concat(parameters('dnsLabelPrefix'),copyIndex(1),'/joindomain')]",
    "location": "[resourceGroup().location]",
    "dependsOn": ["[concat('Microsoft.Compute/virtualMachines/', concat(parameters('dnsLabelPrefix'),copyIndex(1)))]"],
    "properties": {
        "publisher": "Microsoft.Compute",
        "type": "JsonADDomainExtension",
        "typeHandlerVersion": "1.3",
        "autoUpgradeMinorVersion": true,
        "settings": {
            "Name": "[parameters('domainToJoin')]",
            "OUPath": "[parameters('ouPath')]",
            "User": "[concat(parameters('domainToJoin'), '\\', parameters('domainUsername'))]",
            "Restart": "true",
            "Options": "[parameters('domainJoinOptions')]"
        },
        "protectedSettings": {
            "Password": "[parameters('domainPassword')]"
        }
    }
}]

Validation and completion

As before the project can be validated and deployed from within Visual Studio.

With the tools, Resources and templates that you have available to you now you will be able to create a domain in Azure IaaS and add multiple VM resources to that domain.  That’s a pretty good foundation for a lab in Azure.

I would like to follow this up to demonstrate how you can use DSC to create more complex environments, but I suspect time is going to be at a premium for me over the coming weeks.  so apologies in advance!

Simon