Access an object inside an object in Terraform data block

65 views Asked by At

I'm hoping you'd be able to assist me.

I've got a Terraform pipeline set up with the hasicorp/vsphere provider and I'm trying to create a virtual machine with 2 NICs from a tfvars file. I'm trying to make a module out of the vsphere_virtual_machine resource and all I'm getting are errors

I've got my tfvars file formatted as follows

Terraform\VMWare\NewBuildCluster\vmware_server_build.tfvars

tfvars = {

  # Core vSphere Vars
  vsphere_server            = "vcentre.example.com" # Address of vCentre
  vsphere_datacenter        = "vsphere_datacenter" # Location to build new servers
  vsphere_datastore         = "datastore0001" # Data store to save VM files
  vsphere_cluster           = "NewBuildCluster" # Cluster to build on

  #VM Related Vars
  vm = {
    TestVM01 = {
      vsphere_vm_template             = "WindowsTemplate2022" # Name of template
      vsphere_vm_name                 = "TestVM01" # Name of VM
      vsphere_vm_cpu_cores            = 4 # Total cores
      vsphere_vm_num_cores_per_socket = 2 # Cores per socket
      vsphere_vm_memory               = 8 # RAM assignment (Math conducted in module for MB conversion)

      #VM Customisation Variables
      vm_nics = {
        eth0 = {
          vsphere_vm_network        = "VLAN10" # Name of Virtual Network (defined in vSphere)
          vsphere_vm_domain         = "ad.example.com" # Domain to join server to once built
          vsphere_vm_ip             = "10.200.10.240" # Static assigned IP address
          vsphere_vm_ip_gateway     = "10.200.10.1" # Gateway IP Address
          vsphere_vm_ip_dnslist     = ["10.200.10.10", "10.200.10.30"]  # List of DNS addresses
        },
        eth1 = {
          vsphere_vm_network        = "VLAN10" # Name of Virtual Network (defined in vSphere)
          vsphere_vm_domain         = "ad.example.com" # Domain to join server to once built
          vsphere_vm_ip             = "10.200.10.241" # Static assigned IP address
          vsphere_vm_ip_gateway     = "10.200.10.1" # Gateway IP Address
          vsphere_vm_ip_dnslist     = ["10.200.10.10", "10.200.10.30"]  # List of DNS addresses
        }
      }

      data_disk = {
        disk1 = {
          size_gb = 50 # Data disk size
        },
        disk2 = {
          size_gb = 10 # Data disk size
        }
      }
      
    }
}

And my network data block is like this but I can't access the vm_nics as a whole.

Pipeline\VMWare\main.tf

data "vsphere_virtual_machine" "network" {
  for_each = flatten([
    for vm_key, vm_value in var.tfvars.vm : [
      for nic_key, nic_value in vm_value.vm_nics : {
        name         = nic_value.vsphere_vm_network
        datacenter_id = data.vsphere_datacenter.datacenter.id
      }
    ]
  ])

  name          = each.value.name
  datacenter_id = each.value.datacenter_id
}

This has brought me as close as I can get to the variable but I get this error

│ The given "for_each" argument value is unsuitable: the "for_each" argument
│ must be a map, or set of strings, and you have provided a value of type
│ tuple.
2

There are 2 answers

2
Harambo On BEST ANSWER

I'd advice you to create a child module to handle the creation of one VM and just loop that module instead.

module "my_module" {
  source = "./modules/my_module"
  # Deploy this module for each configured VM
  for_each = var.tfvars.vm
  # Pass VM confiuration (properties inside the VM object)
  vm_property = each.value.vm_property
  # Pass shared properties (properties not in the VM object)
  shared_property = var.tfvars.shared_property
  # Pass the NIC configuration for this VM only
  vm_nics = each.value.vm_nics
}

Then in your module you could do this:

variable "vm_nics" {
  # Can use map(any) but I prefer explicit declaration
  type = map(object({
    # Declare the expected properties in here
  })
}

data "vsphere_virtual_machine" "network" {
  # Gives you each NIC object for the VM
  for_each = var.vm_nics

  name          = each.value.vsphere_vm_network
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Outputs a list of all IDs
output "ids" {
  value = data.vsphere_virtual_machine.network.*.id
}

Though I'm a bit confused about this setup, perhaps the vsphere_virtual_machine should be a vsphere_network?

Side note, if you want to you could limit the module to only getting the NIC IDs for each VM. In that case you don't need to declare the VM property variables, just give it the list of NICs for each VM and then you can access them like:

# In the main module, for example when creating the VMs
resource "some_resource" "this" {
  for_each = var.tfvars.vm
  network_interfaces = module.my_module[each.key].ids
}
0
Helder Sepulveda On

Yes the for_each argument is incorrect...

We could provide a set of strings (using toset) and change it to this:

  for_each = toset(flatten([
    for vm_key, vm_value in var.tfvars.vm : [
      for nic_key, nic_value in vm_value.vm_nics : nic_value.vsphere_vm_network
    ]
  ]))

I see no need to include the datacenter_id = data.vsphere_datacenter.datacenter.id
that could be considered a constant, I see no dependency on that resource and the tfvars, there is no need for it to be inside that loop.


Here is a full example

variable "tfvars" {
  type = any
  default = {
    vm = {
      TestVM01 = {
        vm_nics = {
          eth0 = {
            vsphere_vm_network = "VLAN10"
          },
          eth1 = {
            vsphere_vm_network = "VLAN10"
          }
        }
      }
    }
  }
}

resource "null_resource" "tion" {
  for_each = toset(flatten([
    for vm_key, vm_value in var.tfvars.vm : [
      for nic_key, nic_value in vm_value.vm_nics : nic_value.vsphere_vm_network
    ]
  ]))

  provisioner "local-exec" {
    when    = create
    command = "echo ${each.value.name}"
  }
}

a terraform plan on that will look like:

Terraform will perform the following actions:

  # null_resource.tion["VLAN10"] will be created
  + resource "null_resource" "tion" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.