How can I iterate through a map variable in terraform

You can use a combination of map, keys function,index function, and count. This terraform creates 3 acls with various rules.

  • The names of the acl's are determined by the keys.
  • The number of acl's is determined by the count of the keys.
  • The index of each rule (priority) is determined by the index function
  • The name of each rule is from the CONTAINS_WORD or CONTAINS property in the map

=>

variable "acls" {
  type = map(any)
  default = {

    "acl1" = {
      "CONTAINS_WORD" = ["api","aaa", "bbb", "ccc"]
      "CONTAINS" = ["xxx","yyy"]
    }

    "acl2" = {
      "CONTAINS_WORD" = [ "url1,"url2","url3"]
      "CONTAINS" = ["url4"]
    }

    "acl3" = {
      "CONTAINS_WORD" = ["xxx"]
      "CONTAINS" = []
    } 
  }
}

resource "aws_wafv2_web_acl" "acl" {
  name  = keys(var.acls)[count.index]
  scope = "REGIONAL"
  count = length(keys(var.acls))

  default_action {
    block {}
  }

  dynamic "rule" {

    for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)

    content {
      name     =  rule.key
      priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD, rule.key)

      action {
        allow {}
      }

      statement {
        
        #https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
        byte_match_statement  {

          positional_constraint = "CONTAINS_WORD"
          search_string         = lower(rule.key)

          field_to_match {
            uri_path {}
          }

          text_transformation {
            priority = 0
            type     = "LOWERCASE"
          }
        }
      } 

      visibility_config {
        cloudwatch_metrics_enabled = true
        metric_name                = "waf-${keys(var.acls)[count.index]}-${rule.key}"
        sampled_requests_enabled   = true
      }
    }
  }

  dynamic "rule" {

    for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS)

    content {
      name     = replace(rule.key, ".", "_")
      priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS, rule.key) + length(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)

      action {
        allow {}
      }

      statement {
        
        #https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
        byte_match_statement  {

          positional_constraint = "CONTAINS"
          search_string         = lower(rule.key)

          field_to_match {
            uri_path {}
          }

          text_transformation {
            priority = 0
            type     = "LOWERCASE"
          }
        }
      } 

      visibility_config {
        cloudwatch_metrics_enabled = true
        metric_name                = "waf-${keys(var.acls)[count.index]}-${replace(rule.key, ".", "_")}"
        sampled_requests_enabled   = true
      }
    }
  }


  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "waf-${keys(var.acls)[count.index]}"
    sampled_requests_enabled   = true
  }
}

If you are using Terraform 0.12.6 or later then you can use for_each instead of count to produce one instance for each element in your map:

resource "aws_route53_record" "proxy_dns" {
  for_each = var.account_name

  zone_id = infrastructure.zone_id
  name    = "proxy-${each.value}-dns"
  # ... etc ...
}

The primary advantage of for_each over count is that Terraform will identify the instances by the key in the map, so you'll get instances like aws_route53_record.proxy_dns["account1"] instead of aws_route53_record.proxy_dns[0], and so you can add and remove elements from your map in future with Terraform knowing which specific instance belongs to each element.

each.key and each.value in the resource type arguments replace count.index when for_each is used. They evaluate to the key and value of the current map element, respectively.