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”.