azureloopsterraformterraform-provider-azure

Terraform should I try to merge or flatten a complex variable?


Am trying to use Terraform to deploy multiple storage account with variable storage containers. But I only want to call the nodule once, so the storage account variable is a map of objects. The storage container(s) is a nested map of objects inside the storage account map.

It looks like this:

variables.tf

variable "location" {
  type        = string
  description = "Location for storage account resource."
}

variable "resource_group_name" {
  type        = string
  description = "Resource group that will contain the resource."
}

variable "storage_account" {
  type = map(object({
    name                          = string
    access_tier                   = string
    account_kind                  = string
    account_replication_type      = string
    account_tier                  = string
    public_network_access_enabled = bool

    containers = optional(map(object({
      name = string
      container_access_type = optional(string, "private")
    })))

    network_rules = list(object({
      default_action             = string
      bypass                     = optional(set(string))
      ip_rules                   = optional(set(string))
      virtual_network_subnet_ids = optional(set(string))
    }))

  }))
}

The resource module code looks like this:

main.tf

resource "azurerm_storage_account" "storage" {
  for_each = var.storage_account

  name                          = each.value.name
  resource_group_name           = var.resource_group_name
  location                      = var.location
  account_tier                  = each.value.account_tier
  account_replication_type      = each.value.account_replication_type
  account_kind                  = each.value.account_kind
  access_tier                   = each.value.access_tier
  public_network_access_enabled = each.value.public_network_access_enabled != null ? each.value.public_network_access_enabled : false

}

locals {
  containers_list = flatten([
    for key, value in var.storage_account : [
      for container in value.containers : {
        name                 = container
        storage_account_name = azurerm_storage_account.storage[key].name
      }
    ]
  ]...)
}

resource "azurerm_storage_container" "storage" {
  #for_each = { for container in local.containers_list : container.name.name => container }
  for_each = tomap({
    for container in local.containers_list : container.name.name => container
  })

  name                 = each.value.container.name
  storage_account_name = each.value.storage_account_name
}

I call the module using this:

main.tf

module "storage_account" {
  source = "./modules/azurerm_storage_account"

  resource_group_name = "rg-temp"
  location            = "uksouth"
  tags                = {}

  storage_account = {
    "storage01" = {
      name                          = "storagerandom0001"
      account_kind                  = "StorageV2"
      account_tier                  = "Standard"
      account_replication_type      = "LRS"
      access_tier                   = "Hot"
      public_network_access_enabled = true
      network_rules                 = []
      containers = {
        "container-aa" = {
          name = "random01"
          container_access_type = "private"
        },
        "container-ab" = {
          name = "random02"
          container_access_type = "private"
        }
      }
    },
    "storage02" = {
      name                          = "storagerandom0002"
      account_kind                  = "StorageV2"
      account_tier                  = "Standard"
      account_replication_type      = "LRS"
      access_tier                   = "Hot"
      public_network_access_enabled = true
      network_rules                 = []
      containers = {
        "container-01" = {
          name = "container01"
          container_access_type = "private"
        },
        "container-02" = {
          name = "container02"
          container_access_type = "private"
        }
      }
    }
  }
}

When I run this I get the error:

│ Error: Too many function arguments
│
│   on modules/azurerm_storage_account/main.tf line 17, in locals:
│   17:   containers_list = flatten([
│   18:     for key, value in var.storage_account : [
│   19:       for container in value.containers : {
│   20:         name                 = container
│   21:         storage_account_name = azurerm_storage_account.storage[key].name
│   22:       }
│   23:     ]
│   24:   ]...)
│     ├────────────────
│     │ while calling flatten(list)
│     │ azurerm_storage_account.storage is object with 2 attributes
│     │ var.storage_account is map of object with 2 elements
│
│ Function "flatten" expects only 1 argument(s).

From this question (which looks very similar to what I am trying to do):

How to access a sequence in a map inside a for_each loop in Terraform

I wonder do I maybe need to merge rather than flatten?

But if I change to merge I get this error:

╷
│ Error: Error in function call
│
│   on modules/azurerm_storage_account/main.tf line 17, in locals:
│   17:   containers_list = merge([
│   18:     for key, value in var.storage_account : [
│   19:       for container in value.containers : {
│   20:         name                 = container
│   21:         storage_account_name = azurerm_storage_account.storage[key].name
│   22:       }
│   23:     ]
│   24:   ]...)
│     ├────────────────
│     │ while calling merge(maps...)
│     │ azurerm_storage_account.storage is object with 2 attributes
│     │ var.storage_account is map of object with 2 elements
│
│ Call to function "merge" failed: arguments must be maps or objects, got "tuple".
╵

This is my first time having to nest a loop (or use a merge or a flatten) so I'm not doing very well.

Can any help and also try to explain in simple terms what I need to do/am doing wrong please?

Thanks.


Solution

  • The flatten function is expecting a single argument as the error is saying while calling flatten(list) [...] Function "flatten" expects only 1 argument(s). The error arise because of the ... operator you're adding to the generated list of lists. Just removing it should be fine.

    You should then change your local value to:

      containers_list = flatten([
        for key, value in var.storage_account : [
          for _, container in value.containers : {
            name                 = container
            storage_account_name = azurerm_storage_account.storage[key].name
          }
        ]
      ])
    
    

    Also, since containers is optional, and may be null, I'd provide it with an empty map by default:

    variable "storage_account" {
      type = map(object({
        name                          = string
        access_tier                   = string
        account_kind                  = string
        account_replication_type      = string
        account_tier                  = string
        public_network_access_enabled = bool
    
        containers = optional(map(object({
          name = string
          container_access_type = optional(string, "private")
        })), {})
    
        network_rules = list(object({
          default_action             = string
          bypass                     = optional(set(string))
          ip_rules                   = optional(set(string))
          virtual_network_subnet_ids = optional(set(string))
        }))
    
      }))
    }