How to pass output from child module to root

I have am trying to pass output from a child module to the root module within the following output block which is contained in my main.tf in root module.

output "ansible_inventory" {
  value = <<-EOF
        ${module.vsphere_virtual_machine_s4hana.ip} ansible_host=
        [all:vars]
        ansible_user=ansadmin
        EOF
}

The plan is to execute terraform output ansible_inventory once apply is complete to have a populated inventory for ansible.

I receive the following error

 Error: Unsupported attribute
│
│   on main.tf line 158, in output "ansible_inventory":
│  158:         ${module.vsphere_virtual_machine_s4hana.ip} ansible_host=
│     ├────────────────
│     │ module.vsphere_virtual_machine_s4hana is a list of object, known only after apply
│
│ Can't access attributes on a list of objects. Did you mean to access attribute "ip" for a specific element of the list, or across all elements of the list?

Layout of files

$ tree
.
├── main.tf
├── modules
│   └── terraform-vsphere-vm
│       ├── CODE_OF_CONDUCT.md
│       ├── CONTRIBUTING.md
│       ├── LICENSE
│       ├── README.md
│       ├── main.tf
│       ├── output.tf
│       ├── variables.tf
│       └── versions.tf
├── provider.tf
├── terraform.auto.tfvars
├── terraform.tfstate
├── terraform.tfstate.backup
└── variables.tf

The root main.tf is as below

root
|
main.tf

module "vsphere_virtual_machine_s4hana" {
  source            = "./modules/terraform-vsphere-vm/"
  count             = var.create_s4hana_vm ? 1 : 0
  vmname            = "${lower(var.vmnameprefix)}${lower(var.s4hana_sap_sid)}"
  instances         = var.s4hana_instances
  dc                = var.dc
  vmrp              = var.vmrp
  ram_size          = var.s4hana_ram_size
  cpu_number        = var.s4hana_cpu_number
  datastore         = var.datastore
  vmtemp            = var.vmtemp
  domain            = var.domain
  network           = var.s4hana_network
  data_disk = {
    disk1 = {
      size_gb                   = var.s4hana_disks_usr_sap_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 1,
    },
    disk2 = {
      size_gb                   = var.s4hana_disks_sapmnt_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 1,
    },
    disk3 = {
      size_gb                   = var.s4hana_disks_trans_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 1,
    },
    disk4 = {
      size_gb                   = var.s4hana_disks_shared_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 1,
    },
    disk5 = {
      size_gb                   = var.s4hana_disks_data_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 2,
    },
    disk6 = {
      size_gb                   = var.s4hana_disks_log_storage_size,
      thin_provisioned          = true,
      eagerly_scrub             = false,
      data_disk_scsi_controller = 3,
    },
  }
  scsi_controller  = 0                 // This will assign OS disk to controller 0
  dns_server_list  = var.dns_server_list
  vmgateway        = var.vmgateway
  is_windows_image = false
  enable_disk_uuid = true
}

output "ansible_inventory" {
  value = <<-EOF
        ${module.vsphere_virtual_machine_s4hana.ip} ansible_host=
        [all:vars]
        ansible_user=ansadmin
        EOF
}

child module terraform-vsphere-vm
main.tf

resource "vsphere_virtual_machine" "vm" {
  count      = var.instances
  depends_on = [var.vm_depends_on]
  name       = var.staticvmname != null ? var.staticvmname : format("${var.vmname}${var.vmnameformat}", count.index + 1)

  resource_pool_id        = data.vsphere_resource_pool.pool.id
  folder                  = var.vmfolder
  tags                    = var.tag_ids != null ? var.tag_ids : data.vsphere_tag.tag[*].id
  custom_attributes       = var.custom_attributes
  annotation              = var.annotation
  extra_config            = var.extra_config
  firmware                = var.content_library == null && var.firmware == null ? data.vsphere_virtual_machine.template[0].firmware : var.firmware
  efi_secure_boot_enabled = var.content_library == null && var.efi_secure_boot == null ? data.vsphere_virtual_machine.template[0].efi_secure_boot_enabled : var.efi_secure_boot
  enable_disk_uuid        = var.content_library == null && var.enable_disk_uuid == null ? data.vsphere_virtual_machine.template[0].enable_disk_uuid : var.enable_disk_uuid
  storage_policy_id       = var.storage_policy_id

  datastore_cluster_id = var.datastore_cluster != "" ? data.vsphere_datastore_cluster.datastore_cluster[0].id : null
  datastore_id         = var.datastore != "" ? data.vsphere_datastore.datastore[0].id : null

  num_cpus               = var.cpu_number
  num_cores_per_socket   = var.num_cores_per_socket
  cpu_hot_add_enabled    = var.cpu_hot_add_enabled
  cpu_hot_remove_enabled = var.cpu_hot_remove_enabled
  cpu_reservation        = var.cpu_reservation
  cpu_share_level        = var.cpu_share_level
  cpu_share_count        = var.cpu_share_level == "custom" ? var.cpu_share_count : null
  memory_reservation     = var.memory_reservation
  memory                 = var.ram_size
  memory_hot_add_enabled = var.memory_hot_add_enabled
  memory_share_level     = var.memory_share_level
  memory_share_count     = var.memory_share_level == "custom" ? var.memory_share_count : null
  guest_id               = var.content_library == null ? data.vsphere_virtual_machine.template[0].guest_id : null
  scsi_bus_sharing       = var.scsi_bus_sharing
  scsi_type              = var.scsi_type != "" ? var.scsi_type : (var.content_library == null ? data.vsphere_virtual_machine.template[0].scsi_type : null)
  scsi_controller_count = max(
    max(0, flatten([
      for item in values(var.data_disk) : [
        for elem, val in item :
        elem == "data_disk_scsi_controller" ? val : 0
    ]])...) + 1,
    ceil((max(0, flatten([
      for item in values(var.data_disk) : [
        for elem, val in item :
        elem == "unit_number" ? val : 0
    ]])...) + 1) / 15),
  var.scsi_controller)
  wait_for_guest_net_routable = var.wait_for_guest_net_routable
  wait_for_guest_ip_timeout   = var.wait_for_guest_ip_timeout
  wait_for_guest_net_timeout  = var.wait_for_guest_net_timeout

child module output
output.tf

output "DC_ID" {
  description = "id of vSphere Datacenter"
  value       = data.vsphere_datacenter.dc.id
}

output "ResPool_ID" {
  description = "Resource Pool id"
  value       = data.vsphere_resource_pool.pool.id
}

output "VM" {
  description = "VM Names"
  value       = vsphere_virtual_machine.vm.*.name
}

output "ip" {
  description = "default ip address of the deployed VM"
  value       = vsphere_virtual_machine.vm.*.default_ip_address
}

output "guest-ip" {
  description = "all the registered ip address of the VM"
  value       = vsphere_virtual_machine.vm.*.guest_ip_addresses
}

output "uuid" {
  description = "UUID of the VM in vSphere"
  value       = vsphere_virtual_machine.vm.*.uuid
}

output "disk" {
  description = "Disks of the deployed VM"
  value       = vsphere_virtual_machine.vm.*.disk
}

Hi @paulrobinsontkd,

You have used count in the module block and so a reference to that module will produce either a list with a single object or an empty list of objects depending on the value.

Therefore in expressions referring to this you need to explain to Terraform how to handle both of those cases. That is, you need to decide what will happen in the case where there are zero instances of the module. Typically you’d do that by writing an expression that somehow reacts to the length of the list, but I’m not sure exactly what to suggest based on your example because I’m not familiar enough with Ansible to know what would be a reasonable configuration to return in that case.

If you can describe what would be a reasonable result to return in each of the two cases though, I can hopefully show an expression that would achieve that.

@apparentlymart

I’m looking to produce the following output as an example which is used for the inventory of hosts ansible will apply configuration changes too

10.0.0.1 ansible_host=prcvmsap01
ip2         ansible_host=xxx

[all:vars]
ansible_user=ansibleadm

I did managed to get some output using the following

output "vmnames" {
  value = module.vsphere_virtual_machine_s4hana[*].VM
}

output "ip" {
  value = module.vsphere_virtual_machine_s4hana[*].ip
}

I’ve been looking further and think i need to use templates to get what i want to achieve. I am however open to any suggestions.

Thanks

Paul

Hi @paulrobinsontkd,

I agree that a string template with a for directive to repeat over all of your defined hosts seems like a good way to achieve this.

You could make that template easier to write by returning all of the virtual machine settings together in a single output value, rather than lots of separate output values, since then you’ll have only one collection with all of the data which you can use more easily in the template:

output "virtual_machines" {
  value = {
    for vm in vsphere_virtual_machine.vm : vm.default_ip_address => {
      uuid            = vm.uuid
      name            = vm.name
      default_ip_addr = vm.default_ip_address
      guest_ip_addrs  = vm.guest_ip_addresses
    }
  }
}

The above will make module.vsphere_virtual_machine_s4hana[0].virtual_machines appear as a map of objects where each map element has the default IP address as its unique key.

You can then use a template with a for directive to construct the Ansible inventory syntax from this map:

output "ansible_inventory" {
  value = <<-EOF
    %{ for vm in module.vsphere_virtual_machine_s4hana[0].virtual_machines ~}
    ${vm.default_ip_addr} ansible_host=${vm.name}
    %{ endfor ~}
  EOF
}

However, this still doesn’t deal with the problem that module.vsphere_virtual_machine_s4hana might be an empty list, if var.create_s4hana_vm is false and therefore that module has count = 0. Perhaps your goal would be to generate an empty ansible inventory in that case, in which case one way to achieve that would be to use the [*] operator with the module itself, to produce a list of lists of virtual machine objects:

module.vsphere_virtual_machine_s4hana[*].virtual_machines

Because the virtual_machines attribute is already a list, and [*] also produces a list, the above will produce a list of lists and thus an iteration like I showed above won’t work exactly like that. However, the flatten function offers a concise way to remove that extra level of list and just get back a flat list of objects:

flatten(module.vsphere_virtual_machine_s4hana[*].virtual_machines)

The above will be a list with either zero elements, if the module is disabled, or with one element per instance if the module is enabled. Putting that in my example above:

output "ansible_inventory" {
  value = <<-EOF
    %{ for vm in flatten(module.vsphere_virtual_machine_s4hana[*].virtual_machines) ~}
    ${vm.default_ip_addr} ansible_host=${vm.name}
    %{ endfor ~}
  EOF
}

Stepping back a bit and looking at the original examples you shared, I notice that your ./modules/terraform-vsphere-vm module already has an input variable instances which seems to indicate the number of instances to create. If declaring these virtual machines is all that module does, it might be simpler to remove the count from that module call altogether and put the condition on the instances variable instead, like this:

module "vsphere_virtual_machine_s4hana" {
  source            = "./modules/terraform-vsphere-vm/"
  # note: no "count" argument at all

  instances = var.create_s4hana_vm ? var.s4hana_instances : 0
  # (and then everything else, unchanged)

This approach would then avoid the extra complexity of dealing with the list of lists, and mean you’d be able to refer directly to module.vsphere_virtual_machine_s4hana.virtual_machines to get the list of virtual machines, similar to my first template example above but without the need for [0] to select the zeroth module instance.

This would only be a suitable solution if setting instances to zero effectively disables everything in that module, though. I’m not sure from what you shared whether that’s true, so I’m suggesting this alternative just in case it’s applicable.

@apparentlymart thank you for the detailed response has helped loads.

I decided to follow your recommendation removing the count and using instances

  instances = var.create_s4hana_vm ? var.s4hana_instances : 0
  # (and then everything else, unchanged)

That is working fine for what I need and gives the desired results.

Many Thanks :slight_smile: