Azure, how to build a reusable multi-data disks VM ARM template
Tags: azure, devops, arm, iac
Published: 2020-08-15 16:39:22

One difficulty you need to solve when you write an ARM template is to make them reusable. A template can be used for several kinds of environments. More you also want to recycle part of your template as you don't need to reinvent everything each time you need to deploy something.
One thing that can be seen difficult in this perspective is VM with multiple data disks. VMs can have zero to many data disks. Each disk may have their options (size, SKU, encryption options, …) and when you attach them you may have to specify caching and/or write acceleration options.
Creating a reusable template means you have to solve several problems.

* How to represent disk creation and attach options
* How to create these disks
* How to attach disks without know in advance the number of them

## How to represent disks

To represent all the data disks options you want to create, you need to use an array of objects representing the desired state of drives you want to attach to the VM. For the demonstration, I will only use two parameters for the VDH creation, size and SKU, and two for attaching the VHD, LUN, and cache setting.
The parameters should look like 

```json
       "dataDisksArray": {
            "value": [
                {
                    "name": "vhd1",
                    "size": 32,
                    "diskSku": "Premium_LRS",
                    "lun": 0
                },
                {
                    "name": "vhd2",
                    "size": 32,
                    "diskSku": "Premium_LRS",
                    "lun": 1
                },
                {
                    "name": "vhd3",
                    "size": 32,
                    "diskSku": "Premium_LRS",
                    "lun": 1
                }
            ]
        }
```

## How to create disk

Now that you have this array you can build disks in your template by using copy. In the example above we need to create 3 disks, vhd1, vhd2, vhd3, with 32 Gib size each and the same SKU.

Copy let you iterate the array of object in parameter against a resource. ARM will create these resources in parallel by default, but the order cannot be guaranteed. It takes two parameters, a name for the iteration and the count. 
For the last parameter, you need to use the length or number of objects of the parameter dataDiskArray. 

```json
            "copy": {
                "name": "datacreation",
                "count": "[length(parameters('dataDisksArray'))]"
            },
```
How to access object properties in the loop? You need to use copyindex() to get the data in the current iteration. 

```json
[parameters('dataDisksArray')[copyIndex('datadisk')].name]
```

```json
         {
            "name": "[parameters('dataDisksArray')[copyIndex('diskcreation')].name]", 
            "type": "Microsoft.Compute/disks",
            "apiVersion": "2019-07-01",
            "location": "[resourceGroup().location]",
            "copy": {
                "name": "diskcreation",
                "count": "[length(parameters('dataDisksArray'))]"
            },
            "properties": {
                        "diskSizeGB": "[parameters('dataDisksArray')[copyIndex('diskcreation')].size]",
                        "creationData": {
                            "createOption": "empty"
                        }
                    },
                "sku": {
                "name": "[parameters('dataDisksArray')[copyIndex('diskcreation')].diskSku]"
            }
        }
``` 

## how to attach disks to the VM

First things you need to be sure that disks are present. If not, the template deployment may fail because of the resource creation order, disks may be created after the VM. To control the order, you need to use the dependsOn property. 
The dependsOn property takes an array of resource names. You can use the 3 names, vhd1, vhd2, vhd3 but it will not be a reusable template.
You cannot use a copy inside the dependsOn. You may try to create the JSON inside the variable section, but it will not work. Instead, you can simply use the name of the copy used in the disk creation.

```json
          "dependsOn":[
              "diskcreation",
              "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
          ],
```

That all for the dependsOn property

Now that you set up the order of resources creation and make sure disks exist before creating the VM you can attach the disk to the VM. 
The Datadisks property in the storageProfile object allows you to use the copy keyword to attach disks.  

```json
                "storageProfile": {
                    "imageReference": {
                        "publisher": "MicrosoftWindowsServer",
                        "offer": "WindowsServer",
                        "sku": "2019-Datacenter",
                        "version": "latest"
                    },
                    "osDisk": {
                        "name": "[concat(parameters('vmName'),'-os.vhd')]",
                        "caching": "ReadWrite",
                        "createOption": "FromImage"
                    },
                    "copy": [
                        {
                            "name": "dataDisks",
                            "count": "[length(parameters('dataDisksArray'))]",
                            "input": {
                                "name": "[parameters('dataDisksArray')[copyIndex('dataDisks')].name]",
                                "createOption": "attach",
                                "caching": "ReadOnly",
                                "lun": "[parameters('dataDisksArray')[copyIndex('dataDisks')].lun]",
                                "managedDisk": {
                                    "id": "[resourceId('Microsoft.Compute/disks',parameters('dataDisksArray')[copyIndex('dataDisks')].name)]"
                                }
                            }
                        }
                    ]
                }
```

But imagine another situation where you need to create a variable number of disks and one or more fixed disk. For example, you need to build a SQL Server VM where data are stored on a storage pool with x disks depending on the environment and one disk for the SQL Log files. 

You cannot do it into the storageProfile object. The solution is a little contra intuitive. You cannot do it inside the resource section of the template. As the dataDisks property needs a JSON object you can create it in the variable section.


As in the storageProfile in the previous example, you need to use the copy keyword with the input keyword.

```json
        "copy": [
            {
                "name": "diskAttach",
                "count": "[length(parameters('dataDisksArray'))]",  
                "input": {
                    "name": "[parameters('dataDisksArray')[copyIndex('diskAttach')].name]",
                    "createOption": "attach",
                    "caching": "ReadOnly",
                    "lun": "[parameters('dataDisksArray')[copyIndex('diskAttach')].lun]",
                    "managedDisk": {
                        "id": "[resourceId('Microsoft.Compute/disks',parameters('dataDisksArray')[copyIndex('diskAttach')].name)]"
                    }
                }
            }
         ]
``` 

This will create an array named diskAttach. It’s the same as the previous example. Now you need to add the Log disk to this variable using the union function. This function adds two values of the same type. So first you will need to create a new array with the object for your log disk. 

```json
        "logDiskAttachObject": [
            {
                "name": "[concat(parameters('vmName'),'-log-vhd')]",
                "createOption": "attach",
                "caching": "none",
                "lun": "[add(parameters('dataDisksArray'),1)]",
                "managedDisk": {
                    "id": "[resourceId('Microsoft.Compute/disks',concat(parameters('vmName'),'-log-vhd'))]"
                }
            }
        ]
``` 

And then use the union function to merge the two arrays. 

```json
"diskattachAll": "[union(variables('diskAttach'), variables('logDiskAttachObject'))]"
``` 

Now the storageProfile of the VM resource look like 

```json
                "storageProfile": {
                    "imageReference": {
                        "publisher": "MicrosoftWindowsServer",
                        "offer": "WindowsServer",
                        "sku": "2019-Datacenter",
                        "version": "latest"
                    },
                    "osDisk": {
                        "name": "[concat(parameters('vmName'),'-os.vhd')]",
                        "caching": "ReadWrite",
                        "createOption": "FromImage"
                    },
                    "dataDisks": "[variables('diskattachAll')]"
                    /* 
                    "copy": [
                        {
                            "name": "dataDisks",
                            "count": "[length(parameters('dataDisksArray'))]",
                            "input": {
                                "name": "[parameters('dataDisksArray')[copyIndex('dataDisks')].name]",
                                "createOption": "attach",
                                "caching": "ReadOnly",
                                "lun": "[parameters('dataDisksArray')[copyIndex('dataDisks')].lun]",
                                "managedDisk": {
                                    "id": "[resourceId('Microsoft.Compute/disks',parameters('dataDisksArray')[copyIndex('dataDisks')].name)]"
                                }
                            }
                        }
                    ]*/
                }
```

Making reusable ARM Template needs a bit of creativity but as you can see you can do whatever you need to deploy your infrastructure with few options.