Convert to map(objects)

Is there a way of converting the value of the below local (local.nlb_config) to use in a for_each?


nlb_config= [
  {
    "ec2_id" = "i-yyyyyyyyyyyyyy"
    "nlb_target_name" = "example-nlb1"
    "nlb_target_port" = "3689"
    "nlb_target_protocol" = "TCP"
    "nlb_target_type" = "instance"
  },
  {
    "ec2_id" = "i-xxxxxxxxxx"
    "nlb_target_name" = "example-nlb1"
    "nlb_target_port" = "3689"
    "nlb_target_protocol" = "TCP"
    "nlb_target_type" = "instance"
  },
  {
    "ec2_id" = "i-yyyyyyyyyyyyyy"
    "nlb_target_name" = "example-nlb2"
    "nlb_target_port" = "53"
    "nlb_target_protocol" = "TCP"
    "nlb_target_type" = "instance"
  },
  {
    "ec2_id" = "i-xxxxxxxxxx"
    "nlb_target_name" = "example-nlb2"
    "nlb_target_port" = "53"
    "nlb_target_protocol" = "TCP"
    "nlb_target_type" = "instance"
  },
]

I have constructed that local but now need it in the correct format to use in a for_each. The local was created like below:


variable "target_id" {
type = map(object({
    id                = string
    availability_zone = string
    tags              = map(string)
  }))
}
variable "nlb_target_group" {
  type = map(object({
    nlb_target_name     = string
    nlb_target_port     = string
    nlb_target_protocol = string
    nlb_target_type     = string
    #health_check        = map(string)
  }))
}

nlb_target_group = {
  group1 = {
    nlb_target_name     = "example-nlb1"
    nlb_target_port     = "3689"
    nlb_target_protocol = "TCP"
    nlb_target_type     = "instance"
  },
  group2 = {
    nlb_target_name     = "example-nlb2"
    nlb_target_port     = "53"
    nlb_target_protocol = "TCP"
    nlb_target_type     = "instance"
  }
}

locals {
  ec2sflat = [
    for key, ec2 in var.target_id : {
      ec2_id = ec2.id
    }
  ]


  nlbsflat = [
    for key, nlb in var.nlb_target_group : {
      key                 = key
      nlb_target_name     = nlb.nlb_target_name
      nlb_target_port     = nlb.nlb_target_port
      nlb_target_protocol = nlb.nlb_target_protocol
      nlb_target_type     = nlb.nlb_target_type
    }
  ]


  nlb_config = [
    for pair in setproduct(local.nlbsflat, local.ec2sflat) : merge(pair[0], pair[1])
  ]
}

I then want to use local.nlb_config in a for_each like below


resource "aws_lb" "this" {
  name               = var.nlb_name
  internal           = var.internal
  load_balancer_type = "network"
  subnets            = var.nlb_subnets

  enable_deletion_protection       = var.enable_deletion_protection
  enable_cross_zone_load_balancing = var.enable_cross_zone_load_balancing
}


resource "aws_lb_target_group" "this" {
  for_each    = local.nlb_config
  name        = each.value.nlb_target_name
  port        = each.value.nlb_target_port
  protocol    = each.value.nlb_target_protocol
  target_type = each.value.nlb_target_type
  vpc_id      = var.vpc_id

}

resource "aws_lb_target_group_attachment" "this" {
  for_each         = local.nlb_config
  target_group_arn = aws_lb_target_group.this[each.key].arn
  target_id        = each.value.ec2_id
  port             = var.target_group_attachment_port
}

resource "aws_lb_listener" "this" {
  for_each          = local.nlb_config
  load_balancer_arn = aws_lb.this.arn
  port              = var.nlb_listner_port
  protocol          = var.nlb_listner_protocol
  default_action {
    type             = var.nlb_listner_default_action_type
    target_group_arn = aws_lb_target_group.this[each.key].arn
  }
}

I seem so close so hopefully an easy solution. Thanks

Hi @jprouten,

You do seem close indeed!

The two important rules for for_each are:

  1. You must have a collection that has one element for each instance you want to declare.
  2. It must be possible to select or generate a unique string for each element only from values defined directly in the configuration to serve as its instance key.

It looks like you’ve already dealt with rule 1 in your local.nlb_config value, where you have one element for each combination of “nlbs” and “ec2s”, which seems like the right cardinality for an aws_lb_target_group_attachment.

I think the challenge for rule 2 is that the only unique identifier you have for your EC2 instances is the ID chosen by the remote system. That isn’t defined directly in your configuration (it’ll only be known after apply) and so it isn’t suitable to use as part of a unique key for for_each. Instead, you’ll need to select some other unique key that’s based on something your configuration decided, rather than something the remote system decided.

However, I noticed your var.target_id is itself a map of objects and so the keys from that map could potentially serve as the unique keys for your EC2 instances, if you adjust ec2sflat like this:

locals {
  ec2sflat = [
    for key, ec2 in var.target_id : {
      ec2_id  = ec2.id
      ec2_key = key
    }
  ]
}

To keep things unambiguous, let’s also rename the key attribute from local.nlbsflat elements to nlb_key, and thus the final merged data structure will have both ec2_key and nlb_key in each of its attributes, and then we can meet rule 2 by projecting the set into a map with another for expression:

resource "aws_lb_target_group_attachment" "this" {
  for_each = {
    for cfg in local.nlb_config : "${cfg.nlb_key}:${cfg.ec2_key}" => cfg
  }

  target_group_arn = aws_lb_target_group.this[each.value.nlb_key].arn
  target_id        = each.value.ec2_id
  port             = var.target_group_attachment_port
}

I think that local.nlb_config isn’t the right data structure for some of your other resources though, if I’m understanding the relationships correctly. It seems like you only need one aws_lb_target_group and aws_lb_listener per entry in var.nlb_target_group, because neither of those refer to any of the EC2 properties and so I don’t think you want to create a separate target group and listener for each EC2 instance.

Thanks. The solution works… but then you are also right that it isn’t right for other resources in the module.