18

Im trying to iterate through a variable type map and i'm not sure how to

This is what i have so far

In my main.tf:

resource "aws_route_53_record" "proxy_dns" {
  count = "${length(var.account_name)}"
  zone_id = "${infrastructure.zone_id}"
  name = "proxy-${element(split(",", var.account_name), count.index)}-dns
  type = CNAME
  ttl = 60
  records = ["{records.dns_name}"]
}

And in my variables.tf

variable "account_name" {
  type = "map"
  default = {
    "account1" = "accountA"
    "account2" = "accountB"
  }
}

I want to be able to create multiple resources with the different account names

Philipp Kyeck
  • 18,402
  • 15
  • 86
  • 123
TheWalkingMalteser
  • 561
  • 1
  • 9
  • 25

3 Answers3

41

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.

Martin Atkins
  • 62,420
  • 8
  • 120
  • 138
  • Could I exclude an element using for_each? – c4f4t0r Jun 24 '20 at 06:16
  • You can't do that using `for_each` itself, but you _can_ produce a new map with some of the items filtered out using a [`for` expression](https://www.terraform.io/docs/configuration/expressions.html#for-expressions). That's a different question than I was answering here so I can't go into details in a comment here but if you give that a try and have further questions I'd suggest starting a new question here in Stack Overflow to discuss it. – Martin Atkins Jun 24 '20 at 22:33
2

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
  }
}
jlo-gmail
  • 4,453
  • 3
  • 37
  • 64
-1

Make the variable a list instead of a map. Maps are used to reference a name to a value. Lists are better for iterating over via a count method.

variable "account_name" {
  type = "list"
  default = {"accountA","accountB"}
}
resource "aws_route_53_record" "proxy_dns" {
    count = "${length(var.account_name)}"
    zone_id = "${infrastructure.zone_id}"
    name = "proxy-${element(var.account_name, count.index)}-dns
    type = CNAME
    ttl = 60
    records = ["{records.dns_name}"]
}
Michael Quale
  • 568
  • 3
  • 16