amazon-web-servicesterraformterraform-provider-awsterraform0.12+

Looping with for_each


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?


Solution

  • 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
    }