Nathan
Nathan

Reputation: 167

transforming / combining variables (maps) into locals in terraform

In order to simplify variable definition, I'm grouping subnets and attributes (like security groups) into a list of dictionaries... which then has a layer of dicts etc.. (or maps/objects)

Here is a sample definition JSON:

"subnets": [
{
  "name": "inside",
  "prefix": "10.248.0.0/28",
  "route_table": true,
  "nsg": true,
  "security_group_rules": [
    {
      "name": "AllowAllInbound",
      "properties": {
        "priority": 100,
        "access": "Allow",
        "direction": "Inbound",
        "protocol": "*",
        "sourcePortRange": "*",
        "destinationPortRange": "*",
        "sourceAddressPrefix": "*",
        "destinationAddressPrefix": "*"
      }
    }
  ]
}

With this single block, I'd like to be able to run through a loop to build out the subnets, then with a different resource block, build out the security groups. I'm having all sorts of issues being able to iterate over this.

I figured I should do something with locals so I can pull out what I need for each operation

on the subnet side:

resource "azurerm_subnet" "subnet" {
  for_each = var.subnets
  name                 = each.value.name
  resource_group_name  = azurerm_resource_group.hub_network_rg.name
  virtual_network_name = azurerm_virtual_network.hub_vnet.name
  address_prefixes     = [each.value.prefix] 
}

Doesn't work due to issues with variable types: I'm defining the 'subnets' variable as:

variable "subnets" {
  type = list(object({
    name = string
    prefix = string
    route_table = bool
    nsg = bool
    security_group_rules = map(list(string))
  }))
}

I know TF is loosely typed, but it doesn't want to iterate over the objects since it is encountering other things.

Options: 1.break this out into separate elements - I'll run into this again, just on a smaller scale. 2. Find a way to create s local.subnets map and a local.security_groups map that references the var.subnets I have.

Looking for help on #2.

something like:

locals {
  subnets = for k,v in var.subnets: [
    {
      name = var.subnet.name
      prefix = var.subnet.prefix
    }

I'm WAY off here, but I think that gets across the issue. My brain goes to Python and list/dict comprehension but I can figure out how to do this with TF. I feel it is right in front of me, I just can't piece together the syntax.

Upvotes: 0

Views: 1957

Answers (1)

Ervin Szilagyi
Ervin Szilagyi

Reputation: 16805

Terraform for_each accepts only set of strings or map of strings. Passing a var of something like a list simply wont work. What I suggest doing is the following:

# Create a JSON like variable for your subnets. Notice, this variable is a list, the type of it is inferred, we don't need to complicate ourselves here
variable "subnets" {
  default = [{
    "name" : "inside",
    "prefix" : "10.248.0.0/28",
    "route_table" : true,
    "nsg" : true,
    "security_group_rules" : [
      {
        "name" : "AllowAllInbound",
        "properties" : {
          "priority" : 100,
          "access" : "Allow",
          "direction" : "Inbound",
          "protocol" : "*",
          "sourcePortRange" : "*",
          "destinationPortRange" : "*",
          "sourceAddressPrefix" : "*",
          "destinationAddressPrefix" : "*"
        }
      }
    ]
  }]
}

# Create a local which maps the name of the the subnet to the subnet itself
locals {
  subnet_map =  { for s in var.subnets: s.name => s }
}

# Create the resources
resource "azurerm_subnet" "subnet" {
  for_each             = toset([
                             for s in var.subnets: s.name
                         ]) # Note, we create a set from the subnet names. We can use these names to read out properties from the map created above
  name                 = each.value.name
  resource_group_name  = azurerm_resource_group.hub_network_rg.name
  virtual_network_name = azurerm_virtual_network.hub_vnet.name
  address_prefixes     = [local.subnet_map[each.value].prefix] # We use the subnet names here
}

Update:

The reason why we need subnet_map is to have to following mapping:

subnet_map = {
  "inside" = {
    "name" = "inside"
    "nsg" = true
    "prefix" = "10.248.0.0/28"
    ...
  },
  # you have only one subnet as the input, but if you had more than one the map will contain it 
  "other_subnet" = {
  ... 
  }
}

Essentially, we want to map the name of each subnet to the subnet itself. The reason why we need this is, as I stated above, for_each accepts only sets of strings. The value for this toset([for s in var.subnets: s.name]) is a set of subnet names, so set("inside"), meaning that for_each will loop through the name of the subnets and create an azurerm_subnet for each subnet name. Obviously we need more information about the subnet, not only its name. We can get all the information we need from the subnet_map. This is because we can get an object out of it if we provide the name of the subnet. This is exactly what this statement does: local.subnet_map[each.value].prefix

Upvotes: 1

Related Questions