Hi @fran.rodriguez,
I think what this error message is telling you is that the e
in your for
expression isn’t a string, and so it isn’t a valid key. You are using e
as a key by including it just before the =>
symbol in the for
expression.
Looking at your definition of albc_alb_sg__rules
it seems to be generating a list of maps of objects rather than just a single map, and so e
in your for
expression is each whole map, rather than each element of the maps inside.
In order for this to work, your local.albc_alb_sg__rules
value will need to be a map of objects rather than a list of maps of objects. One part of getting to that could be to use merge
to ask Terraform to merge all of these maps together into a single map, like this:
locals {
flat_rules = merge(local.albc_alb_sg__rules...)
}
The extra ...
symbol in this call tells Terraform to take each element of the list as a separate argument to the function, rather than passing the entire list as one argument.
However, the problem here is that all of your elements of local.albc_alb_sg__rules
have the same two keys "ingress_vpn_80"
and "ingress_vpn_443"
, so they will all overwrite each other so that only the last element of var.albc_alb_sg_ids
will survive into the result.
To fix that will require changing some details that aren’t visible in the snippets you’ve shared, so I need to make some guesses about what else you have. I’m guessing that you’ve declared variable "albc_alb_sg_ids"
as being either type = list(string)
or type = set(string)
, and that sg_id
will be the remote-server-generated security group IDs, like sg-abcd1234
. If so, those IDs generated by the server are not suitable for use as instance tracking keys in Terraform because their values will not be decided until the apply step, and so Terraform will not be able to generate a durable address for each of the instances during planning.
To deal with that, the typical answer is to use a map instead, where the map keys are static strings chosen in your configuration and only the map values are dynamic strings chosen by the remote system during the apply step:
variable "albc_alb_sg_ids" {
type = map(string)
}
When you call this module you’d then specify a particular tracking key for each of the security groups you are passing. The module
block calling this module would contain something like this:
module "example" {
# ...
albc_alb_sg_ids = {
example_a = aws_security_group.example_a.id
example_b = aws_security_group.example_b.id
}
}
I would suggest understanding the above as saying “a security group which we will call "example_a"
, whose remote ID is aws_security_group.example_a.id
”. This then specifies both a local key that Terraform can use to track each security group and the ID that the remote system uses to track the object.
With the variable redefined in that way, you can then propagate the keys from that input variable into the keys of your derived map, and then finally into your instance keys for for_each
:
locals {
albc_alb_sg__rules = merge([for key, sg_id in var.albc_alb_sg_ids :
{
"${key}:ingress_vpn_80" = {
description = "Custom HTTP users to EKS Internal ALB"
protocol = "tcp"
from_port = 80
to_port = 80
type = "ingress"
source_security_group_id = sg_id
}
"${key}:ingress_vpn_443" = {
description = "Custom HTTPS users to EKS Internal ALB"
protocol = "tcp"
from_port = 443
to_port = 443
type = "ingress"
source_security_group_id = sg_id
}
}
]...)
}
I changed only two things here relative to your original example:
- The
for
expression is wrapped in a merge
call like I showed above, so that all of the mappings generated by the for
expression will be combined together into a single map.
- I changed the keys expressions for the two generated map elements to include
${key}:
interpolation, which means that with the example module input I showed above this would generate keys like "example_a:ingress_vpn_80"
and "example_b:ingress_vpn_80"
, which ensures that the keys will not conflict when merge
combines the maps.
This result now matches the requirements for for_each
: it’s a mapping with one element per instance you want to declare, and each element has a key that Terraform can know during the plan step because it was specified in the configuration rather than decided by the remote system.
So finally we can just plug that expression directly into for_each
:
resource "aws_security_group_rule" "internal" {
for_each = local.albc_alb_sg__rules
# (everything else unchanged)
}
This should declare the following resource instance addresses, if the module were given the example input I showed above:
aws_security_group_rule.internal["example_a:ingress_vpn_80"]
aws_security_group_rule.internal["example_a:ingress_vpn_443"]
aws_security_group_rule.internal["example_b:ingress_vpn_80"]
aws_security_group_rule.internal["example_b:ingress_vpn_443"]
These unique instance keys will then allow Terraform to track the instance between the plan and apply step, and then on future runs allow Terraform to understand the difference between updating an existing definition in-place vs. adding or removing security groups entirely.