Invalid value for "v" parameter: cannot convert object to set of any single type

First time posting here… I’m trying out Terraform, and I’m trying to setup an environment from scratch to play around with it.

I’ve seen some ideas around using modules, so I’m trying to modulise both my vnet and snets, based on my tfvars file.

I’ve hit a snag and wondered if someone could either direct me to where I can get the answer, or point out the likely obvious mistake I’ve made.

My modules look like this:

Modules:
vnet main.tf:

resource "azurerm_virtual_network" "vnet" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
address_space = var.address_space
dynamic "subnet" {
for_each = toset(var.subnets)
content {
name = subnet.value.name
address_prefix = subnet.value.address_prefix
security_group = subnet.value.security_group
}
}
}

snet main.tf:

resource "azurerm_subnet" "subnet" {
for_each = var.subnets
name = each.value.name
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
address_prefixes = each.value.address_prefixes
service_endpoints = each.value.service_endpoints
dynamic "delegation" {
  for_each = toset(each.value.delegation)
content {
  name = delegation.value.name
  service_delegation {
    name = delegation.value.service_delegation
    actions = delegation.value.service_delegation_actions
  }
 }
}
}

And the main.tf where I use them looks like:

module "vnet_uks" {
source = "./modules/vnet"
name = var.vnetuksname
location = var.vnetukslocation
resource_group_name = var.vnetuksrg
address_space = var.uksaddress_space
subnets = var.subnets
}

module "subnets" {
source = "./modules/subnets"
subnets = var.subnets
resource_group_name = var.resource_group_name
virtual_network_name = module.vnet_uks.name
depends_on = [ module.vnet_uks ]
}

and the tfvars file section for this looks like:

#Virtual Networks and Subnets for UKS
vnetukslocation = "uksouth"
vnetuksrg = "rg-uks-compute"
uksaddress_space = ["10.0.0.0/16", "10.1.0.0/24"]
vnetuksname = "vnet-uks-01"
subnets = {
uks_vms = {name = "snet_uks_vms", address_prefix = ["10.1.1.0/24"], enforce_private_link_endpoint_policies = false, delegations = [], service_endpoints = ["Microsoft.AzureActiveDirectory", "Microsoft.KeyVault", "Microsoft.Sql", "Microsoft.Storage", "Microsoft.Web"] }
}

When I run a tfplan, I get the following error:

Error: Invalid function argument

  on modules\vnet\main.tf line 9, in resource "azurerm_virtual_network" "vnet":
   9: for_each = toset(var.subnets)
    ├────────────────
    │ while calling toset(v)
    │ var.subnets is object with 5 attributes

Invalid value for "v" parameter: cannot convert object to set of any single type.

I’m not sure what else to do - I’ve tried replacing the toset with tomap, but that wants every variable declaring separately. Could someone help point me in the right direction?

Thanks

Hi @theangrytech,

Do your variable blocks – and in particular, your variable "subnets" blocks across both modules, declare a suitable type constraint for that value?

It seems like you are intending to pass a set of objects but you have passed a single object instead. But the fact that this managed to get all the way to that toset call suggests that you are missing some type constraints on the variables themselves, which is where I would expect to catch that sort of problem. I’m also assuming from what you’ve said that you’d be open to using a map here instead, and I think that would be better here because it will save an extra step of transforming the set into map to use it with for_each.

Here’s an example declaration for the variable with a type constraint I’ve guessed based on how you appear to be using this value:

variable "subnets" {
  type = map(object({
    name = string
    address_prefixes = set(string)
    enforce_private_link_endpoint_policies = optional(bool, false)
    delegations = optional(map(object({
      name = string
      service_delegation = string
      service_delegation_actions = set(string)
    })), {})
    service_endpoints = optional(set(string), [])
  }))
}

If you use a variable declaration like the above then Terraform will guarantee to provide a value of this type or return a type checking error for an invalid value in the .tfvars file. Since you are passing this through two levels of module you will need to place this declaration in both modules that accept subnets as input.

for_each accepts maps directly, so there’s no longer any need for explicit conversation once you use a suitable type constraint:

  for_each = var.subnets

In the rest of the resource configuration, each.key will refer to each subnet’s unique key and you can use each.value to refer to the corresponding object to populate all of the other attributes.

The tfvars definition would look something like this:

subnets = {
  uks_vms = {
    name = "snet_uks_vms"
    address_prefix = ["10.1.1.0/24"]
    service_endpoints = [
      "Microsoft.AzureActiveDirectory",
      "Microsoft.KeyVault",
      "Microsoft.Sql",
      "Microsoft.Storage",
      "Microsoft.Web",
    ]
  }
}

(since the type constraint declares some attributes as optional, you can leave those unset in tfvars and Terraform will automatically set them to the specified default values.)