Search code examples
terraformhetzner-cloud

Terraform access value in output from count and for_each


I have a module code to deploy a server, with a dynamic block for networks, shown here

resource "hcloud_server" "this" {
  count               = var.instance_count
  name                = "${local.instance_prefix}.${var.instance_name}${format("%02d", count.index + 1)}"
  image               = data.hcloud_image.image.id
  server_type         = var.flavour
  datacenter          = var.datacenter
  ssh_keys            = var.sshkeys
  placement_group_id  = hcloud_placement_group.this.id
  labels              = local.labels_default
  firewall_ids        = var.firewall_ids

  lifecycle {
    ignore_changes = [
      ssh_keys,
      image,
    ]
  }

  public_net {
    ipv4_enabled = var.public_ipv4
    ipv6_enabled = var.public_ipv6
  }

  dynamic "network" {
    for_each = var.network
    content {
      network_id = lookup(network.value, "network_id")
      ip         = lookup(network.value, "private_ip", null)
      # https://github.com/hetznercloud/terraform-provider-hcloud/issues/650#issuecomment-1497160625
      alias_ips = []
    }
  }
}

According to the documentation the network block has an attribute called ip which holds the assigned IP, see https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/server#network

I want to access this private IP in my module output, but I cant get there. My output now looks like this

output "private_ip" {
  description = "List with public IPv4 IPs of the instances"
  value       = hcloud_server.this.*.network[*].ip
}

and produces the following error

╷
│ Error: Unsupported attribute
│ 
│   on .terraform/modules/testserver/outputs.tf line 18, in output "private_ip":
│   18:   value       = hcloud_server.this.*.network[*].ip
│ 
│ Can't access attributes on a set of objects. Did you mean to access an attribute across all elements of the set?

How can I access the ip atttribute in my output, ideally combined with the instance name?

I tried to play around with * and [count.index] but I didnt get there.

thanks for helping!!!


Solution

  • The expression hcloud_server.this.*.network[*].ip contains nested splat expressions, meaning that the result of one "splat" is being consumed by another one. Therefore it's important to understand how Terraform evaluates these.

    hcloud_server.this.*.network produces a list whose length matches the number of instances of hcloud_server.this (var.instance_count, in your case), where each element is the value of the network attribute of the corresponding instance. From the error message I infer that network is a set of objects, and so the result of hcloud_server.this.*.network is a list of sets of objects.

    The [*].ip part then attempts to access an ip attribute on each element of that list. But the elements of that list are sets of objects rather than individual objects, so the .ip access fails. A set doesn't have any attributes, as the error message states.


    If your intention is to collect up all of the ip attributes across all network blocks across all instances of the resource -- without preserving any information about which instance each IP address belonged to -- then one reasonable approach would be to use the flatten function to transform the list of sets of objects into just a list of objects, and then apply the [*] operator to that result:

    flatten(hcloud_server.this.*.network)[*].ip
    

    The result of flatten(hcloud_server.this.*.network) is a list of objects, discarding the nested sets and just collecting all of their elements into a single list. Therefore the [*].ip portion can now work, because it's now valid to evaluate .ip against each object in that list.


    Note that .* is a legacy (attribute-only) splat expression, which is supported for backward compatibility with older versions of Terraform but isn't needed in this case because the modern [*] operator would be exactly equivalent for the expression I wrote above:

    flatten(hcloud_server.this[*].network)[*].ip
    

    This modification isn't needed -- this expression has exactly the same result as the one above -- but the modern splat operator is more familiar to those who learned Terraform recently, so I personally prefer to use it except for rare cases where I actually need the slightly-different behavior of the legacy operator. (Which doesn't happen very often; that's why it has a more generally-applicable replacement!)