Count or for_each not working, help please!

Hello Everyone,

I am hoping that someone more clever than me can help me, I am on a roadblock and can’t figure out how to progress from here.

However not that relevant to the issue I am facing with Terraform I will try to provide some context. I am trying to create a module that will set up networking in AWS specifically for EKS to address an IPV4 exhaustion issue we are facing because we don’t control the VPC creation and we get very small CIDR blocks.

I am trying to add a new CIDR block, create subnets, Private NGW and create Routing tables that contain a route to the Private NGW and then associate one-to-one relation of the subnet and routing tables without repeating combinations

I apologise in advance for the long post but is difficult to show what I am trying to achieve otherwise.

Code I am using and testing with LocalStack:


# Pass subnets cidrs and az via map
variable "eks_data_cidrs" {
  description = "CIDR blocks for the EKS data plane subnets, e.g. nodes and pods"
  type        = map(any)
  default = {
    subdat-1 = {
      az   = "eu-west-2a"
      cidr = "198.30.16.0/20"
    }
    subdat-2 = {
      az   = "eu-west-2b"
      cidr = "198.30.32.0/20"
    }
    subdat-3 = {
      az   = "eu-west-2c"
      cidr = "198.30.64.0/20"
    }
  }
}

# Create subnets
resource "aws_subnet" "eks_data" {
  for_each = var.eks_data_cidrs

  availability_zone = each.value["az"]
  cidr_block        = each.value["cidr"]
  vpc_id            = data.aws_vpc.vpc.id

  tags = merge(
    var.common_tags,
    var.eks_data_subnet_tags,
    {
      Name        = "${var.prefix}-${var.env}-eksdat-${each.value["az"]}"
      Environment = var.env
    }
  )
}

# Creation of Routing tables with a route to one of the Private NGW
resource "aws_route_table" "eks_data_rttbl" {
  count  = length(values(aws_nat_gateway.private_ngws)[*].id)
  vpc_id = data.aws_vpc.vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = values(aws_nat_gateway.private_ngws)[count.index].id
  }

  tags = merge(
    var.common_tags,
    {
      Name        = "${var.prefix}-${var.env}-eksdata-rttbl-${count.index}"
      Component   = "routing-table"
      Environment = var.env
    }
  )
}


# Attemp to do the associations

# Neeed to generate the one-to-one list so let's say I have the following list of subnets and routing tables:
values(aws_subnet.eks_data)[*].id
[
  "subnet-16b9f7f5",
  "subnet-a83cc7fa",
  "subnet-3886cdeb",
]
aws_route_table.eks_data_rttbl[*].id
[
  "rtb-71353640",
  "rtb-043e1770",
  "rtb-fe69173c",
]

# used zipmap function to generate the list
locals {

  eksdata_assc_map = zipmap(values(aws_subnet.eks_data)[*].id, aws_route_table.eks_data_rttbl[*].id)
}
#Which gives me this:
local.eksdata_assc_map
{
  "subnet-16b9f7f5" = "rtb-71353640"
  "subnet-3886cdeb" = "rtb-fe69173c"
  "subnet-a83cc7fa" = "rtb-043e1770"
}

# I then tried to do the association
# for_each:
resource "aws_route_table_association" "eks_data_rttbl_association" {
  for_each       = local.eksdata_assc_map
  subnet_id      = each.key
  route_table_id = each.value
}

# count:
#resource "aws_route_table_association" "eks_data_rttbl_association" {
#  count          = length(flatten(keys(local.eksdata_assc_map)))
#  subnet_id      = element(keys(local.eksdata_assc_map), count.index)
#  route_table_id = element(values(local.eksdata_assc_map), count.index)
#
#}

So this works fine if the subnets and routing tables have been already created but if I am running this from scratch I get the following errors:

with for_each:
local.eksdata_assc_map will be known only after apply

│ The “for_each” map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource

with count:
The “count” value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the

I can’t figure out how to create a key based map, but even with that not sure I will be able to make it work.

I am really trying to avoid either running this in 2 stages or splitting the code into two modules and using something like Terragrunt to wrap the terraform execution and pass dependencies from one module to the other one more easily.

The keys of a for_each map need to be strings Terraform can figure out before the resources are created.

So, you should be using a for_each expression that gets its keys from your input data and only relating that data to other resource instances in expressions outside of the for_each - perhaps something like:

resource "aws_route_table_association" "eks_data_rttbl_association" {
  for_each       = var.eks_data_cidrs
  subnet_id      = aws_subnet.eks_data[each.key].id
  route_table_id = aws_route_table.eks_data_rttbl[each.key].id
}

You should avoid using count completely, unless you’re provisioning multiple completely identical resources (e.g. VMs in a scalable cluster), because otherwise you can get unexpected side effects when adding/removing instances, and existing instances shift up/down in the numbering scheme, and get modified by Terraform in surprising ways as a result.

Hello @maxb ,

First of all, thank you for taking the time to reply.

To be honest, I am not very knowledgeable on how Terraform does its thing behind the scenes, but it would make sense for Terraform to understand that is going to create the resources and it will “know” the keys and just treat them during plan as “known after apply”, but there are countless threads about that over the internet and it doesn’t work like that unfortunately.

I will give go in changing my approach based on your suggestion but I don’t believe it will work for me, this is because I can’t use any keys for aws_route_table.eks_data_rttbl[each.key].id from the eks_data_cidrs map variable, the routing tables are being created after creating the Private NGW which are also created during apply, there is a lot of dependencies going on here I know!! but in all fairness, I think that is expected when it comes to IaC.

In addition, I need to associate one routing table to one of the subnets created, so subnet-a will associate with route-table-a, and subnet-b will associate with route-table-b and so on, the order actually is not important, only that is 1-1.

I will let you know meanwhile how I get along :slight_smile:

Hello @maxb , @apparentlymart

An update, I managed to change my approach to only use for_each based on your reply, a big thank you for that.

It’s a little bit of a hack but seems to be working, however, my biggest problem still remains, I can’t find a way to do the routing tables association to the subnets, here is my code now.

########################
#  Subnet   Variables                    #
########################
variable "eks_data_cidrs" {
  description = "CIDR blocks for the EKS data plane subnets, e.g. nodes and pods"
  type        = map(any)
  default = {
    subdat-1 = {
      az   = "eu-west-2a"
      cidr = "198.30.16.0/20"
    }
    subdat-2 = {
      az   = "eu-west-2b"
      cidr = "198.30.32.0/20"
    }
    subdat-3 = {
      az   = "eu-west-2c"
      cidr = "198.30.64.0/20"
    }
  }
}

##########################
# Data Source                                    #
##########################
data "aws_subnets" "private_routable" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.vpc.id]
  }

  tags = {
    "Routable" = "true"
    "Type"     = "private"
  }
}

##########################
# Subnet Creation        #
##########################
resource "aws_subnet" "eks_data" {
  for_each = var.eks_data_cidrs

  availability_zone = each.value["az"]
  cidr_block        = each.value["cidr"]
  vpc_id            = data.aws_vpc.vpc.id

  tags = merge(
    var.common_tags,
    var.eks_data_subnet_tags,
    {
      Name        = "${var.prefix}-${var.env}-eksdat-${each.value["az"]}"
      Environment = var.env
    }
  )
}

##########################
# Private NAT Gateway    #
# Creation               #
##########################

resource "aws_nat_gateway" "private_ngws" {
  for_each          = toset(data.aws_subnets.private_routable.ids)
  subnet_id         = each.key
  connectivity_type = "private"

  tags = merge(
    var.common_tags,
    {
      Name        = "${var.prefix}-${var.env}-priv-ngw-${each.key}"
      Component   = "nat-gw"
      Environment = var.env
    }
  )
}

##########################
# EKS Data Plane         #
# Routing Table and      #
# Subnet association     #
##########################
locals {
  eksdata_sub_rttbl_zipmap = zipmap(values(aws_subnet.eks_data)[*].id, values(aws_route_table.eks_data_rttbl)[*].id)

  eksdata_sub_rtbl_map = tomap({
    for k, v in local.eksdata_sub_rttbl_zipmap : k => {
      "subnet_id"      = k
      "route_table_id" = v
    }
  })
}

resource "aws_route_table" "eks_data_rttbl" {
  for_each = toset(data.aws_subnets.private_routable.ids)
  vpc_id   = data.aws_vpc.vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.private_ngws[each.key].id
  }

  tags = merge(
    var.common_tags,
    {
      Name        = "${var.prefix}-${var.env}-eksdata-rttbl-${aws_nat_gateway.private_ngws[each.key].id}"
      Component   = "routing-table"
      Environment = var.env
    }
  )
}

resource "aws_route_table_association" "eks_data_rttbl_association" {
  for_each       = local.eksdata_assc_map
  subnet_id      = each.key
  route_table_id = each.value
}

The aws_route_table_association.eks_data_rttbl_association is erroring as before with:

Terraform return error:

│ Error: Invalid for_each argument

│ on rttables.tf line 71, in resource “aws_route_table_association” “eks_data_rttbl_association”:
│ 71: for_each = local.eksdata_assc_map
│ ├────────────────
│ │ local.eksdata_assc_map will be known only after apply

│ The “for_each” map includes keys derived from resource attributes that cannot be determined until apply, and
│ so Terraform cannot determine the full set of keys that will identify the instances of this resource.

│ When working with unknown values in for_each, it’s better to define the map keys statically in your
│ configuration and place apply-time results only in the map values.

│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each
│ value depends on, and then apply a second time to fully converge.

So I think I need to build somehow a map with keys and values based on the values of that zipmap but I can’t figure out how to do it, so any help will be much appreciated.

To make it really clear what I am trying to achieve:

E.g. I have this:

values(aws_subnet.eks_data)[*].id
[
“subnet-2654e928”,
“subnet-5757d9d4”,
“subnet-67d02d48”,
]

And this:

values(aws_route_table.eks_data_rttbl)[*].id
[
“rtb-c799e9c2”,
“rtb-fa9b7c30”,
“rtb-15d8952a”,
]

And I need to generate a map that I can use for the route table association: which zipmap almost gives me but I can’t use it, the zipmap gives this:

local.eksdata_sub_rttbl_zipmap
{
“subnet-2654e928” = “rtb-c799e9c2”
“subnet-5757d9d4” = “rtb-fa9b7c30”
“subnet-67d02d48” = “rtb-15d8952a”
}

I am having an hard time to generate a map with keys that can be used from the subenet and route table creation

Thank you in advance

There is no definition for a local of this name, in the code you have posted in your most recent response.

Sorry, but this really doesn’t make things clear at all.

I’m not seeing any ordering guarantees here, so it looks to me like when you zipmap you’re actually associating subnets and route tables randomly?!

Hello @maxb ,

Apologies for not coming back earlier, I was away from the computer since yesterday.

That is correct, it is random, however now that you mentioned I should probably confirm or review if the routing tables are associated with both subnets and private ngw in the same AZ for performance and cost optimization.

Not sure how I am going to do this with terraform though, would this be easier if I create submodules that are invoked by the root module?

Anyway, if I don’t care about the order and the random aspect of what I shared yesterday, how would I got about doing those routing table associations, do you have any suggestions?

Hi @nmofonseca,

I feel a little unsure as to what is the relationship between “EKS subnets” and “private routable subnets” in your design, but I think what I understand is that every EKS subnet should have one corresponding “private routable subnet” and that each private routable subnet should have a NAT gateway in it, and then each of the EKS subnets should have a route table that sets the default route to the NAT gateway in the corresponding “private routable subnet”.

Assuming I have that right, I would describe that to Terraform as something like the following:

variable "eks_data_cidrs" {
  description = "CIDR blocks for the EKS data plane subnets, e.g. nodes and pods"

  # NOTE: Using "any" here makes no sense because the
  # module clearly requires exactly these attributes.
  # "any" is for situations where your module makes no
  # assumptions about the type.
  type = map(object({
    az   = string
    cidr = string
  }))
  default = {
    subdat-1 = {
      az   = "eu-west-2a"
      cidr = "198.30.16.0/20"
    }
    subdat-2 = {
      az   = "eu-west-2b"
      cidr = "198.30.32.0/20"
    }
    subdat-3 = {
      az   = "eu-west-2c"
      cidr = "198.30.64.0/20"
    }
  }
}

resource "aws_subnet" "eks_data" {
  for_each = var.eks_data_cidrs

  availability_zone = each.value["az"]
  cidr_block        = each.value["cidr"]
  vpc_id            = data.aws_vpc.vpc.id
  # (etc)
}

data "aws_subnet" "private_routable" {
  for_each = aws_subnet.eks_data

  vpc_id            = each.value.vpc_id
  availability_zone = each.value.availability_zone
}

resource "aws_nat_gateway" "private_ngws" {
  for_each = data.aws_subnet.private_routable

  subnet_id         = each.value.id
  connectivity_type = "private"
  # (etc)
}


resource "aws_route_table" "eks_data" {
  for_each = data.aws_subnet.private_routable
  
  vpc_id = each.value.vpc_id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.private_ngws[each.key].id
  }
  
  # (etc)
}

locals {
  subnet_pairs = tomap({
    for k, eksd_subnet in aws_subnet.eks_data : k => {
      eks_data_subnet         = eksd_subnet
      private_routable_subnet = data.aws_subnet.private_routable[k]
      nat_gateway             = aws_nat_gateway.private_ngws[k]
      route_table             = aws_route_table.eks_data[k]
    }
  })
}

resource "aws_route_table_association" "eks_data" {
  for_each = local.subnet_pairs
  
  subnet_id      = each.value.eks_data_subnet.id
  route_table_id = each.value.route_table.id
}

This uses data "aws_subnet" to find the one “private routable” subnet in the same availability zone as each “eks data” subnet being declared, so these are guaranteed to be paired up by availability zone.

Hello @apparentlymart ,

Thank you so much for your reply and for taking the time for it.

The private routable subnets are some pre-existing subnets that won’t be created as part of this module and they are in a different CIDR hence why I need to get them via a data source, I need them to place the Private NGW’s and I believe you totally got it from your reply.

The data source I use for that as e.g.:

data "aws_subnets" "private_routable" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.vpc.id]
  }

  tags = {
    "Routable" = "true"
    "Type"     = "private"
  }
}

Basically, I use tags to get their information and then use them to create the NAT GW’s.

I will give it a go and let you and @maxb how I get on.

I just want to take the opportunity to thank you both so much regardless of the outcome, both of you helped me already with and helped me with my approach.

Hi @maxb and @apparentlymart ,

Sorry for not replying sooner, I am off work currently so haven’t been around.

I just wanted to confirm that your suggestions totally helped me in figuring out my terraform, I will share the code as soon as I can.

What you guys helped me in figuring out was that the most important thing is that the keys of what you want to loop with for_each need to be known, and I wasn’t really clear on that before.

So what I did was forcing my code to create resources that resulted in a map with the same keys and then used what @apparentlymart suggested for building my subnet_pairs, it’s a little bit of a hack but it totally works.

That way I can do the route table association just fine.

Once again thank you so much for the massive help, this is definitely an amazing community.