On my previous post, we used count parameter to deploy multiple resources of the same kind. This method may cause some unintended results. Let's take a look at the route table as an example. You can check out my previous post here:

Terraform AWS VPC Example
Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. We can use Terraform to design, implement and manage the AWS infrastructure. There are many articles out there explain this in detail so I will dive straight into the example. If you want to learn T…

Previous method using count

When we add the routes to the route table using the count method it works okay at some level.

variable "public-subnets" {
  type = list
  default = ["20.20.20.0/24" , "30.30.30.0/24" , "40.40.40.0/24" , "50.50.50.0/24"]
}
vars.tf
resource "aws_route" "ner-subnets-public-rtable" {
  count                     = length(var.public-subnets)
  route_table_id            = aws_route_table.prod-public-rtable.id
  destination_cidr_block    = element(var.public-subnets , count.index)
  gateway_id                = aws_internet_gateway.prod-igw.id
}
vpc.tf

However, It gives us problems when we want to remove or change the order of the subnets, because,  Terraform creates the resources, each with their own state key, which is using the count index number:

Let's change the order and see what happens.

variable "public-subnets" {
  type = list
  default = ["20.20.20.0/24" , "30.30.30.0/24" , "50.50.50.0/24" , "40.40.40.0/24"]
}
re-order
Terraform will perform the following actions:

  # aws_route.ner-subnets-public-rtable[2] must be replaced
-/+ resource "aws_route" "ner-subnets-public-rtable" {
      ~ destination_cidr_block     = "40.40.40.0/24" -> "50.50.50.0/24" # forces replacement
      + destination_prefix_list_id = (known after apply)
      + egress_only_gateway_id     = (known after apply)
        gateway_id                 = "igw-04c17769351e6f1ee"
      ~ id                         = "r-rtb-08349d005a1d578162197524267" -> (known after apply)
      + instance_id                = (known after apply)
      + instance_owner_id          = (known after apply)
      + local_gateway_id           = (known after apply)
      + nat_gateway_id             = (known after apply)
      + network_interface_id       = (known after apply)
      ~ origin                     = "CreateRoute" -> (known after apply)
        route_table_id             = "rtb-08349d005a1d57816"
      ~ state                      = "active" -> (known after apply)
    }

  # aws_route.ner-subnets-public-rtable[3] must be replaced
-/+ resource "aws_route" "ner-subnets-public-rtable" {
      ~ destination_cidr_block     = "50.50.50.0/24" -> "40.40.40.0/24" # forces replacement
      + destination_prefix_list_id = (known after apply)
      + egress_only_gateway_id     = (known after apply)
        gateway_id                 = "igw-04c17769351e6f1ee"
      ~ id                         = "r-rtb-08349d005a1d57816383268388" -> (known after apply)
      + instance_id                = (known after apply)
      + instance_owner_id          = (known after apply)
      + local_gateway_id           = (known after apply)
      + nat_gateway_id             = (known after apply)
      + network_interface_id       = (known after apply)
      ~ origin                     = "CreateRoute" -> (known after apply)
        route_table_id             = "rtb-08349d005a1d57816"
      ~ state                      = "active" -> (known after apply)
    }

Plan: 2 to add, 0 to change, 2 to destroy.
figure-1

As you can see on figure-1, Terraform is trying to delete and recreate them with a new state key.

New approach using for_each loop

Let's do something a bit more interesting by using for_each to dynamically create multiple resources.

To demonstrate this I updated the previous example with the for_each function.

When we use for_each in a resource, it expects either a set or map so, we can't pass a list directly. We can however, pass a list value to toset to convert it to a set, which will remove any duplicate elements and discard the ordering of the elements for_each = toset(var.public-subnets)

Terraform will loop over the variable called public-subnets and set the value to destination_cidr_block

variable "public-subnets" {
  type = list
  default = ["20.20.20.0/24" , "30.30.30.0/24" , "40.40.40.0/24" , "50.50.50.0/24"]
}
resource "aws_route" "ner-subnets-public-rtable" {
  route_table_id            = aws_route_table.prod-public-rtable.id
  gateway_id                = aws_internet_gateway.prod-igw.id
  for_each = toset(var.public-subnets)
  destination_cidr_block = each.value
}

Let's re-order the subnets and see what happens.

variable "public-subnets" {
  type = list
  default = ["20.20.20.0/24" , "30.30.30.0/24" , "50.50.50.0/24" , "40.40.40.0/24"]
}
ubuntu@ubuntu:~/terraform_vpc$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

*** OUTPUT OMMITED ***

------------------------------------------------------------------------

No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.

As you can see above, Terraform is not going to make any changes to the resources.


Thanks for reading

As always, your feedback and comments are more than welcome

Reference

​​https://binx.io/blog/2020/06/17/creating-multiple-resources-at-once-with-terraform-for-each/

Manage Similar Resources with For Each | Terraform - HashiCorp Learn
Provision similar infrastructure components by iterating over a data structure with the for_each argument. Duplicate an entire VPC including a load balancer and multiple EC2 instances for each project defined in a map.