Help with Index & List Logic

Terraform Version

Terraform v0.12.6
+ provider.azurerm v1.32.1

Terraform Configuration Files

Create a folder for the modules and then a folder called resource_group, place a main.tf with the following:

variable "names" {
  description = "List of resource groups to create"
  type = "list"
}

variable "azure_location" {
  description = "Location in which to deploy resources"
}

resource "azurerm_resource_group" "rg" {
  count    = "${length(var.names)}"
  name     = "${var.names[count.index]}"
  location = "${var.azure_location}"
}

At the root, place a main.tf with the following:

module "rgs" {
  source              = "./modules/resource_group"
  names                = ["rg-0","rg-1","rg-2"]
  azure_location      = "eastus"
}

Steps to Reproduce

Please list the full steps required to reproduce the issue, for example:

  1. terraform init
  2. terraform apply
  3. You will see that three resource groups were applied.
  4. Remove rg-2 from the list and run a plan.
  5. Terraform wants to replace rg-1 with rg-2 and destroy rg-2.
  # module.rgs.azurerm_resource_group.rg[1] must be replaced
-/+ resource "azurerm_resource_group" "rg" {
      ~ id       = "/subscriptions/7d559a72-xxxx-xxxx-xxxx-5ca5b14a25e7/resourceGroups/rg-1" -> (known after apply)
        location = "eastus"
      ~ name     = "rg-1" -> "rg-2" # forces replacement
      ~ tags     = {} -> (known after apply)
    }

  # module.rgs.azurerm_resource_group.rg[2] will be destroyed
  - resource "azurerm_resource_group" "rg" {
      - id       = "/subscriptions/7d559a72-xxxx-xxxx-xxxx-5ca5b14a25e7/resourceGroups/rg-2" -> null
      - location = "eastus" -> null
      - name     = "rg-2" -> null
      - tags     = {} -> null
    }

Makes complete sense that this is happening, however how can we have terraform not do this without having main.tf have three instances of the RG module.

Hi @rohrerb,

The version of Terraform you are using introduced a new feature for_each which you can use instead of count to get the result you’re looking for:

resource "azurerm_resource_group" "rg" {
  for_each = toset(var.names)

  name     = each.value
  location = var.azure_location
}

The for_each argument here converts your list of strings into a set of strings (you could also get this result without the function call by changing the variable type to set(string), in which case Terraform will convert automatically) and then uses each unique string as an instance key. each.value inside the block evaluates to the string that corresponds to that instance.

When using this model, you’ll see Terraform instead track these instances using the string keys, and thus you can add and remove items without affecting unrelated instances:

  • module.rgs.azurerm_resource_group.rg["rg-0"]
  • module.rgs.azurerm_resource_group.rg["rg-1"]
  • module.rgs.azurerm_resource_group.rg["rg-2"]

Amazing! Thank you so much. :smiley:

@apparentlymart - What is the approach to to reference a resource which is using for_each?

I have tried each.key and each.value with no luck.

resource "random_password" "sql_server_password" {
  for_each = toset(var.db_names)

  length = 32
  special = true
}


resource "azurerm_sql_server" "sqlserver" {
  for_each = toset(var.db_names)

  name                         = "${format("%s%s%s-sqlserver-%s", lower(var.environment_code), lower(var.deployment_code), lower(var.location_code), lower(each.value))}"
  resource_group_name          = "${var.resource_group_name}"
  location                     = "${var.azure_location}"
  version                      = "${var.sql_version}"
  administrator_login          = "${format("%s_%s", each.value, "admin")}"
  administrator_login_password = "${random_password.sql_server_password.*.result[each.value]}"

  provisioner "local-exec" {
    command = "${format("echo fully qualified sql server domain name %s", self.fully_qualified_domain_name)}"
  }
}

Results:

Error: Unsupported attribute

  on ../modules/sql_server_database/main.tf line 18, in resource "azurerm_sql_server" "sqlserver":
  18:   administrator_login_password = "${random_password.sql_server_password.*.result[each.value]}"

This object does not have an attribute named "result".

or

Error: Invalid index

  on ../modules/sql_server_database/main.tf line 18, in resource "azurerm_sql_server" "sqlserver":
  18:   administrator_login_password = "${random_password.sql_server_password.*.result[each.key]}"
    |----------------
    | each.key is "db2"
    | random_password.sql_server_password is object with 2 attributes

The given key does not identify an element in this collection value: a number
is required.```

Figured it out.

 administrator_login_password = random_password.sql_server_password[each.key].result

I’m glad you figured it out, @rohrerb! As you’ve seen, a resource with for_each behaves like a map rather than like a list, so the splat operators are no longer applicable.

(The new Terraform 0.12 way to write the count.index expression is more like thefor_each way: random_password.sql_server_password[count.index].result; in Terraform 0.12 and later I’d suggest to use the splat operators only when you want to use the entire resulting list, and prefer the simple index syntax for accessing single elements.)

For this sort of chaining of resources a different way to do it is to use the first resource as the for_each for the second, like this:

resource "random_password" "sql_server_password" {
  for_each = toset(var.db_names)

  length  = 32
  special = true
}

resource "azurerm_sql_server" "sqlserver" {
  for_each = random_password.sql_server_password

  # ...

  # each.value is the corresponding random_password object
  administrator_login_password = each.value.result
}

In a lot of cases I find that this pattern reads better, because you can see more clearly how each resource relates to the others. In this particular situation though it does seem rather weird to express this like “Create one database per database password”; the way you wrote probably makes the intent clearer to a future reader: “for each database name there is both a database and a database password”.

Hi @apparentlymart,

Appreciate all the help thus far.

I am running into a wierd issue im hoping you can help me figure out. I am sure its something dumb on my end.

The reason im converting to a map is due to empty list > https://github.com/hashicorp/terraform/issues/22281

variable "function_names" {
 default     = ["tf1"]
 type        = list
}
resource "azurerm_function_app" "app" {
  for_each = {for v in var.function_names : v => v }

  name                      = format("%s%s%s%s%s",upper(var.environment_code), upper(var.deployment_code), upper(var.location_code), "-", each.key)
  location                  = azurerm_resource_group.function.location
  resource_group_name       = azurerm_resource_group.function.name
  app_service_plan_id       = azurerm_app_service_plan.service_plan.id
  storage_connection_string = module.storage.primary_connection_string
  version                   = "~2"

  app_settings = {
    WEBSITE_RUN_FROM_PACKAGE = var.appsetting_website_run_from_package
  }
}

resource "azurerm_role_assignment" "ADO_service_principal_to_function_app" {
  for_each = azurerm_function_app.app

  scope                = each.value.id
  role_definition_name = "Contributor"
  principal_id         = var.principal_id
}
Error: Unsupported attribute

  on ../modules/function/main.tf line 48, in resource "azurerm_role_assignment" "ADO_service_principal_to_function_app":
  48:   scope                = each.value.id
    |----------------
    | each.value is false

This value does not have any attributes.

If i then use splat i get a different error:

resource "azurerm_function_app" "app" {
  for_each = {for v in var.function_names : v => v }

  name                      = format("%s%s%s%s%s",upper(var.environment_code), upper(var.deployment_code), upper(var.location_code), "-", each.key)
  location                  = azurerm_resource_group.function.location
  resource_group_name       = azurerm_resource_group.function.name
  app_service_plan_id       = azurerm_app_service_plan.service_plan.id
  storage_connection_string = module.storage.primary_connection_string
  version                   = "~2"

  app_settings = {
    WEBSITE_RUN_FROM_PACKAGE = var.appsetting_website_run_from_package
  }
}

resource "azurerm_role_assignment" "ADO_service_principal_to_function_app" {
  for_each = {for v in var.function_names : v => v }

  scope                = azurerm_function_app.app[each.key].id
  role_definition_name = "Contributor"
  principal_id         = var.principal_id
}
Error: Invalid index

  on ../modules/function/main.tf line 48, in resource "azurerm_role_assignment" "ADO_service_principal_to_function_app":
  48:   scope                = azurerm_function_app.app[each.key].id
    |----------------
    | azurerm_function_app.app is object with 22 attributes
    | each.key is "tf1"

The given key does not identify an element in this collection value.

Any ideas?

Hi @rohrerb!

Unfortunately I think you’re hitting the problem described in issue #22407, where referring to azurerm_function_app.app is returning a single azurerm_function_app object rather than the whole map as expected. This leads to some odd error messages like the one you saw here because your for_each is then iterating over the attributes of that object rather than the map keys.

The fix for that is merged already but hasn’t yet been included in a release because the 0.12.7 release was blocked by some other issues. However, those other blockers are clearing up right now so that new release should be available soon.

One of the known causes of that issue is having some leftover objects in the state from before for_each was enabled for a particular resource. If you run terraform show, you should be able to see in there if there are any existing instances of azurerm_function_app.app already created.

If there are and you can share their addresses with me (including any index portion like [0] or ["foo"] on the end) then I might be able to suggest some terraform state mv commands that might unblock you here, if you’re hitting the particular cause of the bug I’m thinking of.

Thank you @apparentlymart