Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terraform - how to use for_each loop on a list of objects to create resources

I have an object containing the list of subnets I want to create.

variable "subnet-map" {
  default = {
    ec2 = [
      {
        cidr_block        = "10.0.1.0/24"
        availability_zone = "eu-west-1a"
      }
    ],
    lambda = [
      {
        cidr_block        = "10.0.5.0/24"
        availability_zone = "eu-west-1a"
      },
      {
        cidr_block        = "10.0.6.0/24"
        availability_zone = "eu-west-1b"
      },
      {
        cidr_block        = "10.0.7.0/24"
        availability_zone = "eu-west-1c"
      }
    ],
    secrets = [
      {
        cidr_block        = "10.0.8.0/24"
        availability_zone = "eu-west-1a"
      },
      {
        cidr_block        = "10.0.9.0/24"
        availability_zone = "eu-west-1b"
      },
      {
        cidr_block        = "10.0.10.0/24"
        availability_zone = "eu-west-1c"
      }
    ],
    rds = [
      {
        cidr_block        = "10.0.11.0/24"
        availability_zone = "eu-west-1a"
      },
      {
        cidr_block        = "10.0.12.0/24"
        availability_zone = "eu-west-1b"
      },
      {
        cidr_block        = "10.0.13.0/24"
        availability_zone = "eu-west-1c"
      }
    ]
  }
}

Earlier I was using the count loop construct. So I used to flatten the above structure into a list of objects

locals {
  subnets = flatten([
    for resource in keys(var.subnet-map) : [
      for subnet in var.subnet-map[resource] : {
        resource          = resource
        cidr_block        = subnet.cidr_block
        availability_zone = subnet.availability_zone
      }
    ]
  ])
}

And then I would create the resources by doing

resource "aws_subnet" "aws-subnets" {
  count             = length(local.subnets)
  vpc_id            = aws_vpc.aws-vpc.id
  cidr_block        = local.subnets[count.index].cidr_block
  availability_zone = local.subnets[count.index].availability_zone

  tags = {
    Name = "subnet-${local.subnets[count.index].resource}-${local.subnets[count.index].availability_zone}"
  }
}

Now I want to use the for_each loop. But I cannot figure out how to do it. This is what I've done so far.

resource "aws_subnet" "subnets-dev" {
  for_each          = var.subnet-map
  vpc_id            = aws_vpc.vpc-dev.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone

  tags = {
    Name        = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
    environment = "dev"
  }
}

But it keeps giving an error saying

Error: Unsupported attribute

  on vpc/main.tf line 93, in resource "aws_subnet" "subnets-dev":
  93:     Name        = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
    |----------------
    | each.value is tuple with 3 elements

This value does not have any attributes.

How could I fix this?

like image 377
JP96 Avatar asked Aug 20 '19 09:08

JP96


1 Answers

I'm not sure I fully follow all of what you tried here because your initial snippet of var.subnet-map shows it being a map of maps of lists of objects, but later on when you used for_each = var.subnet-map it seems to have treated it as a map of lists instead. Did you remove that extra level of maps (the "default" key) before trying for_each here?

Working with your original definition of variable "subnet-map", your first step with for_each will be similar to what you did with count: you need to flatten the structure, this time into a map of objects rather than a list of objects. The easiest way to get there is to derive a map from your existing flattened list:

locals {
  subnets = flatten([
    for resource in keys(var.subnet-map) : [
      for subnet in var.subnet-map[resource] : {
        resource          = resource
        cidr_block        = subnet.cidr_block
        availability_zone = subnet.availability_zone
      }
    ]
  ])

  subnets_map = {
    for s in local.subnets: "${s.resource}:${s.availability_zone}" => s
  }
}

Here I assumed that your "resource" string and your availability zone together are a suitable unique identifier for a subnet. If not, you can adjust the "${s.resource}:${s.availability_zone}" part to whatever unique key you want to use for these.

Now you can use the flattened map as the for_each map:

resource "aws_subnet" "subnets-dev" {
  for_each          = local.subnets_map
  vpc_id            = aws_vpc.vpc-dev.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone

  tags = {
    Name        = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
    environment = "dev"
  }
}

This will give you instances with addresses like aws_subnet.subnets-dev["ec2:eu-west-1a"].


Note that if you are migrating from count with existing subnets that you wish to retain, you'll need to also do a one-time migration step to tell Terraform which indexes from the existing state correspond to which keys in the new configuration.

For example, if (and only if) index 0 was previously the one for ec2 in eu-west-1a, the migration command for that one would be:

terraform state mv 'aws_subnet.subnets-dev[0]' 'aws_subnet.subnets-dev["ec2:eu-west-1a"]'

If you're not sure how they correlate, you can run terraform plan after adding for_each and look at the instances that Terraform is planning to destroy. If you work through each one of those in turn, taking the address Terraform currently knows along with the resource and availability zone names shown in the Name tag, you can migrate each of them to its new address so that Terraform will no longer think you're asking for it to destroy the numbered instances and replace them with named ones.

like image 183
Martin Atkins Avatar answered Sep 27 '22 16:09

Martin Atkins