The ‘copy’ property is an Azure ARM template feature that allows you to iterate (for each) loop over a resource to create multiple copies of it. You can combine it with an ‘array of objects’ custom parameter to easily duplicate resources with your supplied values. However there are a couple of snags you might run into so I figured it’s worth a write up with an example.
Start with a single instance of the resource
Before we start passing in arrays of objects its helpful to look at the single-resource template and then we can transform it from there. In our case I’m using a storage account with three template parameters (storageAccountName, storageAccountSku, and storageAccountLocation). It’s a simple template with just one resource.
Tip: test out the deployment template resource as a single instance first (before adding the copy/iteration). This should make it easier to debug and pinpoint if the issue you are having is with the copy code or the template resource definition itself.
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": {
"type": "string"
},
"storageAccountSku": {
"type": "string"
},
"storageAccountLocation": {
"type": "string"
}
},
"variables": { },
"functions": [ ],
"resources": [
{
"name": "[parameters('storageAccountName')]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2018-07-01",
"sku": {
"name": "[parameters('storageAccountSku')]"
},
"kind": "StorageV2",
"location": "[parameters('storageAccountLocation')]",
"properties": {
"supportsHttpsTrafficOnly": true
}
}
],
"outputs": { }
}
Add the array parameter and copy property
We can make the following changes to our template to enable copy/loop iteration over resources:
- The parameters section: remove individual parameters and replace it with a single ‘array’ type.
- Add the ‘copy’ property to resource we want to duplicate.
- Note that the ‘name’ property here is an arbitrary string, just give it a friendly name.
- The count property defines how many times we will loop over the resource (the storage account). We want to loop the same number of times as we have total objects in our supplied array parameter.
- Add CopyIndex() lookups to the parameter() uses in the resource.
Here is the resulting template that now supports iteration:
Note: The toLower() usage below isn’t technically required, but is present to show an example of using multiple template functions together.
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccounts": {
"type": "array"
}
},
"variables": { },
"functions": [ ],
"resources": [
{
"name": "[toLower(parameters('storageAccounts')[CopyIndex()].Name)]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2018-07-01",
"sku": {
"name": "[parameters('storageAccounts')[CopyIndex()].Sku]"
},
"kind": "StorageV2",
"location": "[parameters('storageAccounts')[CopyIndex()].Location]",
"properties": {
"supportsHttpsTrafficOnly": true
},
"copy": {
"name": "storagecopy",
"count": "[length(parameters('storageAccounts'))]"
}
}
],
"outputs": { }
}
Dynamic resources from PowerShell
In the above template you may have noticed that we reference some properties on the objects inside the array. Based on our modifications, the template now expects an array of objects that look like this (expressed as JSON):
{
"Name": "<value>",
"Sku": "<value>",
"Location": "<value>"
}
These objects will be used to supply the details on the storage accounts that should be created (one object for each storage account).
We could of course supply this data via a literal JSON file. Another approach is to generate this via business logic / code in PowerShell at deployment time. Let’s take that route below.
Warning: If you load JSON data from a file using PowerShell’s ConvertFrom-Json cmdlet and then pass that into the deployment, you may experience errors. The reason for this is that the deserialized JSON object from this cmdlet has a structure you may not expect compared to a simple/vanilla class object (ConvertFrom-Json returns a PSCustomObject). I recommend using the Newtonsoft.JSON library to deserialize from a JSON blob to receive a cleaner object.
Define a PowerShell class object/schema
We can use PowerShell class objects to define our schema. We will end up creating new instances of these objects at runtime and will pass them into the template parameters. For more about class objects in PowerShell v5 and later, read here.
class StorageAccountDetails
{
[String] $Name
[String] $Sku
[String] $Location
}
Construct the custom resource detail objects
In your PowerShell application code: make a new collection/list object. Then use some business logic to determine which resources to create and add those details as custom StorageAccountDetails objects to the list.
At the end of this block we should have a collection populated with one or more objects- each of which contains details for a single storage account resource.
<#
This is just a simple example that shows how the objects could be constructed. In the example case we have 3 statically defined storage accounts. All are created in different locations and have unique names, but the same SKU.
You would replace this section with your business logic that determines which resources should be created and the details required to create them.
#>
$storageAccounts = New-Object -TypeName System.Collections.Generic.List[StorageAccountDetails]
$account1 = New-Object -TypeName StorageAccountDetails
$account1.Name = 'deploytest1west'
$account1.Sku = 'Standard_LRS'
$account1.Location = 'westus2'
$account2 = New-Object -TypeName StorageAccountDetails
$account2.Name = 'deploytest2east'
$account2.Sku = 'Standard_LRS'
$account2.Location = 'eastus2'
$account3 = New-Object -TypeName StorageAccountDetails
$account3.Name = 'deploytest3central'
$account3.Sku = 'Standard_LRS'
$account3.Location = 'northcentralus'
$storageAccounts.Add($account1)
$storageAccounts.Add($account2)
$storageAccounts.Add($account3)
Deploy the template using PowerShell
Connect to Azure and fire off the template deployment:
# login (if you haven't already)
Login-AzureRmAccount
# create the resource group (if required)
New-AzureRmResourceGroup -Name 'deploytest-resources' -Location 'westus2'
# run the deployment
# pass in our $storageAccounts array of objects.
New-AzureRmResourceGroupDeployment -ResourceGroupName 'deploytest-resources' -TemplateFile '.\test.json' -storageAccounts $storageAccounts
Deployment results
After deployment we should see all 3 of our resources were created. Each with the names, locations, and sku’s that were specified in our ‘array of objects’ parameter.

We can also see what the input parameter looked like to Azure in the deployment results (parameters tab):

After lots of searching I found your blog and found what I needed. I am trying to create an array of instances from a class. I found it above in your code:
$storageAccounts = New-Object -TypeName System.Collections.Generic.List[StorageAccountDetails]
Then instead of creating multiple objects with different names-> $account1 = New-Object -TypeName StorageAccountDetails I created one and used a for loop to create multiple instances.
Even though that was not the meat of your article I am happy to find it after searching for a good hour. Thank you for putting the time into writting blog aritcles. I know it is very time consuming and I wanted to express my thanks.
LikeLike
Hey Fred — thanks for your comment, really appreciate it. Glad this post was helpful!
LikeLike
I recently had to implement converting a JSON object array into an object array ARM template parameter. I found that the Newtonsoft deserialization did not work, and I instead converted the deserialized object array into an array of hash tables which worked:
function ConvertTo-HashTableArray
{
param (
[string] $JSONObjectArray
)
$DeserializedObjectArray = ConvertFrom-Json -InputObject $JSONObjectArray
$HashTableArray = @()
for ($i=0; $i -lt $DeserializedObjectArray.Count; $i++) {
$HashTable = @{}
foreach ($PropertyName in $DeserializedObjectArray[$i].PSObject.Properties.Name) {
$HashTable.Add($PropertyName, $DeserializedObjectArray[$i].$PropertyName)
}
$HashTableArray += $HashTable
}
return $HashTableArray
}
LikeLike