skip to Main Content

Context: This is continuation of what I was doing at below post.

what is correct way of reference to value in object/map type value in terraform

Objective: Trying to create subnets in loop using for_each in terraform

My terraform.tfvars.json: (only I have mentioned variable realated to my problem I am facing)

 "subnets" : {
        "Dev" :
        [  
        {"gw_snet":{
          "name"                 : "GatewaySubnet",
          "address_prefixes"     : ["10.1.1.0/24"]
        },
        "dns-snet" : {
          "name"                 : "InboundDNSSubnet",
          "address_prefixes"     : ["10.1.2.0/24"]
        },
        "common_snet" : {
          "name"                 : "Common",
          "address_prefixes"     : ["10.1.3.0/24"]
        },
        "clientdata_snet" : {
          "name"                 : "ClientDataSubnet",
          "address_prefixes"     : ["10.1.4.0/20"]
        }}
        ],
        "Stage" :
        [  
        {"gw_snet":{
          "name"                 : "GatewaySubnet",
          "address_prefixes"     : ["10.2.1.0/24"]
        },
        "dns-snet" : {
          "name"                 : "InboundDNSSubnet",
          "address_prefixes"     : ["10.2.2.0/24"]
        },
        "common_snet" : {
          "name"                 : "Common",
          "address_prefixes"     : ["10.2.3.0/24"]
        },
        "clientdata_snet" : {
          "name"                 : "ClientDataSubnet",
          "address_prefixes"     : ["10.2.4.0/20"]
        }}
        ],
        "Prod" :
        [  
        {"gw_snet":{
          "name"                 : "GatewaySubnet",
          "address_prefixes"     : ["10.3.1.0/24"]
        },
        "dns-snet" : {
          "name"                 : "InboundDNSSubnet",
          "address_prefixes"     : ["10.3.2.0/24"]
        },
        "common_snet" : {
          "name"                 : "Common",
          "address_prefixes"     : ["10.3.3.0/24"]
        },
        "clientdata_snet" : {
          "name"                 : "ClientDataSubnet",
          "address_prefixes"     : ["10.3.4.0/20"]
        }}
        ]
      }  

My vnet creation code:

resource "azurerm_virtual_network" "vnet" {
  name                = var.hub_vnet_name
  location            = azurerm_resource_group.rg[0].location
  resource_group_name = azurerm_resource_group.rg[0].name
  for_each = {for k,v in var.vnet_address_space: k=>v if k == "Dev"}
  address_space       =  each.value
  dns_servers         = var.dns_servers
  tags     = {
    environment = "${var.env}"
    costcentre = "14500"
  }
  dynamic "ddos_protection_plan" {
    for_each = local.if_ddos_enabled

    content {
      id     = azurerm_network_ddos_protection_plan.ddos[0].id
      enable = false
    }
  }
}

I am trying to create subnets with for_each like below

  resource "azurerm_subnet" "mysubnet" {
  for_each = {for k,v in var.subnets: k=>v if k == "Dev"}
  name                 = each.value.name
  address_prefixes     = [each.value.address_prefixes]
  virtual_network_name = var.hub_vnet_name
  resource_group_name  = var.resource_group_name
}

Error I get:

No errors in my terraform plan, its not creating vnet also as my plan is not validated.

Is my subnets variable definition ok ?

I guess the below is not working at all.. correct way of accessing this nested value ?

  name                 = each.value.name
  address_prefixes     = [each.value.address_prefixes]

Please help me to identify issue

2

Answers


  1. The closest solution to which I got (based on your input) is this:

    locals {
       net_subnets = merge([
        for env, network in var.subnets : {
          for k, v in network[0] :
          "${k}-${v.name}" => {
            subnet_name      = v.name
            address_prefixes = v.address_prefixes
          } if env == "Dev"
      }]...)
    }
    

    Here merge built-in function [1] and expanding function argument [2] are used. This will result in the output:

    > local.net_subnets
    {
      "clientdata_snet-ClientDataSubnet" = {
        "address_prefixes" = [
          "10.1.4.0/20",
        ]
        "subnet_name" = "ClientDataSubnet"
      }
      "common_snet-Common" = {
        "address_prefixes" = [
          "10.1.3.0/24",
        ]
        "subnet_name" = "Common"
      }
      "dns-snet-InboundDNSSubnet" = {
        "address_prefixes" = [
          "10.1.2.0/24",
        ]
        "subnet_name" = "InboundDNSSubnet"
      }
      "gw_snet-GatewaySubnet" = {
        "address_prefixes" = [
          "10.1.1.0/24",
        ]
        "subnet_name" = "GatewaySubnet"
      }
    }
    

    That means you can do for_each on that variable value, where the keys will be a combination of the *-snet key and name from the var.subnets variable. Then, the resource code block should look like:

    resource "azurerm_subnet" "mysubnet" {
      for_each             = local.net_subnets
      name                 = each.value.subnet_name
      address_prefixes     = each.value.address_prefixes
      virtual_network_name = var.hub_vnet_name
      resource_group_name  = var.resource_group_name
    }
    

    In order to avoid the issue with pre-computed values required for for_each, it might be the best to use the locals block and just use the same logic for the pipeline, i.e., instead of using if env == "Dev", just use if env == var.env. Or alternatively define three local variables for each of the environments.


    [1] https://www.terraform.io/language/functions/merge

    [2] https://www.terraform.io/language/expressions/function-calls#expanding-function-arguments

    Login or Signup to reply.
  2. I think this is seeming much more complex than it really is. What you seek, I believe, is the lookup function. Just lookup your var.env in the map. Your current data structure doesn’t make much sense. I show it here as locals with just few enough to show the structure.

    locals {
      subnets = {
        "Dev" = [
          {
            "some_name_a" = {
              name             = "SomeOtherNameA",
              address_prefixes = ["10.1.1.0/24"]
            },
            "some_name_b" = {
              name             = "SomeOtherNameB",
              address_prefixes = ["10.1.2.0/24"]
            }
          }
        ],
        "Stage" = [
          {
            "some_name_a" = {
              name             = "SomeOtherNameA",
              address_prefixes = ["10.1.1.0/24"]
            },
            "some_name_b" = {
              name             = "SomeOtherNameB",
              address_prefixes = ["10.1.2.0/24"]
            }
          }
        ]
      }
    }
    

    So each environment section is a list of length one of an object with a key per some network name you don’t need housing an object that actually defines your configuration. What you need is much more simple.

    locals {
      subnets = {
        "Dev" = [
          {
            name             = "SomeOtherNameA",
            address_prefixes = ["10.1.1.0/24"]
          },
          {
            name             = "SomeOtherNameB",
            address_prefixes = ["10.1.2.0/24"]
          }
        ],
        "Stage" = [
          {
            name             = "SomeOtherNameA",
            address_prefixes = ["10.1.1.0/24"]
          },
          {
            name             = "SomeOtherNameB",
            address_prefixes = ["10.1.2.0/24"]
          }
        ]
      }
    }
    

    In this case you can use:

    resource "azurerm_subnet" "mysubnet" {
      for_each = { for v in lookup(var.subnets, var.env, []) : v.name => v.address_prefixes }
    
      name                 = each.key
      address_prefixes     = each.value
      virtual_network_name = var.hub_vnet_name
      resource_group_name  = var.resource_group_name
    }
    

    Or even more simple, given your data:

    locals {
      subnets_simple = {
        "Dev" = {
          "SomeOtherNameA" = ["10.1.1.0/24"]
          "SomeOtherNameB" = ["10.1.2.0/24"]
        },
        "Stage" = {
          "SomeOtherNameA" = ["10.1.1.0/24"]
          "SomeOtherNameB" = ["10.1.2.0/24"]
        },
      }
    }
    

    In this case, you should be able to simply use:

    resource "azurerm_subnet" "mysubnet" {
      for_each = lookup(var.subnets, var.env, {})
    
      name                 = each.key
      address_prefixes     = each.value
      virtual_network_name = var.hub_vnet_name
      resource_group_name  = var.resource_group_name
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search