Terraform can create multiple resources using the for_each argument. However, depending on your variable type or for_each syntax, Terraform can inadvertently destroy resources due to index issues. In this article, you will see examples of Terraform variables and for_each syntax that can cause indexing issues and how to resolve them.

Check out more Terraform content here!

Using Terraform For_Each

This example creates multiple Azure Storage Accounts using a list of objects. Here is the storage_accounts variable definition as a list of objects. Each object has three properties: name, replication_type, and account_tier.

variable "storage_accounts" {
  type = list(object({
    name             = string
    replication_type = string
    account_tier     = string
  }))
}

Here is an example storage_accounts value with two elements creating storage accounts with different properties.

storage_accounts = [
  {
    name             = "jeffbrowntech001"
    replication_type = "LRS"
    account_tier     = "Standard"
  },
  {
    name             = "jeffbrowntech002"
    replication_type = "GRS"
    account_tier     = "Standard"
  }
]

You might initially attempt to use the for_each argument by assigning var.storage_accounts as the value like this:

resource "azurerm_storage_account" "sa" {
  for_each = var.storage_accounts
  name                     = each.value.name
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_replication_type = each.value.replication_type
  account_tier             = each.value.account_tier
}

However, if you attempt to create a plan or apply the configuration, you receive a message that the for_each argument needs to be a map or set of strings, not a list of objects.

The given “for_each” argument value is unsuitable: the “for_each” argument must be a map, or set of strings, and you have provided a value of type list of object.

Converting the list to a map

One solution to solve the above issue is to convert the list to a map using a for expression. The for expression transforms one complex type value to another complex type value. An example you might come across uses the for expression in the for_each argument to transform the complex type.

In this example, the for expression declares a pair of temporary symbols (key and value) to use the key or index of each item. The for expression is wrapped in curly brackets { } indicating the result is an object. Since the result is an object, you must use two result expressions separated by the => symbol.

resource "azurerm_storage_account" "sa" {
  for_each = { for key, value in var.storage_accounts : key => value }
  name                     = each.value.name
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_replication_type = each.value.replication_type
  account_tier             = each.value.account_tier
}

As Terraform visits each element in the list, it sets the index or key to the element order in the list, starting at 0. Terraform also converts this index number to a string, as maps use strings as keys. In this example, Terraform assigns the first list object jeffbrowntech001 to index “0”.

After deploying the configuration, view the state file contents using the terraform state list command. You will see the resource group and two instances of the azurerm_storage_account.sa resource with indexes of [“0”] and [“1”].

terraform state showing numbers as index values
Terraform state with storage accounts using indexes “0” and “1”.

The problem: making changes to the list

The above deployment will work fine initially. However, if you make changes to the storage_accounts variable that modifies the order of the objects, Terraform thinks you are changing that instance and performs a destroy and add action to fix the order.

Let’s go through an example. Let’s say you add another storage account between the two existing objects in the list. This example shows adding a storage account named jeffbrowntech999 in the middle of the current list.

storage_accounts = [
  {
    name             = "jeffbrowntech001"
    replication_type = "LRS"
    account_tier     = "Standard"
  },
  {
    name             = "jeffbrowntech999"
    replication_type = "LRS"
    account_tier     = "Premium"
  },
  {
    name             = "jeffbrowntech002"
    replication_type = "GRS"
    account_tier     = "Standard"
  }
]

Running terraform plan reveals that Terraform now sees the storage account jeffbrowntech999 at index [“1”] instead of jeffbrowntech002. Terraform does a replace action (delete and add) to update the storage account in this index correctly. This update is also apparent in the replication_type and account_tier properties, as jeffbrowntech999 has different values (“LRS” and “Premium”) than jeffbrowntech002 (“GRS” and “Standard”).

terraform plan showing destroy and create actions due to index changes
Terraform destroying existing due to index value changes

Terraform now sees jeffbrowntech002 at index [“2”] and creates the storage account as usual. However, this behavior is undesirable as Terraform destroys an existing resource to keep the correct index order defined in the storage_accounts variable. Terraform think you are attempting to change existing resource properties rather than adding a new resource to a list of existing resources.

terraform for_each index changes
Terraform re-creating existing resource due to index value changes

Solutions to Terraform for_each Index Issues

This article covers two ways to avoid indexing issues if you run into this scenario. The first keeps the variable as a list of objects; the second changes this to a map of objects.

List of objects solution

In this example, if you keep the storage_accounts variable as a list of objects, you can change the syntax of your for_each argument to avoid indexing issues. The syntax below uses the name property of each item in the list as the index when Terraform creates the resource.

for_each = { for storage in var.storage_accounts : storage.name => storage }

Terraform creates each resource using the storage account name as the index value. You can see this result by viewing the state file using terraform state list. Note that the storage account resources use the storage account name as the index value when Terraform previously used [“0”], [“1”], etc.

terraform state showing resources with names as index values
Terraform state storing resources with names for index values

Adding another storage account in the middle of the list does not force Terraform to delete existing resources. Instead, Terraform creates the resource as normal without affecting existing resources. Here is the terraform plan output after adding a storage account in the middle of the list only resulting in create actions, no deletes.

terraform plan output with only create actions
Terraform plan only showing create actions

Map of objects solution

A second solution is to define the resources as a map of objects from the start. In a map of objects, you assign the index value as part of the variable value. Here’s an example of defining the storage_accounts variable as a map of objects and setting the value. Note the storage account name is used as the index value.

variable "storage_accounts" {
  type = map(object({
    name             = string
    replication_type = string
    account_tier     = string
  }))
}
storage_accounts = {
  "jeffbrowntech001" = {
    name             = "jeffbrowntech001"
    replication_type = "LRS"
    account_tier     = "Standard"
  },
  "jeffbrowntech002" = {
    name             = "jeffbrowntech002"
    replication_type = "GRS"
    account_tier     = "Standard"
  }
}

Inside the azurerm_storage_account resource definition, use just the variable name in the for_each argument. There is no need to create a for statement to iterate through the map of objects.

resource "azurerm_storage_account" "sa" {
  for_each = var.storage_accounts
 
  name                     = each.value.name
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_replication_type = each.value.replication_type
  account_tier             = each.value.account_tier
}

This method prevents Terraform from deleting and creating new resources when objects are added or removed from the variable value. Terraform stores the resources in the state file using the index value defined in the variable and created by the for_each argument.

Terraform state with names in index values

Summary

Modifying a list or map of objects in a for_each shouldn’t cause Terraform to destroy and create resources due to index changes. If you run into a situation like this, it may require refactoring your code to avoid unnecessary resource modifications. Hopefully, the solutions above will help in that endeavor.

If you want to see full code examples from above, discover more in this GitHub repository:
JeffBrownTech | terraform_learning | for_each-index-issues

Have you run into this situation before? How did you resolve it? Leave a comment below!