18

I'm deploying an ARM template that uses a copy resource block to deploy 1 or more data disks to a VM. What I'd like to do is change this to 0 or more.

The parameter I use is

    "VirtualMachineDiskSizeArray": {
        "type": "array",
        "defaultValue": [ "100" ]
    },

Which is then called in a resource:

   "resources": [
    {
        "name": "[parameters('virtualMachineName')]",
        "type": "Microsoft.Compute/virtualMachines",
        "apiVersion": "2016-04-30-preview",
        "location": "[parameters('rgLocation')]",
        "dependsOn": [
            "[concat('Microsoft.Storage/storageAccounts/', parameters('rgStorageAccountName'))]"
        ],
        "properties": {
            "osProfile": { ... },
            "hardwareProfile": { ... },
            "storageProfile": {
                "imageReference": { ... },
                "osDisk": { ... },
                "copy": [
                    {
                        "name": "dataDisks",
                        "count": "[length(parameters('VirtualMachineDiskSizeArray'))]",
                        "input": {
                            "lun": "[copyIndex('dataDisks')]",
                            "name": "[concat(parameters('vmDataDiskNameStub'), add(copyIndex('dataDisks'),1), '.vhd')]",
                            "diskSizeGB": "[parameters('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')]]",
                            "createOption": "Empty",
                            "vhd": {
                                "uri": "[concat(concat(reference(resourceId(parameters('rgName'), 'Microsoft.Storage/storageAccounts', parameters('rgStorageAccountName')), '2015-06-15').primaryEndpoints['blob'], 'vhds/'), concat(parameters('vmDataDiskNameStub'),  add(copyIndex('dataDisks'),1), '.vhd') )]"
                            }
                        }
                    }
                ]
            }
        }
    },

However, when I pass in an array of data disks with 0 elements, I get this error, as expected:

Validation returned the following errors:
: Deployment template validation failed: 'The template 'copy' definition at line '0' and column '0' has an invalid copy count. The co
py count must be a postive integer value and cannot exceed '800'. Please see https://aka.ms/arm-copy for usage details.'.

Template is invalid.

I would like to try to work around this somehow - I tried adding a condition on the copy:

"condition": "[  greater(length(parameters('VirtualMachineDiskSizeArray')), 0)]",

But that returned the same error.

I'm researching nested templates, but that doesn't look good for a section of a resource.

Christopher G. Lewis
  • 4,777
  • 1
  • 27
  • 46
  • Nested template example: http://marcvaneijk.com/2016/03/14/nested.html – Christopher G. Lewis Sep 04 '17 at 23:53
  • consider accepting this answer – 4c74356b41 Jan 26 '19 at 13:17
  • just FYI, you can use now 0 as copy count value. – Miq Nov 19 '20 at 09:27
  • @Miq the challenge wasn't with using a copy count of 0 - MS got that working a while ago. The challenge is when you are using a "DependsOn" with that resource - the template would fail if you depend on a resource that ended up executing 0 times. I haven't tested this in quite some time so it may be fixed. – Christopher G. Lewis Nov 20 '20 at 19:59
  • @ChristopherG.Lewis - oh, ok, sorry, I came here searching for answer if copy with 0 value works. I don't believe dependsOn will work - as you might want to depend on particular resource rather than on the copy group. However, a solution might be to use copy in a nested template and depend on that template, but it's just a thought, not tested. – Miq Nov 20 '20 at 21:15

7 Answers7

13

The easiest way to work around that is using this:

{
    "condition": "[if(equals(parameters('numberOfDataDisks'), 0), bool('false'), bool('true'))]",
    "apiVersion": "2017-03-30",
    "type": "Microsoft.Compute/virtualMachines",
    "name": "[variables('vmName')]",
    "location": "[resourceGroup().location]",
    "properties": {
        "storageProfile": {
            "imageReference": { xxx },
            "osDisk": { xxx },
            "copy": [
                {
                    "name": "dataDisks",
                    "count": "[if(equals(parameters('numberOfDataDisks'), 0), 1, parameters('numberOfDataDisks'))]",
                    "input": {
                        "diskSizeGB": "1023",
                        "lun": "[copyIndex('dataDisks')]",
                        "createOption": "Empty"
                    }
                }
            ]
        }
    }
}

this will work around the fact that you are passing 0 data disks and at the same time wont deploy this vm. all you need to do is add another vm resource. But it has to be with a different name (else template will fail) or you can use nested template to deploy a vm with the same name.

this can be improved with recent fixes to if() function, you can always work around by using nested deployments as well

4c74356b41
  • 69,186
  • 6
  • 100
  • 141
  • Yes, great thoughts. I was looking at this approach using `"condition": "[empty(parameters('VirtualMachineDiskSizeArray'))]"` but even when this condition is false my count section fails because `"diskSizeGB": "[parameters('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')]]",` fails with an empty index error. (Array used to pass different sized disks). This seems to be getting me *much* closer – Christopher G. Lewis Sep 05 '17 at 15:42
  • OK, I've isolated it down to this: `"diskSizeGB": "[if(empty(parameters('VirtualMachineDiskSizeArray')), 0 , parameters('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')] ) ]",` which fails with `Deployment template language expression evaluation failed: The language expression property array index '0' is out of bounds. ` . I'm not sure why the `false` side of the `If` is being evaluated on a `true` condition. – Christopher G. Lewis Sep 05 '17 at 15:58
  • that's part of the problem with the if statement in arm templates, nothing you can do about it. it evaluates both expressions. – 4c74356b41 Sep 05 '17 at 16:14
  • well, you better get used to it. that's what your life turns into when you spend a lot of time on arm templates ;) – 4c74356b41 Sep 05 '17 at 16:21
  • I've opened a support issue with MS regarding this, and hopefully will end up with an appropriate answer. Until then, I'm actually using the "interest of time" answer above. – Christopher G. Lewis Sep 12 '17 at 02:25
  • I've also raised this issue with product team. This is a bit late for you Christopher I'm sorry, but for other users with a similar problem this workaround can help: Construct variables and use those in the else branch of the 'if'. So for instance, something like this in the variables section: ``` "safeVMDiskSizeArray": "[if(empty(parameters('VirtualMachineDiskSizeArray')), array(0), parameters('VirtualMachineDiskSizeArray')) ] ``` That makes an array with one element (a zero) in it, if the input array is empty. Then use safeVMDiskSizeArray in the else branch of the existing code. – John Rusk - MSFT Dec 07 '17 at 23:53
  • is it possible to apply the same trick on the resource itself? and not a property on it? -- I couldn't :\ – johni Jan 21 '18 at 22:50
  • so if copy = 0 skip the resource? it seems like it should be. whats the error? @johni – 4c74356b41 Jan 22 '18 at 07:32
  • @4c74356b41 if you copy 0 times it get some error of index out of bound. the error comes from the `copy` part as it expects positive copy value – johni Jan 25 '18 at 20:21
  • well thats why you have `if` in the count parameter value – 4c74356b41 Jan 25 '18 at 20:23
  • @4c74356b41 right, but if I use the copy index to build the resource name (i.e.: `"name": "[concat('uploadStorageKey-', string(parameters('storageNames')[copyIndex()]))]",` - then I get index out of bounds, as the array 'storageNames' is empty. – johni Jan 26 '18 at 17:00
  • I guess you would need to do some nested deployments magic in this case – 4c74356b41 Jan 26 '18 at 18:20
3

So in the Interest of Time, I have changed my approach to this, but don't really like it...

I now have two deploy json files, VMDeploy.json and VMDeploy-NoDataDisks.json.

They are identical other than the storageProfile section of the VM resource:

VMDeploy.json:

"storageProfile": {
  "imageReference": { ... },
  "osDisk": { ... },
  "copy": [
    {
    "name": "dataDisks",
    "count": "[length(parameters('VirtualMachineDiskSizeArray'))]",
    "input": {
      "lun": "[copyIndex('dataDisks')]",
      "name": "[concat(parameters('vmDataDiskNameStub'), add(copyIndex('dataDisks'),1), '.vhd')]",
      "diskSizeGB": "[parameters('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')]]",
      "createOption": "Empty",
      "vhd": {
        "uri": "[concat(concat(reference(resourceId(parameters('rgName'), 'Microsoft.Storage/storageAccounts', parameters('rgStorageAccountName')), '2015-06-15').primaryEndpoints['blob'], 'vhds/'), concat(parameters('vmDataDiskNameStub'),  add(copyIndex('dataDisks'),1), '.vhd') )]"
        }
      }
    }
  ]
}

VMDeploy-NoDataDisks.json:

"storageProfile": {
  "imageReference": { ... },
  "osDisk": { ... },
  "dataDisks": []
}

And I have a Powershell block switches between the two json files:

if ($DriveArray.Count -eq 0) {
    $TemplateFile = $TemplateFile.Replace('.json','-NoDataDisks.json')
}
Christopher G. Lewis
  • 4,777
  • 1
  • 27
  • 46
3

I would like to share our solution based many of the answers already here. This is a simple example to have an input array of disks - values being their sizes - and create with those with your vm. works from 0-n. n needs to be smaller than supported disks of your VM size and other azure limits :) This should also help with this comment

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        [...]
        "dataDiskArray": {
            "type": "array",
            "defaultValue": [
                "100",
                "50"
            ]
        }
    },
    "variables": {
        "copy": [
            {
                "name": "dataDisks",
                "count": "[if(equals(length(parameters('dataDiskArray')),0),1, length(parameters('dataDiskArray')))]",
                "input": {
                    "lun": "[if(equals(length(parameters('dataDiskArray')),0),0, copyIndex('dataDisks'))]",
                    "createOption": "Empty",
                    "diskSizeGB": "[if(equals(length(parameters('dataDiskArray')),0),10,parameters('dataDiskArray')[copyIndex('dataDisks')])]"
                }
            }
        ]
    },
    "resources": [
        {
            "name": "[parameters('virtualMachineName')]",
            "type": "Microsoft.Compute/virtualMachines",
            [...]
            "properties": {
                [...]
                "storageProfile": {
                    [...]
                    "dataDisks": "[if(equals(length(parameters('dataDiskArray')),0),json('null'),variables('dataDisks'))]"
                },
                [...]
                }
            }
    ],
    "outputs": {
    }
}
Klaas
  • 31
  • 2
  • 1
    FINALLY Thank you, this was the correct way of doing it. A lot of "solutions" had the correct concept but what was lacking is the If condition in EACH, SINGLE value of the variable. The main roadblock, for me at least, was that I couldn't comprehend just how badly designed is this, starting with the impossiblity of using an empty array and followed by that the ARM template will check line by line trying to make sense out of every condition, even if they will never happen. – Dante Nahuel Ciai May 13 '20 at 22:57
1

I'm going to use this answer to use a reference on my research into nested templates.

Looking at here I can see an approach that would have two nested templates, one like this:

"resources": [
{
    "name": "MultiDataDisk",
    "type": "Microsoft.Compute/virtualMachines",
    "apiVersion": "2016-04-30-preview",
    "location": "[parameters('rgLocation')]",
    "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', parameters('rgStorageAccountName'))]"
    ],
    "properties": {
        "osProfile": { ... },
        "hardwareProfile": { ... },
        "storageProfile": {
            "imageReference": { ... },
            "osDisk": { ... },
            "copy": [
                {
                    "name": "dataDisks",
                    "count": "[length(parameters('VirtualMachineDiskSizeArray'))]",
                    "input": {
                        "lun": "[copyIndex('dataDisks')]",
                        "name": "[concat(parameters('vmDataDiskNameStub'), add(copyIndex('dataDisks'),1), '.vhd')]",
                        "diskSizeGB": "[parameters('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')]]",
                        "createOption": "Empty",
                        "vhd": {
                            "uri": "[concat(concat(reference(resourceId(parameters('rgName'), 'Microsoft.Storage/storageAccounts', parameters('rgStorageAccountName')), '2015-06-15').primaryEndpoints['blob'], 'vhds/'), concat(parameters('vmDataDiskNameStub'),  add(copyIndex('dataDisks'),1), '.vhd') )]"
                        }
                    }
                }
            ]
        }
    }
}
]

and one like this:

"resources": [
{
    "name": "ZeroDataDisk",
    "type": "Microsoft.Compute/virtualMachines",
    "apiVersion": "2016-04-30-preview",
    "location": "[parameters('rgLocation')]",
    "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', parameters('rgStorageAccountName'))]"
    ],
    "properties": {
        "osProfile": { ... },
        "hardwareProfile": { ... },
        "storageProfile": {
            "imageReference": { ... },
            "osDisk": { ... },
             "dataDisks": []
        }
    }
}
] 

And reference them from a parent template:

"parameters": {
 "nestedType": {
  "type": "string",
  "defaultValue": "ZeroDataDisk",
  "allowedValues": [
   "ZeroDataDisk",
   "MultiDataDisk"
  ],
 }
},
"resources": [
 {
 "name": "[concat("nested-",parameters('virtualMachineName')]",
 "type": "Microsoft.Resources/deployments",
 "apiVersion": "2015-01-01",
 "properties": {
  "mode": "Incremental",
  "templateLink": {
   "uri": "[concat('https://someplace.on.the.internet/nested/',parameter('nestedType'),".json")],
   "contentVersion": "1.0.0.0"
   },
   "parameters": {
    "rgStorageAccountName": {
     "value": "[parameters(rgStorageAccountName)]"
    },
    "OtherParms": {
     "value": "[parameters('otherParms')]"
    }
     . . .
  }
 }
]
}

However, I don't believe this is better/easier than what I did in my "Interest of Time" answer because

  1. The section that I'm interested in (dataDisks) is surrounded by a bunch of other json that doesn't change leaving me to the same crappy issue of having to manually sync code between two files.
  2. I now have to not only maintain two json files for the nested resources, but I have to publish them via a publicly accessible URL.

So basically, unless I can have a copy of 0-N of a section (rather than 1-N) or nest just a section, my two file hack with the powershell switch seems to be the least overhead.

Christopher G. Lewis
  • 4,777
  • 1
  • 27
  • 46
1

Regarding the Disksize I can help you out. What I have actually done is reference to a JSON template file where the size, name, and caching is defined for disk creation.

These can easily be parsed to the deployment

"storageProfile": {
      "copy": [{
        "name": "dataDisks",
        "count": "[length(parameters('dataDiskArray'))]",
        "input": {
          "name": "[concat(parameters('vmName'), if(less(copyindex(1), 10), concat('0', copyindex(1)), concat(copyindex(1))), '-datadisk-', parameters('dataDiskArray')[copyIndex('dataDisks')].name)]",
          "diskSizeGB": "[parameters('dataDiskArray')[copyIndex('dataDisks')].size]",
          "caching": "[parameters('dataDiskArray')[copyIndex('dataDisks')].cache]",
          "lun": "[copyIndex('dataDisks')]",
          "createOption": "Empty"
        }
      }],

The issue remains that is part of the VM deployment, so there is no option to select a Null value if you want to deploy a VM without extra disks.

I have tried to move the disk creation to the variables section of the template, but than it is not possible to use the VMName as part of the name of the disks you want to create. This is because the copy loop for the VM is not yet initialized, so It will give the wrong name.

  • My problem is that if your `dataDiskArray` has a lengh of 0, the `"diskSizeGB": "[parameters('dataDiskArray')[copyIndex('dataDisks')].size]"` line throws an error because of the null sized array. – Christopher G. Lewis May 29 '18 at 23:28
1

The workaround I've used for myself doesn't need linked templates or complicated logic; it's based on this:

If the array is empty, create another array with only one item in it with the value of "EMPTY_ARRAY_INDICATOR".

Also, in my copy loop, I have added this condition: "condition": "[not(equals(variables('input')[0]), "EMPTY_ARRAY_INDICATOR")]" so that it doesn't deploy when the first element of the array is "EMPTY_ARRAY_INDICATOR".

Having a look at your template, I guess I can rewrite it this way:

   "parameters": [
      "VirtualMachineDiskSizeArray": {
        "type": "array",
        "defaultValue": []
      }
   ],
   "variables": [
      "VirtualMachineDiskSizeArray": "[if(empty(parameters('VirtualMachineDiskSizeArray')), array('EMPTY_ARRAY_INDICATOR'), parameters('VirtualMachineDiskSizeArray'))]"
   ],
   "resources": [
    {
        "name": "[parameters('virtualMachineName')]",
        "type": "Microsoft.Compute/virtualMachines",
        "apiVersion": "2016-04-30-preview",
        "location": "[parameters('rgLocation')]",
        "dependsOn": [
            "[concat('Microsoft.Storage/storageAccounts/', parameters('rgStorageAccountName'))]"
        ],
        "properties": {
            "osProfile": { ... },
            "hardwareProfile": { ... },
            "storageProfile": {
                "imageReference": { ... },
                "osDisk": { ... },
                "copy": [
                    {
                        "name": "dataDisks",
                        "condition": "[not(equals(variables('VirtualMachineDiskSizeArray')[0], 'EMPTY_ARRAY_INDICATOR'))]"
                        "count": "[length(variables('VirtualMachineDiskSizeArray'))]",
                        "input": {
                            "lun": "[copyIndex('dataDisks')]",
                            "name": "[concat(parameters('vmDataDiskNameStub'), add(copyIndex('dataDisks'),1), '.vhd')]",
                            "diskSizeGB": "[variables('VirtualMachineDiskSizeArray')[copyIndex('dataDisks')]]",
                            "createOption": "Empty",
                            "vhd": {
                                "uri": "[concat(concat(reference(resourceId(parameters('rgName'), 'Microsoft.Storage/storageAccounts', parameters('rgStorageAccountName')), '2015-06-15').primaryEndpoints['blob'], 'vhds/'), concat(parameters('vmDataDiskNameStub'),  add(copyIndex('dataDisks'),1), '.vhd') )]"
                            }
                        }
                    }
                ]
            }
        }
    },

Please note the condition in the copy loop, and the usage of variables('VirtualMachineDiskSizeArray') instead of parameters(VirtualMachineDiskSizeArray).

I have tested it for my own template, but haven't run your template. Please let me know if it works or not.

hosjay
  • 901
  • 2
  • 7
  • 19
0

Working sample https://github.com/mariuszdotnet/azure-resource-manager-tutorial/blob/master/azuredeploy.json

Here is the summary:

  • Move the Disks into a variable and use variable iteration.
  • If numDisks = 0 then diskLoop = 1 (this prevents an error in variable copy)
  • Create variable copy object with count = diskLoop
"copy": [
  {
      "name": "dataDisks",
      "count": "[if(equals(parameters('numberOfDataDisks'),0),1, parameters('numberOfDataDisks'))]",
      "input": {
        "lun": "[copyIndex('dataDisks')]",
        "createOption": "Empty",
        "diskSizeGB": "1023"
      }
  }   ]
  • On the VM Resource for data disks.
  • On the VM resource, if numDisk=0 then datadisks = json(‘null’) else datadisks = variable
"dataDisks": 
                "[if(equals(parameters('numberOfDataDisks'),0),json('null'),variables('dataDisks'))]"

            },
  • My issue is that I need the disk size in an array of variable disk sizes. For example - D:1024,E:2048,F:512. Anything I do ends up referencing the array with a 0 index. so while the above simple case works, as soon as you need different disk sizes it doesn't. – Christopher G. Lewis Feb 06 '18 at 14:24