š„Let's Do DevOps: Terraform Iterative AWS Subnet Module
Hey all!
Terraform has the ability to call modules, which are snippets of terraform code that can be passed information to build resources. Generally these modules enshrine best practices, and help to keep your DevOps teams on-track in terms of resource nomenclature, structure, and security guidelines.
Modules also have the ability to contain multiple resources, and be passed a ācountā variable, which will lead to several similar resources being constructed.
In this blog Iāll share code for an AWS subnet and route-table association module that can accept a count variable, and build ānā number of subnets. New subnets are as easy as updating the count variable and updating the list of subnets passed to the module.
But enough talking about the cool thing, letās build it.
Modules Overview
Creating a module is as easy as saying āhey, terraform, call this module, hereās where it lives, like this:
module "module_name" { | |
source = "./path/to/module" | |
} |
Thatād work just fine, however thereās much more power in modules when we pass information to them. Imagine calling an ec2 module and passing the subnetID, AMI, size of subnet, etc to it. That makes the module a lot more powerful.
module "module_name" { | |
source = "./path/to/module" | |
variable1 = "value1" | |
variable2 = "value2 | |
} |
You can imagine how extensible this solution is. For example, hereās the finish module call weāll be building today:
module "kyler_test_subnets" { | |
source = "./modules/subnet" | |
vpc_id = aws_vpc.vpc.id | |
subnet_group = "KylerTest" | |
availability_zone = [ | |
"a", | |
"c" | |
] | |
subnet_addresses = [ | |
"10.1.10.0/24", | |
"10.1.20.0/24", | |
"10.1.30.0/24", | |
"10.1.40.0/24", | |
"10.1.50.0/24" | |
] | |
route_table_id = [ | |
aws_route_table.route_table1.id, | |
aws_route_table.route_table2.id | |
] | |
} |
Weāre calling a module that builds subnets, weāre telling it a āgroupā to use in naming of the subnets, availability zones, subnet addresses, route table, oh my! The module has to be written to accept and use all these values, which is the tricky part. So letās jump into that.
Iterative ModulesāāāThe SecretĀ Sauce
Writing modules isnāt terribly hardāāāyou tell it what values to accept from the caller, and assign those values to fields that terraform accepts, and boom, you have a functional module. However, that module can only build a single resource. Telling it to build several resources in a cogent way is some engineering, some creativity, and some luck. It starts with the ācountā parameter.
Count is a built-in terraform variable that terraform uses to know how many times to loop over the same resource and build it several times. This built-in loop within terraform is exposed to the resource via an āindexā attribute that tells the loop how many times the loop has run.
If thatās made your head spin, thatās okayāāāweāll walk through it with examples. First, letās take the subnet module a few lines at a time. Hereās the start of our subnet module, and weāre building a subnet resource. Weāre also using the count attribute we just talked about, but rather than assigning it by hand (which would work fine, as long as we remember to update it for each new subnet!), weāre going to pass the number of items in the subnet_addresses list instead using the length function. Basically, weāre saying to terraform, āIf there are 3 subnets in the subnet_addresses list, iterate 3 times and build 3 subnets.ā
You can also see that the VPC ID is just specified like you would in any normal non-iterative module. Thatās because that value will remains static across all the subnets we build.
resource "aws_subnet" "subnet" { | |
count = length(var.subnet_addresses) | |
vpc_id = var.vpc_id |
Letās add one more lineāāāthe variable that tells terraform the CIDR address of this subnet. Now, this item needs to change depending on which iteration of the loop weāre on. So we tell terraform to pick up the variable passed to this module called subnet_addresses using the element function, of index whatever number of the loop weāre on. So if we pass this module an array of ā1, 2, 3ā and the loop is on iteration 3, itāll pick out the 3rd item in the list, and use the value ā3ā.
resource "aws_subnet" "subnet" { | |
count = length(var.subnet_addresses) | |
vpc_id = var.vpc_id | |
cidr_block = element(var.subnet_addresses, count.index) |
You can use those index values to interpolate also. It makes a lot of sense to me to name the subnets starting at 1, rather than 0 where a computer starts counting, so we interpolate the loop index value, and add 1 (so 0 becomes 1, and 1 becomes 2).
resource "aws_subnet" "subnet" { | |
count = length(var.subnet_addresses) | |
vpc_id = var.vpc_id | |
cidr_block = element(var.subnet_addresses, count.index) | |
tags = { | |
Terraform = "true" | |
Name = "${var.subnet_group}Subnet${count.index + 1}" | |
} | |
} |
And boom, thatās our iterative module. However, we also need to associate the subnet to a route-table. Check out the second half of this module and see if you can pick out where iterative looping is done, where interpolation and value modification is done, and follow along.
resource "aws_route_table_association" "subnet_route_table_association" { | |
count = length(var.subnet_addresses) | |
subnet_id = aws_subnet.subnet[count.index].id | |
route_table_id = element(var.route_table_id, count.index) | |
} |
Notice also that when weāre calling the subnet module above, weāre only 2 availability zones and 2 route tables. How is the subnet module dealing with only having 2 values when it iterates 5 times? The answer is that lists inherently loop. So if a list contains value āA, Bā and the loop is called 5 times, loop 1 will be A, loop 2 will be B, loop 3 will be A, and so on.
OutputsāāāSplat!
Normally, a module can output a static number of resources, so outputs are easy to write. However, in an iterative module, any number of resources can be created. Outputs donāt support the ācountā parameter in the same way resources do, so we have to use another creation of Terraformāsāāāthe Splat expression.
Hereās what our output looks like in this subnet module:
output "subnet_ids" { | |
value = aws_subnet.subnet.*.id | |
} |
So within the subnet module, thatās how youād export ALL subnet IDs. But say you wanted to reference one of the subnets from the main.tf? Itād look like thisāāāreferencing the array value and module name from within the main.tf file:
output "KylerTestSubnet1_id" { | |
value = module.kyler_test_subnets.subnet_ids[0] | |
} | |
output "KylerTestSubnet2_id" { | |
value = module.kyler_test_subnets.subnet_ids[1] | |
} |
Go Build It Yourself!
You can find all the functional code at GitHub here: https://github.com/KyMidd/TerraformAwsSubnetModule
Go build some cool iterative modules! Imagine building a dozen ec2 instances, and standardizing their names, security settings, and managing them as a single flexible unit. The possibilities are endless.
Good luck out there.
kyler