Terraform looping with for_each

Viewed 4266

How would one get the subnet id if you're using for_each as opposed to count? In my case, I'm doing something like this

resource "aws_instance" "k8s" {
    for_each = var.profiles

    ami = data.aws_ami.latest-ubuntu.id
    instance_type = "t2.medium"
    iam_instance_profile = "${each.value}"
    subnet_id = ??????????????
    vpc_security_group_ids = [var.security_group]
    key_name = var.keyname
    
    connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ubuntu"
    private_key = file(var.private_key_path)

  }
    tags = {
     Name = "${each.key}"
  }
}

And this is because I'm creating similar instances but need to assign them different instance profiles.

Ideally, I'd have done something like

subnet_id = element(var.subnets, count.index )

to place the instances in different subnets but I don't think count and for_each can be used in the same block definition.

I have to subnets and 4 instances and would like to loop through the subnets, placing each instance in one.

Any ideas, please? Thanks.

3 Answers

If var.profiles is a list, you can use each.key to get the index of each element.

A good general strategy with resource for_each is to design the data structure you pass it so that each.value contains all of the per-instance data you need inside the resource block.

In this case, that means that the for_each expression for your aws_instance resource would be a map of objects where each object has both an instance profile and a subnet id.

One way to achieve that would be to write a for expression that transforms var.profiles (which is presumably a set(string) value) into a map of objects that would get the result you want. For example:

resource "aws_instance" "k8s" {
  for_each = {
    # This assigns a subnet to each of the profiles
    # by first sorting them by name to produce a list
    # and then selecting subnets based on the order
    # of the sort result.
    for i, profile_name in sort(var.profiles) : profile_name => {
      iam_instance_profile = profile_name
      subnet_id            = element(var.subnets, i)
    }
  }

  ami                    = data.aws_ami.latest-ubuntu.id
  instance_type          = "t2.medium"
  iam_instance_profile   = each.value.iam_instance_profile
  subnet_id              = each.value.subnet_id
  vpc_security_group_ids = [var.security_group]
  key_name               = var.keyname
}

Using count.index with the element element relied on each item having its own index, but that isn't the case for a set of strings so in the above I used sort to convert to a list under the assumption that for this situation it doesn't really matter which subnet is assigned to each instance as long as the instances end up roughly evenly distributed between the subnets.

However, there is a big implication of that to keep in mind: if you add a new item to var.profiles later then it may cause the subnet_id for existing instances to be reassigned, and would thus require those instances to be recreated. If you don't want that to be true then you'll need to make the selection of subnet per profile more explicit somehow, which could be done by making var.profiles be a list(string) instead of a set(string) and then documenting that new profiles should only be added to the end of that list, or alternatively you could move the decision up into the caller of your module by making var.profiles itself a map of objects where the caller would then specify one subnet per profile:

variable "profiles" {
  type = map(object({
    subnet_id = string
  }))
}

In that case, your resource block would become simpler because the variable value would already be of a suitable shape:

resource "aws_instance" "k8s" {
  for_each = var.profiles

  ami                    = data.aws_ami.latest-ubuntu.id
  instance_type          = "t2.medium"
  iam_instance_profile   = each.key
  subnet_id              = each.value.subnet_id
  vpc_security_group_ids = [var.security_group]
  key_name               = var.keyname
}

you can provide you profiles as a list of objects :

for exemple :

 variable "profiles" {
  type = list(object({
    name = string
    key =  string
  }))
  default = 
    [
      {name = "exemple" , key = "exemplekey" },
      {name = "exemple2" , key = "exemplekey2" }
    ]
}

like this the each.key will containes the index of the element in the list and each.value will contain the object {name,key}

so your code would be like this

resource "aws_instance" "k8s" {
    for_each = var.profiles
    ami = data.aws_ami.latest-ubuntu.id
    instance_type = "t2.medium"
    iam_instance_profile = each.value
    subnet_id = element(var.subnets , each.key)
    vpc_security_group_ids = [var.security_group]
    key_name = var.keyname
    connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ubuntu"
    private_key = file(var.private_key_path)

  }
    tags = {
     Name = each.value.key
  }

}

Related