Shubham Jairath
Shubham Jairath

Reputation: 392

How to use nested loop in terraform

I am trying to create 3 VM's in Azure using Terraform count and each VM requires multiple disks. Is there any way to accomplish this ? I tried creating map of disk name and sizes but got an error that I can not use count and for_each together?

resource "azurerm_managed_disk" "this" {
  for_each             = var.disks
  count                = each.value > 0 ? var.node_count : 0
  name                 = format("%s-%02d-datadisk", each.key, count.index + 1)
  location             = var.location
  resource_group_name  = var.resource_group_name
  storage_account_type = "Premium_LRS"
  create_option        = "Empty"
  disk_size_gb         = each.value

  tags = {
    environment = "staging"
  }
 }  

resource "azurerm_virtual_machine_data_disk_attachment" "this" {
  for_each           = var.disks
  count              = each.value > 0 ? var.node_count : 0
  managed_disk_id    = azurerm_managed_disk.this.*.id[count.index]
  virtual_machine_id = azurerm_virtual_machine.this.*.id[count.index]
  lun                = "10"
  caching            = "None"
}

vm.tf
variable "disks" {
  description = "Map of disk name and respective disk size"
  type        = map(string)
  default     = {
    "binlog_disk"    = "30"
    "innodb_disk"    = "20"
    "data_disk"      = "100"
    "tmp_disk"       = "10"
    "backup_disk"    = "150"
  }
}

Upvotes: 2

Views: 5535

Answers (2)

Martin Atkins
Martin Atkins

Reputation: 74209

The key requirement for resource for_each is that you construct a map that has one element for each instance you want to create.

In this case, you want one instance for each (instance, disk) pair, which is a good use-case for the setproduct function. In this case, because one of your resources is using count, we'll combine it with the range function to produce a suitable sequence of integers to identify the instances:

locals {
  instance_disks = {
    for pair in setproduct(range(length(azurerm_virtual_machine.this)), keys(var.disks)) : "${pair[0]}:${pair[1]}" => {
      vm_index  = pair[0]
      disk_key  = pair[1]
      disk_size = var.disks[pair[1]]
    }
  }
}

The above will produce a map with one element per pair of virtual machine index and disk, with elements like this:

{
  "0:binlog_disk" = {
    vm_index  = 0
    disk_key  = "binlog_disk"
    disk_size = 30
  },
  "1:binlog_disk" = {
    vm_index  = 1
    disk_key  = "binlog_disk"
    disk_size = 30
  },
  etc...
}

This map meets the resource for_each requirement, so we can use it as the for_each expression in the azurerm_managed_disk and azurerm_virtual_machine_data_disk_attachment resources:

resource "azurerm_managed_disk" "this" {
  for_each = local.instance_disks

  name                 = format("%s-%02d-datadisk", each.value.disk_key, each.value.instance_index + 1)
  location             = var.location
  resource_group_name  = var.resource_group_name
  storage_account_type = "Premium_LRS"
  create_option        = "Empty"
  disk_size_gb         = each.value.disk_size

  tags = {
    environment = "staging"
  }
}

resource "azurerm_virtual_machine_data_disk_attachment" "this" {
  for_each = local.instance_disks

  managed_disk_id    = azurerm_managed_disk.this[each.key].id
  virtual_machine_id = azurerm_virtual_machine.this[each.value.instance_index].id
  lun                = "10"
  caching            = "None"
}

The addresses of the instances of these resources will use the same keys as the map, giving addresses like this:

  • azurerm_managed_disk.this["0:binlog_disk"]
  • azurerm_managed_disk.this["1:binlog_disk"]
  • ...
  • azurerm_managed_disk.this["0:innodb_disk"]
  • azurerm_managed_disk.this["1:innodb_disk"]
  • ...

It's important to think about the instance keys you're using for a resource because they decide how Terraform will understand changes to the underlying map. In this case in particular, because we're identifying the virtual machines by their positions in the count sequence, reducing the number of virtual machines by one would be understood by Terraform as a request to destroy all of the attachments which have the highest index. Removing the innodb_disk entry from var.disks would be understood by Terraform as a request to destroy all of the attachments whose addresses end with :innodb_disk.

Upvotes: 6

Sajeetharan
Sajeetharan

Reputation: 222582

This is possible with v0.12 and above. You can do something like this,

variable "disks" {
  default = [
    {
      unit_number = 0
      size = "80"
      thin_provisioned = true
    },
  ]
}

  dynamic "disk" {
    for_each = [for s in var.disks: {
      label = s.unit_number == "0" ? "${var.name}.vmdk" : "${var.name}_${s.unit_number}.vmdk"
      unit_number = s.unit_number
      size = s.size
      thin_provisioned = contains(keys(s),"thin_provisioned") ? s.thin_provisioned : "true"
    }]
    content {
      label = disk.value.label
      unit_number = disk.value.unit_number
      size = disk.value.size
      thin_provisioned = disk.value.thin_provisioned
    }
  }

Upvotes: 0

Related Questions