Terraform - Azure Multiple VM Managed Disks
Published: 2020-06-17 11:41:08

---
title: Terraform - Azure Multiple VM Managed Disks
published: true
date: 2020-05-17 19:55:28 UTC
tags: 
canonical_url: https://www.nealshah.dev/posts/2020/05/terraform-azure-multiple-vm-managed-disks/
---

## Intro

Terraform has come a long way since last year, v0.12.25 provides some new features and stability improvements that makes the developer experience far more friendly.

I recently came across an old module that I had developed on v0.11.7 which deploys Linux (Ubuntu) virtual machines on Azure, unfortunately not usable now as it requires a whole lot of refactoring. During the porting process, I discovered some naivety on my part and I’d like to share my experience.

Future-proofing the script means refactoring resources like `azurerm_virtual_machine`, as per the [docs](https://www.terraform.io/docs/providers/azurerm/r/virtual_machine.html):

> Note: The `azurerm_virtual_machine` resource has been superseded by the `azurerm_linux_virtual_machine` and `azurerm_windows_virtual_machine resources`. The existing `azurerm_virtual_machine` resource will continue to be available throughout the 2.x releases however is in a feature-frozen state to maintain compatibility - new functionality will instead be added to the `azurerm_linux_virtual_machine` and `azurerm_windows_virtual_machine resources`.

## Prerequisites

In this post we are using the following versions:

```hcl
terraform {
    required_version = ">= 0.12.25"
    required_providers {
        azurerm = ">= 2.10.0"
    }
}
```

## Deploying Multiple VMs / Multiple Data Disks

Using `count` was the obvious answer (at first!) because of its easy-to-use nature. I’m passing in a list of VMs for somefine grained control of what is deployed/destroyed. For example:

`terraform.tfvars`

```hcl
resource_group_name = "my-test-rg"
instances = ["vm-test-1", "vm-test-2", "vm-test-3"]
nb_disks_per_instance = 2
tags = {
    environment = "test"
}
```

`main.tf`

```hcl
resource "azurerm_linux_virtual_machine" "vm" {
    count = length(var.instances)
    name = element(var.instances, count.index)
    resource_group_name = azurerm_resource_group.rg.name
    location = azurerm_resource_group.rg.location
    size = "Standard_D2s_v3"
    network_interface_ids = [element(azurerm_network_interface.nic.*.id, count.index)]
    admin_username = "adminuser"
    admin_password = "Password1234!@"
    disable_password_authentication = false
    os_disk {
        name = "osdisk-${element(var.instances, count.index)}-${count.index}"
        caching = "ReadWrite"
        storage_account_type = "Standard_LRS"
    }
    source_image_reference {
        publisher = "Canonical"
        offer = "UbuntuServer"
        sku = "18.04-LTS"
        version = "latest"
    }
    tags = var.tags
}
resource "azurerm_managed_disk" "managed_disk" {
    count = length(var.instances) * var.nb_disks_per_instance
    name = element(var.instances, count.index)
    location = azurerm_resource_group.rg.location
    resource_group_name = azurerm_resource_group.rg.name
    storage_account_type = "Standard_LRS"
    create_option = "Empty"
    disk_size_gb = 10
    tags = var.tags
}
resource "azurerm_virtual_machine_data_disk_attachment" "managed_disk_attach" {
    count = length(var.instances) * var.nb_disks_per_instance
    managed_disk_id = azurerm_managed_disk.managed_disk.*.id[count.index]
    virtual_machine_id = azurerm_linux_virtual_machine.vm.*.id[ceil((count.index + 1) * 1.0 / var.nb_disks_per_instance) - 1]
    lun = count.index + 10
    caching = "ReadWrite"
}
```

Great, so I can deploy 3 VMs, each with 2 data disks! Awesome right? WRONG!

### Problems

What if we remove `vm-test-2` from the list of `instances`?

- **Problem 1:** Pretty much all of the disks have to be amended. With managed disks, this mean replacements are forced which is probably not very healthy.

- **Problem 2:** Each managed disk attachment for each VM requires a unique LUN (Logical Unit Number) - although we did achievethis in the script above (6 data disks, with LUNs [10, 11, 12, 13, 14, 15]), any change to this number forces a new resource created. This is inefficient.

- **Problem 3:** It doesn’t achieve what I want it to do.

## A Better Solution

After endlessly searching for solutions, I mashed up a few and found a way using `for_each`, `for` and `locals`.

`for_each` and `for` were [introduced in v0.12](https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each/) of Terraform, it has provided a much need breath of fresh air!

I’ve abstracted away the evolved resources as it’s pretty straight forward to do - all you need to do is remove the references to count and replace with `for_each` at the top of each resource and iterate over `var.instances`.As this is a list, you would need to [use `toset()` to convert its type](https://www.terraform.io/docs/configuration/functions/toset.html).

Introducing `locals` at the top of `main.tf` as follows:

```hcl
locals {
    vm_datadiskdisk_count_map = { for k in toset(var.instances) : k => var.nb_disks_per_instance }
    luns = { for k in local.datadisk_lun_map : k.datadisk_name => k.lun }
    datadisk_lun_map = flatten([
        for vm_name, count in local.vm_datadiskdisk_count_map : [
            for i in range(count) : {
                datadisk_name = format("datadisk_%s_disk%02d", vm_name, i)
            lun = i
            }
        ]
    ])
}
```

This is like a temporary store to evaluate expressions before being used in resources. For example, the expression for `luns`above is looping through the `datadisk_lun_map`, and creating a key / value pair for each key. We can see the result using`terraform console` (you reference `locals` in resources as `local.variable`:

```bash
➜ terraform console
> local.luns
{
    "datadisk_vm-test-1_disk00" = 0
    "datadisk_vm-test-1_disk01" = 1
    "datadisk_vm-test-2_disk00" = 0
    "datadisk_vm-test-2_disk01" = 1
    "datadisk_vm-test-3_disk00" = 0
    "datadisk_vm-test-3_disk01" = 1
}
```

So now we can use these `locals` in our resources - here are the `managed_disk` and `manage_disk_attach` blocks:

```hcl
resource "azurerm_managed_disk" "managed_disk" {
    for_each = toset([for j in local.datadisk_lun_map : j.datadisk_name])
    name = each.key
    location = azurerm_resource_group.rg.location
    resource_group_name = azurerm_resource_group.rg.name
    storage_account_type = "Standard_LRS"
    create_option = "Empty"
    disk_size_gb = 10
    tags = var.tags
}
resource "azurerm_virtual_machine_data_disk_attachment" "managed_disk_attach" {
    for_each = toset([for j in local.datadisk_lun_map : j.datadisk_name])
    managed_disk_id = azurerm_managed_disk.managed_disk[each.key].id
    virtual_machine_id = azurerm_linux_virtual_machine.vm[element(split("_", each.key), 1)].id
    lun = lookup(local.luns, each.key)
    caching = "ReadWrite"
}
```

## Results

When removing `vm-test-2` from `instances`, our plan shows that ONLY the resources related to this particular machine are destroyed. No unnecessary updates or forced replacements are carried out.

```bash
➜ terraform plan
azurerm_linux_virtual_machine.vm["vm-test-2"] will be destroyed
...
azurerm_managed_disk.managed_disk["datadisk_vm-test-2_disk00"] will be destroyed
...
azurerm_managed_disk.managed_disk["datadisk_vm-test-2_disk01"] will be destroyed
...
azurerm_network_interface.nic["vm-test-2"] will be destroyed
...
azurerm_virtual_machine_data_disk_attachment.managed_disk_attach["datadisk_vm-test-2_disk00"]
...
azurerm_virtual_machine_data_disk_attachment.managed_disk_attach["datadisk_vm-test-2_disk01"]
...
Plan: 0 to add, 0 to change, 6 to destroy.
```