In the last post, we provided an introduction to the for in construct. This post provides more examples, shows local assignment, and covers how to set default attribute values. Remember, for is useful for data structure manipulation. Sometimes you may not get the data in the structure that is easy for you to work with, so understanding how to manipulate the data structure in Terraform can be quite useful.

Simple Functions Like Lookup, Keys, Contains

Have seen folks use the lookup function to check if Map elements in a List contain the key they’re looking. Here’s an example:

locals {
  list = [{a = 1}, {b = 2}, {a = 3}]
}
output "list" {
  value = [for m in local.list : m if lookup(m, "a", null) != null ]
}

Results in:

$ terraform apply
Outputs:

list = [{"a" = 1}, {"a" = 3}]

We can also achieve the same result with contains and keys.

locals {
  list = [{a = 1}, {b = 2}, {a = 3}]
}
output "list" {
  value = [for m in local.list2 : m if contains(keys(m), "a") ]
}

Also results in:

$ terraform apply
Outputs:

list = [{"a" = 1}, {"a" = 3}]

Local to Local Conversion

It’s worth pointing out that you can use one local variable to set another local variable. In a normal procedural programming language this is obvious. But with Terraform being declarative, it can throw some people off. Constructing a new local variable can sometimes clean up your code. Let’s say we have this data structure that holds:

locals {
  groups = {
    example0 = {
      description = "sg description 0"
      rules = [{
        description = "rule description 0",
      },{
        description = "rule description 1",
      }]
    },
    example1 = {
      description = "sg description 1"
      rules = [{
        description = "rule description 0",
      },{
        description = "rule description 1",
      }]
    }
  }
}
output "groups" {
  value = local.groups
}

And we needed to get all the rules as a Map with the same groups keys. We can do that with a simple for expression and assigning the result to another local variable.

locals {
  groups = {
    example0 = {
    # ...
    }
  }
  rules = { for k,v in local.groups : k => v.rules }
}
output "rules" {
  value = local.rules
}

That results in:

$ terraform apply
Outputs:

rules = {
  "example0" = [
    {
      "description" = "rule description 0"
    },
    {
      "description" = "rule description 1"
    },
  ]
  "example1" = [
    {
      "description" = "rule description 0"
    },
    {
      "description" = "rule description 1"
    },
  ]
}

Now we’re able to use a more straightforward flatter structure local.rules instead of local.groups.

Default Values and Removing Duplication

Back in the Terraform Intro 6: Nested Loops post, we showed you a direct assignment approach which rid the code of an extra inner loop. It would also tell Terraform to remove ingress rules properly. It came with a cost, though: duplication. We had to set these extra attributes like ipv6_cidr_blocks and prefix_list_ids.

Combining our gained knowledge from the previous posts on for_each and for loops, we are now posed to remove that duplication. Here’s the code:

variable "groups" {
  default = {
    example0 = {
      description = "sg description 0"
    },
    example1 = {
      description = "sg description 1"
    }
  }
}
variable "rules" {
  default = {
    example0 = [{
      description      = "rule description 0"
      to_port          = 80
      from_port        = 80
      cidr_blocks      = ["10.0.0.0/16"]
    },{
      description      = "rule description 1"
      to_port          = 80
      from_port        = 80
      cidr_blocks      = ["10.1.0.0/16"]
    }]
    example1 = [] # empty Array removes rules
  }
}
locals {
  rules = {
    for k,v in var.rules:
      k => [
        for i in v:
        merge({
          ipv6_cidr_blocks = null
          prefix_list_ids  = null
          security_groups  = null
          protocol         = "tcp"
          self             = null
        }, i)
      ]
  }
}
resource "aws_security_group" "this" {
  for_each    = var.groups

  name        = each.key # top-level key is security group name
  description = each.value.description

  ingress = contains(keys(local.rules), each.key) ? local.rules[each.key] : []  # List of Maps with rule attributes
}
output "rules" {
  value = local.rules
}
Expand to see terraform plan results.
$ terraform apply
Outputs:

rules = {
  "example0" = [
    {
      "cidr_blocks" = [
        "10.0.0.0/16",
      ]
      "description" = "rule description 0"
      "from_port" = 80
      "protocol" = "tcp"
      "to_port" = 80
    },
    {
      "cidr_blocks" = [
        "10.1.0.0/16",
      ]
      "description" = "rule description 1"
      "from_port" = 80
      "protocol" = "tcp"
      "to_port" = 80
    },
  ]
  "example1" = []
}

The user would just set the rules variable. The code uses 2 for loops to set default values for the Maps within the rules structure. The merge function is used to set the default values. The custom user-defined values are passed through, making this code quite flexible.

Note, one tricky thing is if you try to see the default values for rules with terraform output, you will not set the null values. Terraform output does not display null values.

Other Examples Worth Pointing Out

Cleanup

Note, remember to clean up the resources:

terraform destroy

Summary

This post covered more examples with the for loop construct, which is useful for data structure manipulation. We used our combined knowledge from previous to set default values. We’ve covered a fair amount of Terraform HCL fundamentals.

Want It to be Easier to Work with Terraform?

Check out Terraspace: The Terraform Framework.

The Terraform HCL Language Intro Tutorials