Working with arrays of objects and multiple resource instances in Azure ARM templates

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:

  1. The parameters section: remove individual parameters and replace it with a single ‘array’ type.
  2. Add the ‘copy’ property to resource we want to duplicate.
    1. Note that the ‘name’ property here is an arbitrary string, just give it a friendly name.
    2. 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.
  3. 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):

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s