Error message:Invalid value for “inputMap” parameter: lookup() requires a map as the first argument

Hi, I’m getting this error - invalid value for “inputMap” parameter: lookup() requires a map as the first argument.
I’m creating WAFv2 web acl and rules for the acl.

Snippert of my module look like this:

terraform {
  required_version = ">= 1.0.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

module "wafv2" {
  source  = "terraform-aws-waf.git?ref=v0.0.5"

  web_acl_name                   = "defect-dojo-wafv2"
  scope                          = "REGIONAL"
  allow_default_action           = true 
  create_alb_association         = true"
  enabled                        = true 
  name_prefix                    = "defect-dojo"

  visibility_config = {
    metric_name = "defect-dojo"
  }

  rules = {
    name = "defect-dojo"
    priority = "1"

    action = "count"

    visibility_config = {
        cloudwatch_metrics_enabled = false
        metric_name                = "defect-dojo"
        sampled_requests_enabled   = false
    }

    byte_match_statement = {
        field_to_match = {
            uri_path = "{}"
        }
        positional_constraint = "STARTS_WIH"
        search_string         = "/portal"
        priority              = 0
        type                  = "NONE"
    }
  }
}
 resource "aws_wafv2_web_acl" "waf_web_acl" {
  count = var.enabled ? 1 : 0

  name        = local.waf_web_acl_name
  scope       = var.scope
  description = var.description

  default_action {
    dynamic "allow" {
      for_each = var.allow_default_action ? [1] : []
      content {}
    }

    dynamic "block" {
      for_each = var.allow_default_action ? [1] : []
      content {}
    }
  }

  dynamic "rule" {
    for_each = var.rules
    content {
      name     = lookup(rule.value, "name")
      priority = lookup(rule.value, "priority")
      dynamic "action" {
        for_each = length(lookup(rule.value, "action", {})) == 0 ? [] : [1]
        content {
          dynamic "allow" {
            for_each = lookup(rule.value, "action", {}) == "allow" ? [1] : []
            content {}
          }

          dynamic "block" {
            for_each = lookup(rule.value, "action", {}) == "block" ? [1] : []
            content {}
          }

          dynamic "count" {
            for_each = lookup(rule.value, "action", {}) == "count" ? [1] : []
            content {}
          }
        }
      }

      dynamic "override_action" {
        for_each = length(lookup(rule.value, "override_action", {})) == 0 ? [] : [1]
        content {
          dynamic "none" {
            for_each = lookup(rule.value, "override_action", {}) == "none" ? [1] : []
            content {}
          }

          dynamic "count" {
            for_each = lookup(rule.value, "override_action", {}) == "count" ? [1] : []
            content {}
          }
        }
      }

      statement {

        dynamic "byte_match_statement" {
          for_each = length(lookup(rule.value, "byte_match_statement", {})) == 0 ? [] : [lookup(rule.value, "byte_match_statement", {})]
          content {
            dynamic "field_to_match" {
              for_each = length(lookup(byte_match_statement.value, "field_to_match", {})) == 0 ? [] : [lookup(byte_match_statement.value, "field_to_match", {})]
              content {
                dynamic "uri_path" {
                  for_each = length(lookup(field_to_match.value, "uri_path", {})) == 0 ? [] : [lookup(field_to_match.value, "uri_path")]
                  content {}
                }

                dynamic "query_string" {
                  for_each = length(lookup(field_to_match.value, "query_string", {})) == 0 ? [] : [lookup(field_to_match.value, "query_string")]
                  content {}
                }

                dynamic "all_query_arguments" {
                  for_each = length(lookup(field_to_match.value, "all_query_arguments", {})) == 0 ? [] : [lookup(field_to_match.value, "all_query_arguments")]
                  content {}
                }
              }
            }
            positional_constraint = lookup(byte_match_statement.value, "positional_constraint")
            search_string         = lookup(byte_match_statement.value, "search_string")
            text_transformation {
              priority = lookup(byte_match_statement.value, "priority")
              type     = lookup(byte_match_statement.value, "type")
            }
          }
        }

        dynamic "rate_based_statement" {
          for_each = length(lookup(rule.value, "rate_based_statement", {})) == 0 ? [] : [lookup(rule.value, "rate_based_statement", {})]
          content {
            limit              = lookup(rate_based_statement.value, "limit")
            aggregate_key_type = lookup(rate_based_statement.value, "aggregate_key_type", "IP")

            dynamic "forwarded_ip_config" {
              for_each = length(lookup(rule.value, "forwarded_ip_config", {})) == 0 ? [] : [lookup(rule.value, "forwarded_ip_config", {})]
              content {
                fallback_behavior = lookup(forwarded_ip_config.value, "fallback_behavior")
                header_name       = lookup(forwarded_ip_config.value, "header_name")
              }
            }

          }
        }
      }

variable "rules" {
  description = "list of WAF rules"
  type        = any
  default     = []
}

the error message is below, i am not sure what i’m doing wrong:

 on .terraform/modules/wafv2/main.tf line 35, in resource "aws_wafv2_web_acl" "waf_web_acl":
│   35:         for_each = length(lookup(rule.value, "action", {})) == 0 ? [] : [1]
│     ├────────────────
│     │ rule.value is "defect-dojo"
│ 
│ Invalid value for "inputMap" parameter: lookup() requires a map as the
│ first argument.
╵
╷
│ Error: Invalid function argument
│ 
│   on .terraform/modules/wafv2/main.tf line 35, in resource "aws_wafv2_web_acl" "waf_web_acl":
│   35:         for_each = length(lookup(rule.value, "action", {})) == 0 ? [] : [1]
│     ├────────────────
│     │ rule.value is "1"
│ 
│ Invalid value for "inputMap" parameter: lookup() requires a map as the
│ first argument.
╵
╷
│ Error: Invalid function argument
│ 
│   on .terraform/modules/wafv2/main.tf line 56, in resource "aws_wafv2_web_acl" "waf_web_acl":
│   56:         for_each = length(lookup(rule.value, "override_action", {})) == 0 ? [] : [1]
│     ├────────────────
│     │ rule.value is "count"
│ 
│ Invalid value for "inputMap" parameter: lookup() requires a map as the
│ first argument.
╵```

Hi @darekorex!

It seems like the final resolved type of var.rules isn’t what you’re expecting, but because you declared type = any Terraform can’t catch that at the point of declaration and so it’s failing downstream where it’s harder to debug.

I think the root problem here is that your rules value is only describing a single rule, not a collection of rules. Therefore when Terraform tries to repeat the dynamic rule block content for each “element” of var.rules it’s instead visiting each of the attributes of the object, causing the keys and values to make no sense.

I would suggest first changing your variable "rules" block to specify the exact type constraint you are intending to use. That will then give Terraform some more information so it can return a more precise error message.

Since you named that variable rules I’m assuming you intended it to be a collection of objects rather than just a single object. I don’t know which of the collection types makes most sense for this situation but here’s an example of using a list. The principle is the same if you want to declare a set or a map instead.

variable "rules" {
  type = list(object({
    name            = string
    priority        = number
    actions         = optional(string)
    override_action = optional(string)
    byte_match_statement = optional(object({
      field_to_match        = optional(string)
      positional_constraint = optional(string)
      search_string         = optional(string)
      priority              = optional(number)
      type                  = optional(string)
    }))
    rate_based_statement = optional(object({
      limit              = optional(number)
      aggregate_key_type = optional(string)
      forwarded_ip_config = optional(object({
        header_name       = string
        fallback_behavior = optional(string)
      }), {})
    }))
  }))
}

I’m not familiar with aws_wafv2_web_acl so I made some guesses here about what the appropriate data types might be. My goal here is to show you what the syntax might look like, not to illustrate exactly what the right types are. Please review this and make sure what I’ve written out here makes sense!

Some of what I wrote above isn’t actually compatible with the resource configuration you shared, so here’s a revised dynamic "rule" block which I think matches with this declaration, although I’ve not tested it because I don’t currently have access to an AWS account.

  dynamic "rule" {
    for_each = var.rules
    content {
      name     = rule.value.name
      priority = rule.value.property

      dynamic "action" {
        for_each = rule.value.action[*]
        content {
          dynamic "allow" {
            for_each = action.value == "allow" ? [action.value] : []
            content {}
          }

          dynamic "block" {
            for_each = action.value == "block" ? [action.value] : []
            content {}
          }

          dynamic "count" {
            for_each = action.value == "count" ? [action.value] : []
            content {}
          }
        }

        dynamic "override_action" {
          for_each = rule.value.override_action[*]
          content {
            dynamic "none" {
              for_each = override_action.value == "none" ? [override_action.value] : []
              content {}
            }

            dynamic "count" {
              for_each = override_action.value == "count" ? [override_action.value] : []
              content {}
            }
          }
        }
        statement {
          dynamic "byte_match_statement" {
            for_each = rule.byte_match_statement[*]
            content {
              dynamic "field_to_match" {
                for_each = byte_match_statement.value.field_to_match[*]
                content {
                  dynamic "uri_path" {
                    for_each = field_to_match.value == "uri_path" ? [field_to_match.value] : []
                    content {}
                  }
                  dynamic "query_string" {
                    for_each = field_to_match.value == "query_string" ? [field_to_match.value] : []
                    content {}
                  }
                  dynamic "all_query_arguments" {
                    for_each = field_to_match.value == "all_query_arguments" ? [field_to_match.value] : []
                    content {}
                  }
                }
              }

              positional_constraint = byte_match_statement.value.positional_constraint
              search_string         = byte_match_statement.value.search_string

              text_transformation {
                priority = byte_match_statement.value.priority
                type     = byte_match_statement.value.type
              }
            }
          }

          dynamic "rate_based_statement" {
            for_each = rule.rate_based_statement[*]
            content {
              limit              = rate_based_statement.value.limit
              aggregate_key_type = rate_based_statement.value.aggregate_key_type

              dynamic "forwarded_ip_config" {
                for_each = rate_based_statement.value.forwarded_ip_config[*]
                content {
                  fallback_behavior = forwarded_ip_config.value.fallback_behavior
                  header_name       = forwarded_ip_config.value.header_name
                }
              }
            }
          }
        }
      }
    }
  }

The design of this resource type – using empty nested blocks to specify settings rather than just arguments – means this will always be pretty verbose whatever we do. But by specifying the type constraint exactly we can make it a little more concise in two ways:

  • If you declare an attribute as being optional then Terraform will guarantee that it will always be present in the object but it might be null. Assigning null to a resource argument is the same as leaving it completely unset, so we can just assign the possibly-null value to the arguments without any special conditional statements and without using lookup.
  • The [*] operator (the “splat” operator) can be applied to a non-list value to concisely convert it into a sequence of either zero or one values. If the operand is null then it’ll produce a zero-length sequence and if it’s not null then it’ll produce a sequence of one element which exactly matches the operand. This is therefore a convenient way to use a possibly-null attribute as the for_each of a dynamic block, again without having to write out long-winded conditional expressions.

The need to project individual attributes from the input into one of several nested block types is not a common design pattern for Terraform providers and so unfortunately there isn’t any shorthand syntax to deal with that situation. For those ones there isn’t anything more concise than the conditional expressions you already wrote to choose between zero or one elements based on a string equality test.

Since this is a complicated example it’s inevitable I’ve made at least a few mistakes in here and so I expect this will probably not work immediately. If you see an error and you’re not sure how to respond to it, please let me know and I’ll try to figure out what I did wrong here!