terraform-aws-modules / terraform-aws-vpc

Terraform module to create AWS VPC resources πŸ‡ΊπŸ‡¦
https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws
Apache License 2.0
2.96k stars 4.41k forks source link

tuple vs list problem with output database_route_table_ids during first plan #857

Closed martin566 closed 1 year ago

martin566 commented 1 year ago

Description

Usage of output database_route_table_ids in another module causes error "Invalid for_each argument" during first plan/apply.

Cause seems to be that the output delivers a tuple during first plan/apply, because the route_table is still not existing, but a list after the first apply. This seems to be related to https://github.com/hashicorp/terraform/issues/31102, but the usage of tolist() function, did not help in the exampe below.

Versions

Reproduction Code [Required]

provider "aws" {
  region = "eu-central-1"
}

locals {
  name   = "ex-${replace(basename(path.cwd), "_", "-")}"
  region = "eu-central-1"

  # Normaly this is a variable containg a more complex map with multiple cidrs
  # routed to different tgws
  routes_to_transit_gateway = {
    "example_with_simple_routing" = {
      "tgw_id"            = "tgw-example-id" # Normaly a real existing tgw id
      "destination_cidrs" = ["0.0.0.0/0"]
      "name"              = "example"
      "subnet_name"       = "app"
    }
  }

  ###########################################################
  # Generate a list containing all vpc route_table_ids with
  # associated subnet names
  route_table_ids_list = flatten(concat([
    for route_table_id in module.vpc.public_route_table_ids :
    {
      "route_table_id" = route_table_id,
      "subnet_name"    = "web"
    }
    ], [
    for route_table_id in module.vpc.private_route_table_ids :
    {
      "route_table_id" = route_table_id,
      "subnet_name"    = "app"
    }
    ], [
    for route_table_id in module.vpc.database_route_table_ids :
    {
      "route_table_id" = route_table_id,
      "subnet_name"    = "data"
    }
    ], [
  ]))

  #####################################################
  # Generate a list of maps of all required routes to
  # transit gateways in this vpc
  routes_to_transit_gateway_per_table = flatten([
    for tgw_entry in local.routes_to_transit_gateway : [
      for destination_cidr in tgw_entry.destination_cidrs : [
        for rtb_value in local.route_table_ids_list : [
          {
            "route_entry_name" = "${tgw_entry.name}_${rtb_value.subnet_name}_${destination_cidr}"
            # Add route table id and destination CIDR to each tgw attachment entry
            "route_table_id"                        = rtb_value.route_table_id,
            "destination_cidr"                      = destination_cidr
            "tgw_id"                                = tgw_entry.tgw_id
            "routes_to_transit_gateway_subnet_name" = tgw_entry.subnet_name # for debugging
            "route_table_ids_map_subnet_name"       = rtb_value.subnet_name # for debugging
          }
          ] if contains([rtb_value.subnet_name], tgw_entry.subnet_name) || (
          contains(["default"], tgw_entry.subnet_name) && rtb_value.subnet_name != "dmz"
        )
      ]
    ]
  ])

}

################################################################################
# VPC Module
################################################################################

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.16.1"

  name = local.name
  cidr = "10.0.0.0/16"

  azs              = ["${local.region}a", "${local.region}b"]
  private_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  database_subnets = ["10.0.3.0/24", "10.0.4.0/24"]

  enable_ipv6        = false
  enable_nat_gateway = false
  single_nat_gateway = true

  public_subnet_tags = {
    Name = "overridden-name-public"
  }

  vpc_tags = {
    Name = "vpc-name"
  }
}

#####################################################
# Generate all route to all tgws from one map
resource "aws_route" "dynamic" {
  for_each = {
    for route in local.routes_to_transit_gateway_per_table : route.route_entry_name => route
  }

  route_table_id         = each.value.route_table_id
  destination_cidr_block = each.value.destination_cidr
  transit_gateway_id     = each.value.tgw_id

}

output "debug_routes_to_transit_gateway_per_table" {
  value = local.routes_to_transit_gateway_per_table
}

Steps to reproduce the behavior:

terraform init & terraform plan

Expected behavior

terraform plan shows, that the following resource will be created

 # aws_route.dynamic["example_app_0.0.0.0/0"] will be created
  + resource "aws_route" "dynamic" {
      + destination_cidr_block = "0.0.0.0/0"
      + id                     = (known after apply)
      + instance_id            = (known after apply)
      + instance_owner_id      = (known after apply)
      + network_interface_id   = (known after apply)
      + origin                 = (known after apply)
      + route_table_id         = (known after apply)
      + state                  = (known after apply)
      + transit_gateway_id     = "tgw-example-id"
    }

Actual behavior

terraform plan shows the following error message:

$ terraform plan
β•·
β”‚ Error: Invalid for_each argument
β”‚ 
β”‚   on main.tf line 110, in resource "aws_route" "dynamic":
β”‚  110:   for_each = {
β”‚  111:     for route in local.routes_to_transit_gateway_per_table : route.route_entry_name => route
β”‚  112:   }
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ local.routes_to_transit_gateway_per_table is tuple with 1 element
β”‚ 
β”‚ The "for_each" 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 resources that the for_each depends on.

Solution

Can be solved by the following change:

diff --git a/outputs.tf b/outputs.tf
index 9d93dda..c5c0200 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -245,7 +245,7 @@ output "private_route_table_ids" {

 output "database_route_table_ids" {
   description = "List of IDs of database route tables"
-  value       = try(coalescelist(aws_route_table.database[*].id, aws_route_table.private[*].id), [])
+  value       = length(aws_route_table.database[*].id) > 0 ? aws_route_table.database[*].id : aws_route_table.private[*].id
 }

 output "redshift_route_table_ids" {
github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 10 days

untcha commented 1 year ago

This issue is still open.

SungHyeon commented 1 year ago

I have same issue. When this issue fix release?

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 10 days

bryantbiggs commented 1 year ago

I am afraid this is a core Terraform issue, that is coming from here

for_each = {
    for route in local.routes_to_transit_gateway_per_table : route.route_entry_name => route
  }

You cannot use for_each over a map where the key is unknown; Hashi recommends using a static value and not a computed value as the key for this reason https://github.com/hashicorp/terraform/issues/30937

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 10 days

github-actions[bot] commented 1 year ago

This issue was automatically closed because of stale in 10 days

github-actions[bot] commented 1 year ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

antonbabenko commented 1 year ago

This issue has been resolved in version 5.1.1 :tada: