This post is specifically to dot down some terraform scripting blocks which is useful for specific use cases and might be a little tricky if you are starting with Terraform. Also acts as a reference for me to refer back 😉
So Terraform is pretty awesome in managing infra as code and the ability to create resources on conditions is something that can be easily achieved (using count or for-each) . I would be discussing a use-case and how to use terraform to create infra effectively for such cases.
- Let’s consider a scenario where we need to create resources based on some nested condition. For e.g.:
- Add routes for multiple subnets in multiple route tables to configure VPC peering or Transit Gateway in AWS.
- Add EFS mount targets for multiple subnets for a EFS filesystem in AWS and so on..
In the above cases the common thing is that we need to create resources based on nested conditions. If we take the first case, let’s say we have 3 subnets and 3 RT id’s defined as below:
locals {
my_subnets = ["10.1.16.0/21", "10.1.24.0/21", "10.1.32.0/21"]
rt_ids = ["rtb-01", "rtb-02", "rtb-03"]
}
We need to add an RT entry for each of the subnets in all the route tables specified. Now we can have multiple resource blocks to do this, however that would be difficult to maintain if there is a large number of route tables or subnets. What we can do here is have a nested loop defined and create resources in a single block using for-each.
First, let’s create a flattened list.
flatten ensures that this value is a flat list of objects, rather than a list of lists of objects. distinct is to remove duplicate entries if any.
tgw_routes = distinct(flatten([
for rt_ids in local.rt_ids : [
for subnets in local.my_subnets : {
rt_ids = rt_ids
subnets = subnets
}
]
]))
tgw_routes is a list, now we project it into a map where each key is unique. We’ll combine the rt_id and subnet keys to produce a single unique key per rt_id.
resource "aws_route" "my_routes" {
for_each = { for entry in local.tgw_routes : "${entry.rt_ids}.${entry.subnets}" => entry }
route_table_id = each.value.rt_ids
destination_cidr_block = each.value.subnets
transit_gateway_id = "tgw-01"
}
The above effectively creates the routes in every route table for all the subnets. If we see the terraform plan we see resources will be created with the key being rt_id.subnets i.e.
aws_route.my_routes["rtb-01.10.1.16.0/21"] aws_route.my_routes["rtb-02.10.1.16.0/21"] aws_route.my_routes["rtb-03.10.1.16.0/21"] aws_route.my_routes["rtb-01.10.1.24.0/21"] aws_route.my_routes["rtb-02.10.1.24.0/21"] aws_route.my_routes["rtb-03.10.1.24.0/21"] aws_route.my_routes["rtb-01.10.1.32.0/21"] aws_route.my_routes["rtb-02.10.1.32.0/21"] aws_route.my_routes["rtb-03.10.1.32.0/21"]
Thus we just used a single resource block to provision required resources rather than 9 blocks. If we want to add/delete, we just remove the entry from the locals rt_id or my_subnet list.
A similar approach can be taken if you encounter similar use-case. The advantage of using for-each is it handles creation/deletion of resources appropriately not affecting other existing resources as the resources are referred not by index (as in count) but by the key that we specified (rt_id.subnets)