Terraform nested Azure resource isn't quite nested correctly

Hi.

I’m trying to create Azure storage accounts using a module that takes a list of storage accounts to create. No problem with this.

I’m am now trying to extend this a little by permitting a list of storage containers for each storage account to be supplied as sub-components of each storage account.

The variable structure looks like this:

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(list(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 module code looks like this:

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

  name                          = each.key
  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 = merge([
    for storage_account_name, details in var.storage_account: {
      for container in details.containers:
      "${storage_account_name}-${container.name}" =>
        merge(details, {
          storage_account_name = storage_account_name
          container = container
        })
    }
  ]...)
}

resource "azurerm_storage_container" "storage" {
  depends_on = [ azurerm_storage_account.storage ]

  for_each = local.containers_list

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

output "test_output" {
  value = local.containers_list
}

And my calling code looks like this:

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 = [
        {
          name = "container01",
          container_access_type = "private"
        },
        {
          name = "container02",
          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 = [
        {
          name = "container03"
          container_access_type = "private"
        },
        {
          name = "container04"
          container_access_type = "private"
        }
      ]
    }
  }
}

The TF plan runs with this output:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.storage_account.azurerm_storage_account.storage["storage01"] will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                        = "Hot"
      + account_kind                       = "StorageV2"
      + account_replication_type           = "LRS"
      + account_tier                       = "Standard"
      + allow_nested_items_to_be_public    = true
      + cross_tenant_replication_enabled   = true
      + default_to_oauth_authentication    = false
      + dns_endpoint_type                  = "Standard"
      + enable_https_traffic_only          = (known after apply)
      + https_traffic_only_enabled         = (known after apply)
      + id                                 = (known after apply)
      + infrastructure_encryption_enabled  = false
      + is_hns_enabled                     = false
      + large_file_share_enabled           = (known after apply)
      + local_user_enabled                 = true
      + location                           = "uksouth"
      + min_tls_version                    = "TLS1_2"
      + name                               = "storage01"
      + nfsv3_enabled                      = false
      + primary_access_key                 = (sensitive value)
      + primary_blob_connection_string     = (sensitive value)
      + primary_blob_endpoint              = (known after apply)
      + primary_blob_host                  = (known after apply)
      + primary_blob_internet_endpoint     = (known after apply)
      + primary_blob_internet_host         = (known after apply)
      + primary_blob_microsoft_endpoint    = (known after apply)
      + primary_blob_microsoft_host        = (known after apply)
      + primary_connection_string          = (sensitive value)
      + primary_dfs_endpoint               = (known after apply)
      + primary_dfs_host                   = (known after apply)
      + primary_dfs_internet_endpoint      = (known after apply)
      + primary_dfs_internet_host          = (known after apply)
      + primary_dfs_microsoft_endpoint     = (known after apply)
      + primary_dfs_microsoft_host         = (known after apply)
      + primary_file_endpoint              = (known after apply)
      + primary_file_host                  = (known after apply)
      + primary_file_internet_endpoint     = (known after apply)
      + primary_file_internet_host         = (known after apply)
      + primary_file_microsoft_endpoint    = (known after apply)
      + primary_file_microsoft_host        = (known after apply)
      + primary_location                   = (known after apply)
      + primary_queue_endpoint             = (known after apply)
      + primary_queue_host                 = (known after apply)
      + primary_queue_microsoft_endpoint   = (known after apply)
      + primary_queue_microsoft_host       = (known after apply)
      + primary_table_endpoint             = (known after apply)
      + primary_table_host                 = (known after apply)
      + primary_table_microsoft_endpoint   = (known after apply)
      + primary_table_microsoft_host       = (known after apply)
      + primary_web_endpoint               = (known after apply)
      + primary_web_host                   = (known after apply)
      + primary_web_internet_endpoint      = (known after apply)
      + primary_web_internet_host          = (known after apply)
      + primary_web_microsoft_endpoint     = (known after apply)
      + primary_web_microsoft_host         = (known after apply)
      + public_network_access_enabled      = true
      + queue_encryption_key_type          = "Service"
      + resource_group_name                = "rg-temp"
      + secondary_access_key               = (sensitive value)
      + secondary_blob_connection_string   = (sensitive value)
      + secondary_blob_endpoint            = (known after apply)
      + secondary_blob_host                = (known after apply)
      + secondary_blob_internet_endpoint   = (known after apply)
      + secondary_blob_internet_host       = (known after apply)
      + secondary_blob_microsoft_endpoint  = (known after apply)
      + secondary_blob_microsoft_host      = (known after apply)
      + secondary_connection_string        = (sensitive value)
      + secondary_dfs_endpoint             = (known after apply)
      + secondary_dfs_host                 = (known after apply)
      + secondary_dfs_internet_endpoint    = (known after apply)
      + secondary_dfs_internet_host        = (known after apply)
      + secondary_dfs_microsoft_endpoint   = (known after apply)
      + secondary_dfs_microsoft_host       = (known after apply)
      + secondary_file_endpoint            = (known after apply)
      + secondary_file_host                = (known after apply)
      + secondary_file_internet_endpoint   = (known after apply)
      + secondary_file_internet_host       = (known after apply)
      + secondary_file_microsoft_endpoint  = (known after apply)
      + secondary_file_microsoft_host      = (known after apply)
      + secondary_location                 = (known after apply)
      + secondary_queue_endpoint           = (known after apply)
      + secondary_queue_host               = (known after apply)
      + secondary_queue_microsoft_endpoint = (known after apply)
      + secondary_queue_microsoft_host     = (known after apply)
      + secondary_table_endpoint           = (known after apply)
      + secondary_table_host               = (known after apply)
      + secondary_table_microsoft_endpoint = (known after apply)
      + secondary_table_microsoft_host     = (known after apply)
      + secondary_web_endpoint             = (known after apply)
      + secondary_web_host                 = (known after apply)
      + secondary_web_internet_endpoint    = (known after apply)
      + secondary_web_internet_host        = (known after apply)
      + secondary_web_microsoft_endpoint   = (known after apply)
      + secondary_web_microsoft_host       = (known after apply)
      + sftp_enabled                       = false
      + shared_access_key_enabled          = true
      + table_encryption_key_type          = "Service"

      + blob_properties (known after apply)

      + network_rules (known after apply)

      + queue_properties (known after apply)

      + routing (known after apply)

      + share_properties (known after apply)

      + static_website (known after apply)
    }

  # module.storage_account.azurerm_storage_account.storage["storage02"] will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                        = "Hot"
      + account_kind                       = "StorageV2"
      + account_replication_type           = "LRS"
      + account_tier                       = "Standard"
      + allow_nested_items_to_be_public    = true
      + cross_tenant_replication_enabled   = true
      + default_to_oauth_authentication    = false
      + dns_endpoint_type                  = "Standard"
      + enable_https_traffic_only          = (known after apply)
      + https_traffic_only_enabled         = (known after apply)
      + id                                 = (known after apply)
      + infrastructure_encryption_enabled  = false
      + is_hns_enabled                     = false
      + large_file_share_enabled           = (known after apply)
      + local_user_enabled                 = true
      + location                           = "uksouth"
      + min_tls_version                    = "TLS1_2"
      + name                               = "storage02"
      + nfsv3_enabled                      = false
      + primary_access_key                 = (sensitive value)
      + primary_blob_connection_string     = (sensitive value)
      + primary_blob_endpoint              = (known after apply)
      + primary_blob_host                  = (known after apply)
      + primary_blob_internet_endpoint     = (known after apply)
      + primary_blob_internet_host         = (known after apply)
      + primary_blob_microsoft_endpoint    = (known after apply)
      + primary_blob_microsoft_host        = (known after apply)
      + primary_connection_string          = (sensitive value)
      + primary_dfs_endpoint               = (known after apply)
      + primary_dfs_host                   = (known after apply)
      + primary_dfs_internet_endpoint      = (known after apply)
      + primary_dfs_internet_host          = (known after apply)
      + primary_dfs_microsoft_endpoint     = (known after apply)
      + primary_dfs_microsoft_host         = (known after apply)
      + primary_file_endpoint              = (known after apply)
      + primary_file_host                  = (known after apply)
      + primary_file_internet_endpoint     = (known after apply)
      + primary_file_internet_host         = (known after apply)
      + primary_file_microsoft_endpoint    = (known after apply)
      + primary_file_microsoft_host        = (known after apply)
      + primary_location                   = (known after apply)
      + primary_queue_endpoint             = (known after apply)
      + primary_queue_host                 = (known after apply)
      + primary_queue_microsoft_endpoint   = (known after apply)
      + primary_queue_microsoft_host       = (known after apply)
      + primary_table_endpoint             = (known after apply)
      + primary_table_host                 = (known after apply)
      + primary_table_microsoft_endpoint   = (known after apply)
      + primary_table_microsoft_host       = (known after apply)
      + primary_web_endpoint               = (known after apply)
      + primary_web_host                   = (known after apply)
      + primary_web_internet_endpoint      = (known after apply)
      + primary_web_internet_host          = (known after apply)
      + primary_web_microsoft_endpoint     = (known after apply)
      + primary_web_microsoft_host         = (known after apply)
      + public_network_access_enabled      = true
      + queue_encryption_key_type          = "Service"
      + resource_group_name                = "rg-temp"
      + secondary_access_key               = (sensitive value)
      + secondary_blob_connection_string   = (sensitive value)
      + secondary_blob_endpoint            = (known after apply)
      + secondary_blob_host                = (known after apply)
      + secondary_blob_internet_endpoint   = (known after apply)
      + secondary_blob_internet_host       = (known after apply)
      + secondary_blob_microsoft_endpoint  = (known after apply)
      + secondary_blob_microsoft_host      = (known after apply)
      + secondary_connection_string        = (sensitive value)
      + secondary_dfs_endpoint             = (known after apply)
      + secondary_dfs_host                 = (known after apply)
      + secondary_dfs_internet_endpoint    = (known after apply)
      + secondary_dfs_internet_host        = (known after apply)
      + secondary_dfs_microsoft_endpoint   = (known after apply)
      + secondary_dfs_microsoft_host       = (known after apply)
      + secondary_file_endpoint            = (known after apply)
      + secondary_file_host                = (known after apply)
      + secondary_file_internet_endpoint   = (known after apply)
      + secondary_file_internet_host       = (known after apply)
      + secondary_file_microsoft_endpoint  = (known after apply)
      + secondary_file_microsoft_host      = (known after apply)
      + secondary_location                 = (known after apply)
      + secondary_queue_endpoint           = (known after apply)
      + secondary_queue_host               = (known after apply)
      + secondary_queue_microsoft_endpoint = (known after apply)
      + secondary_queue_microsoft_host     = (known after apply)
      + secondary_table_endpoint           = (known after apply)
      + secondary_table_host               = (known after apply)
      + secondary_table_microsoft_endpoint = (known after apply)
      + secondary_table_microsoft_host     = (known after apply)
      + secondary_web_endpoint             = (known after apply)
      + secondary_web_host                 = (known after apply)
      + secondary_web_internet_endpoint    = (known after apply)
      + secondary_web_internet_host        = (known after apply)
      + secondary_web_microsoft_endpoint   = (known after apply)
      + secondary_web_microsoft_host       = (known after apply)
      + sftp_enabled                       = false
      + shared_access_key_enabled          = true
      + table_encryption_key_type          = "Service"

      + blob_properties (known after apply)

      + network_rules (known after apply)

      + queue_properties (known after apply)

      + routing (known after apply)

      + share_properties (known after apply)

      + static_website (known after apply)
    }

  # module.storage_account.azurerm_storage_container.storage["storage01-container01"] will be created
  + resource "azurerm_storage_container" "storage" {
      + container_access_type             = "private"
      + default_encryption_scope          = (known after apply)
      + encryption_scope_override_enabled = true
      + has_immutability_policy           = (known after apply)
      + has_legal_hold                    = (known after apply)
      + id                                = (known after apply)
      + metadata                          = (known after apply)
      + name                              = "storagerandom0001"
      + resource_manager_id               = (known after apply)
      + storage_account_name              = "storage01"
    }

  # module.storage_account.azurerm_storage_container.storage["storage01-container02"] will be created
  + resource "azurerm_storage_container" "storage" {
      + container_access_type             = "private"
      + default_encryption_scope          = (known after apply)
      + encryption_scope_override_enabled = true
      + has_immutability_policy           = (known after apply)
      + has_legal_hold                    = (known after apply)
      + id                                = (known after apply)
      + metadata                          = (known after apply)
      + name                              = "storagerandom0001"
      + resource_manager_id               = (known after apply)
      + storage_account_name              = "storage01"
    }

  # module.storage_account.azurerm_storage_container.storage["storage02-container03"] will be created
  + resource "azurerm_storage_container" "storage" {
      + container_access_type             = "private"
      + default_encryption_scope          = (known after apply)
      + encryption_scope_override_enabled = true
      + has_immutability_policy           = (known after apply)
      + has_legal_hold                    = (known after apply)
      + id                                = (known after apply)
      + metadata                          = (known after apply)
      + name                              = "storagerandom0002"
      + resource_manager_id               = (known after apply)
      + storage_account_name              = "storage02"
    }

  # module.storage_account.azurerm_storage_container.storage["storage02-container04"] will be created
  + resource "azurerm_storage_container" "storage" {
      + container_access_type             = "private"
      + default_encryption_scope          = (known after apply)
      + encryption_scope_override_enabled = true
      + has_immutability_policy           = (known after apply)
      + has_legal_hold                    = (known after apply)
      + id                                = (known after apply)
      + metadata                          = (known after apply)
      + name                              = "storagerandom0002"
      + resource_manager_id               = (known after apply)
      + storage_account_name              = "storage02"
    }

Plan: 6 to add, 0 to change, 0 to destroy.

However, the apply fails with this:

Plan: 6 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.storage_account.azurerm_storage_account.storage["storage01"]: Creating...
module.storage_account.azurerm_storage_account.storage["storage02"]: Creating...
╷
│ Error: creating Storage Account (Subscription: "e286703f-8ba4-4a0d-bd44-8fb115bdebcd"
│ Resource Group Name: "rg-temp"
│ Storage Account Name: "storage02"): performing Create: unexpected status 409 (409 Conflict) with error: StorageAccountAlreadyTaken: The storage account named storage02 is already taken.
│
│   with module.storage_account.azurerm_storage_account.storage["storage02"],
│   on modules/azurerm_storage_account/main.tf line 1, in resource "azurerm_storage_account" "storage":
│    1: resource "azurerm_storage_account" "storage" {
│
╵
╷
│ Error: creating Storage Account (Subscription: "e286703f-8ba4-4a0d-bd44-8fb115bdebcd"
│ Resource Group Name: "rg-temp"
│ Storage Account Name: "storage01"): performing Create: unexpected status 409 (409 Conflict) with error: StorageAccountAlreadyTaken: The storage account named storage01 is already taken.
│
│   with module.storage_account.azurerm_storage_account.storage["storage01"],
│   on modules/azurerm_storage_account/main.tf line 1, in resource "azurerm_storage_account" "storage":
│    1: resource "azurerm_storage_account" "storage" {

I think my loop syntax might be off a bit? I think for each container it’s trying to create a new storage account, rather than associating the storage containers with the storage accounts being created?

Can anyone see where I’ve messed up at all?

Appreciate any insights.

Curious: Not sure if this directly solves your problem, would this get you what you need in that map of containers in a smaller / slightly simpler data structure?

You might also be able to do a list comprehension directly in the container for_each resource, though this may be more readable.

locals {
  containers_map = merge([
    for storage_account_index, details in var.storage_account: {
      for container in details.containers: "${storage_account_name}-${container.name}" => {
        "name" = container.name
        "storage_account_index" = storage_account_name
      }
    }
  ]...)
}

Also, I think you might be able to avoid the explicit depends_on if this works

resource "azurerm_storage_container" "storage" {
  for_each = local.containers_map

  name                 = each.value.name
  storage_account_name = azurerm_storage_account.storage[each.value.storage_account_index].name
}

Also, compare your data structure to your plan.
It seems like your intent is to make a storage account with the resource name storage02, but with the name storagerandom0002, and then a container mapped to that with the name container04. Instead, you have:

input:

    "storage02" = {
      name                          = "storagerandom0002"
      [...]
      containers = [
        {
          name = "container03"
          container_access_type = "private"
        },
        {
          name = "container04"
          container_access_type = "private"
        }
      ]

plan:

  # module.storage_account.azurerm_storage_account.storage["storage02"] will be created
  + resource "azurerm_storage_account" "storage" {
      + name                               = "storage02"
  [...]
  # module.storage_account.azurerm_storage_container.storage["storage02-container04"] will be created
  + resource "azurerm_storage_container" "storage" {
      + name                              = "storagerandom0002"
      + storage_account_name              = "storage02"

I don’t have an Azure account setup right now, so not easy to play around too much with planning this, but I think that is a good place to start looking.

edited above examples slightly.

I think you want to call it storage_account_index vs storage_account_name, and then reference azurerm_storage_account.storage[each.value.storage_account_index].name. Or something like that.

I was being a dummy. The code to create the storage accounts was using the key for the storage account names instead of the name value.

storage account block now looks like this:

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
}

And then I had to fiddle a bit with the loop. But this does seem to work now.

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
  tags                          = var.tags

}

locals {
  containers_list = merge([
    for storage_account_index, details in var.storage_account : {
      for container in details.containers :
      "${container.name}" =>
      merge(details, {
        storage_account_name = details.name
        container            = container
      })
    }
  ]...)
}

resource "azurerm_storage_container" "storage" {
  depends_on = [azurerm_storage_account.storage]
  for_each   = local.containers_list

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

Calling with this:

module "storage_account_inline_containers" {
  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 = [
        {
          name                  = "container01",
          container_access_type = "private"
        },
        {
          name                  = "container02",
          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 = [
        {
          name                  = "container03"
          container_access_type = "private"
        },
        {
          name                  = "container04"
          container_access_type = "private"
        },
        {
          name                  = "containeraa"
          container_access_type = "private"
        }
      ]
    }
  }
}

Thanks for all your help pointing me in the right direction.

1 Like