Is there a way to use a function to run a loop on an input object based a range

I originally posted this on Stack Overflow because I was not aware of this forum.

I have created a module with the following

resource "aci_ranges" "add_vlan" {
  for_each      = local.vlan_list
  alloc_mode    = each.value["alloc_mode"]
  annotation    = each.value["annotation"]
  name_alias    = each.value["name_alias"]
  vlan_pool_dn  = each.value["vlan_pool"]
  role          = each.value["role"]
  from          = "vlan-${each.value["from"]}"
  to            = "vlan-${each.value["to"]}"
}

From Here I have defined a variables file to make it so users don’t have to enter every variable… they can accept defaults

terraform {
  experiments = [module_variable_optional_attrs]
}

variable "vlan_list" {
  description = "Add VLANs to VLAN Pools"
  type = map(object({
    alloc_mode  = optional(string)
    annotation  = optional(string)
    from        = optional(number)
    name_alias  = optional(string)
    role        = optional(string)
    to          = optional(number)
    vlan_pool   = optional(string)
  }))
}

locals {
  vlan_list = {
    for k, v in var.vlan_list : k => {
      alloc_mode  = coalesce(v.alloc_mode, "static")
      annotation  = (v.annotation != null ? v.annotation : "")
      from        = (v.from != null ? v.from : 1)
      name_alias  = (v.name_alias != null ? v.name_alias : "")
      role        = coalesce(v.role, "external")
      to          = coalesce(v.to, 1)
      vlan_pool   = (v.vlan_pool != null ? v.vlan_pool : "")
    }
  }
}

Here is what a user would enter then to consume the module (note that I know the for statements are not correct at all but are just a reference to what I would like to accomplish):

module "vlan_list" {
  depends_on  = [module.vlan_pools]
  source      = "../modules/add_vlans"
   vlan_list = {
      for i in range(1, 100):
        "access" => {
          vlan_pool   = module.vlan_pools.vlan_pool["access"]
          from        = i
          to          = i
        }...
      for i in ranges([1000-1200], [1300-1400]):
        "vmm_dynamic" => {
          alloc_mode  = "dynamic"
          vlan_pool   = module.vlan_pools.vlan_pool["vmm_dynamic"]
          from        = i
          to          = i
        }...
      for i in list[4, 100, 101]:
        "l3out" => {
          vlan_pool   = module.vlan_pools.vlan_pool["l3out"]
          from        = i
          to          = i
        }...
   }
}

When the resource creates the entries, from the API, if I do it in a range as shown below it can be problematic. In example; if someone needed to change the range for the first pool (in example) to 1-50,52-99, it would delete the entire VLAN range and then re-create it. whereas if they are creating the entry with each entry being created individually via a range loop then they can delete individual entries without affecting the entire pool.

I can do the following and it works fine… but as mentioned above; being able to add the VLANs individually from a loop would be preferable.

module "vlan_list" {
  depends_on  = [module.vlan_pools]
  source      = "../modules/add_vlans"
  vlan_list = {
    "access" = {
      vlan_pool   = module.vlan_pools.vlan_pool["access"]
      from        = 1
      to          = 99
    },
    "vmm_dynamic" = {
      alloc_mode  = "dynamic"
      vlan_pool   = module.vlan_pools.vlan_pool["vmm_dynamic"]
      from        = 1000
      to          = 1199
    },
    "l3out_1" = {
      vlan_pool   = module.vlan_pools.vlan_pool["l3out"]
      from        = 4
      to          = 4
    },
    "l3out_2" = {
      vlan_pool   = module.vlan_pools.vlan_pool["l3out"]
      from        = 100
      to          = 101
    },
  }
}

Thanks in advance for help on this.

Hi @scottyso!

With more complex object structures, I find it can be tricky to balance the iteration behavior of a collection functions with the provider. I try to avoid the passing of maps of object types into modules. As a module user, I’d be more inclined to use a module for_each to attach a list of VLANs to each pool.

Needs to be compiled, not validated. Something like this:

module "vlan_list" {
  for_each        = module.vlan_pools.vlan_pool
  depends_on      = [module.vlan_pools]
  source          = "../modules/add_vlans"
  vlan_pool       = each.key
  vlan_list       = local.vlan_list[each.key]
}

If you do need to keep the complex input, I think you can achieve the reference with the merge function.

module "vlan_list" {
  depends_on  = [module.vlan_pools]
  source      = "../modules/add_vlans"
  vlan_list   = merge(
    {
      for i in range(1, 100) :
      "access" => {
        vlan_pool = "module.vlan_pools.vlan_pool["access"]"
        from      = i
        to        = i
      }...
    },
    {
      for i in concat(range(1000, 1200), range(1300, 1400)) :
      "vmm_dynamic" => {
        alloc_mode = "dynamic"
        vlan_pool  = module.vlan_pools.vlan_pool["vmm_dynamic"]
        from = i
        to   = i
      }...
    },
    {
      for i in list(4, 100, 101) :
      "l3out" => {
        vlan_pool = module.vlan_pools.vlan_pool["l3out"]
        from = i
        to   = i
      }...
    }
  )
}

Looking forward to hearing what works out!

Thank you joatmon08… I will start testing this and see if it works. What you have shown makes sense and it seems to be what I have been missing… Let me see if I can get it to work.

So I believe I need to keep the complex input structure because it is with object type that I am able to have the different input types of number, string, Boolean etc… this is just one example where I am only using number and string.

I tried what was shared in your second example and It gives me the feedback that I need an object type for the variable. I am sorry I don’t really understand how it is an object without the function and then not an object type with the function… that is beyond what I understand.

Here is what I defined:

module "vlan_list" {
  depends_on  = [module.vlan_pools]
  source      = "../modules/add_vlans"
  vlan_list = merge(
    {
      for i in range(1, 100) :
      "access" => {
        vlan_pool   = module.vlan_pools.vlan_pool["access"]
        from        = i
        to          = i
      }...
    },
    {
      for i in concat(range(1000, 1200), range(1300, 1400)) :
      "vmm_dynamic" => {
        alloc_mode  = "dynamic"
        vlan_pool   = module.vlan_pools.vlan_pool["vmm_dynamic"]
        from        = i
        to          = i
      }...
    },
    {
      for i in list(4, 100, 101) :
      "l3out" => {
        vlan_pool   = module.vlan_pools.vlan_pool["l3out"]
        from        = i
        to          = i
      }...
    }
  )
}

Here is the error message I receive when I try to run the plan

terraform plan -out=main.plan

Warning: Experimental feature "module_variable_optional_attrs" is active

  on ../modules/add_vlans/variables.tf line 2, in terraform:
   2:   experiments = [module_variable_optional_attrs]

Experimental features are subject to breaking changes in future minor or patch
releases, based on feedback.

If you have feedback on the design of this feature, please open a GitHub issue
to discuss it.

(and 7 more similar warnings elsewhere)


Error: Invalid value for module argument

  on pools_vlan.tf line 30, in module "vlan_list":
  30:   vlan_list = merge(
  31:     {
  32:       for i in range(1, 100) :
  33:       "access" => {
  34:         vlan_pool   = module.vlan_pools.vlan_pool["access"]
  35:         from        = i
  36:         to          = i
  37:       }...
  38:     },
  39:     {
  40:       for i in concat(range(1000, 1200), range(1300, 1400)) :
  41:       "vmm_dynamic" => {
  42:         alloc_mode  = "dynamic"
  43:         vlan_pool   = module.vlan_pools.vlan_pool["vmm_dynamic"]
  44:         from        = i
  45:         to          = i
  46:       }...
  47:     },
  48:     {
  49:       for i in list(4, 100, 101) :
  50:       "l3out" => {
  51:         vlan_pool   = module.vlan_pools.vlan_pool["l3out"]
  52:         from        = i
  53:         to          = i
  54:       }...
  55:     }
  56:   )

The given value is not suitable for child module variable "vlan_list" defined
at ../modules/add_vlans/variables.tf:5,1-21: element "access": object
required.

I took a look at the output and it seems that it is a map(list(object())). I think if you change the vlan_list variable type to:

variable "vlan_list" {
  description = "Add VLANs to VLAN Pools"
  type = map(list(object({
    alloc_mode  = optional(string)
    annotation  = optional(string)
    from        = optional(number)
    name_alias  = optional(string)
    role        = optional(string)
    to          = optional(number)
    vlan_pool   = optional(string)
  })))
}

It may change some iteration you do in the child module, hopefully it is the correct type structure.