Image: The Chef Show

Terraform + CSVs = Netflix

Caption: John, David and Roy. Such a great show.

Let's address the title first. Yes, I know that click-batey goodness of a title got you here. It's a bit uncool of me. However, I think it's warranted as there's method behind the sneaky madness.

If you want to enjoy more Netflix, CSVs in Terraform can save you a bunch of time (as well as lots of lines of code; for me: a total of 170,000 to be exact). Let me explain…

Let's talk Azure Route Tables. I appreciate there's not a universal use case for this scenario, but I certainly think there's more resources this can be applied to than just Route Tables. However, Route Tables are funny in that that you can look to manage these in various ways, but in some circumstances you have to just put together lots of Terraform and it's time consuming.

What's this got to do with Netflix? Well, a new season of The Chef Show is started on Netflix and I'd rather watch that than write an excessive amount of Terraform.

Rather than just give you the final dish, served nice and warm (obviously…), lets follow the heroes journey and enjoy the process as the journey is often, if not always, more enjoyable than the destination alone.

Level 1

This is a basic resource block in Terraform / a module or whatever name you want to give it. It's simple enough and does the job of creating a static route in a Route Table. What's important is that the Route Table and the Routes are tied together. So this solution is basic. Lets call it… Level 1. Ba dum, tish.

resource "azurerm_route_table" "AwesomeRouteTable" {
  name                          = "AwesomeRouteTable"
  resource_group_name           = azurerm_resource_group.my_rg.name
  location                      = azurerm_resource_group.my_rg.location
  disable_bgp_route_propagation = false

  route {
    name           = "MyFirstRoute"
    address_prefix = "10.10.10.0/24"
    next_hop_type  = "vnetlocal"
  }

}

Level 2

Let's level up and separate out those two resources and so we can manage those independently. Best practice by Terraform says its always best to separate out resources to manage them independently where possible. Here's what that looks like:

resource "azurerm_route_table" "AwesomeRouteTable" {
  name                = "AwesomeRouteTable_v2"
  resource_group_name = azurerm_resource_group.my_rg.name
  location            = azurerm_resource_group.my_rg.location
}

resource "azurerm_route" "MyFirstRoute" {
  name                = "MyFirstRoute"
  resource_group_name = azurerm_resource_group.my_rg.name
  route_table_name    = azurerm_route_table.AwesomeRouteTable.name
  address_prefix      = "10.10.10.0/24"
  next_hop_type       = "vnetlocal"
}

The main benefit of this approach is that you can create multiple routes quickly and easily. Well, reasonably quickly. Not quite quick enough though…

Level 3

This next level allows for some repeatability as the core module, can be stored somewhere and then referenced. If all your Route Tables are in the same Resource Group, then you could hard code that into the module so you don't need to repeat that line in all your instantiation code.

resource "azurerm_route" "test_routes" {
  for_each = var.route_object
  name                   = each.value.route_name
  resource_group_name    = each.value.rg_name
  route_table_name       = each.value.route_table_name
  address_prefix         = each.value.route_address_prefix
  next_hop_type          = each.value.route_next_hop_type
  next_hop_in_ip_address = each.value.route_next_hop_in_ip_address
}

variable  route_object {}

I would then save this module and call it from somewhere else, as outlined below:

module "azurerm_route_v3" {
  source = "modules/azurerm_route"
  route_object = {
    route_001 = {
      name                = "MyFirstRoute"
      resource_group_name = azurerm_resource_group.my_rg.name
      route_table_name    = azurerm_route_table.AwesomeRouteTable.name
      address_prefix      = "10.10.10.0/24"
      next_hop_type       = "vnetlocal"
    }
    route_002 = {
      name                = "MySecondRoute"
      resource_group_name = azurerm_resource_group.my_rg.name
      route_table_name    = azurerm_route_table.AwesomeRouteTable.name
      address_prefix      = "10.10.20.0/24"
      next_hop_type       = "vnetlocal"
    }
  }
}

Problem

With all of these options thus far, I'm having to create lots of Terraform. As much as I enjoy working with Terraform, I'd very much rather be watching Netflix. Now, there's some options around using ‘Locals’ to streamline some of this process, but I still found lots of code having to be written.

I originally wanted to leverage JSON files for this, but after thinking it through, CSVs were much faster to put together (❤️ Excel) so I went with that approach. The tricky part was making that work in Terraform. Here's my solution…

Enlightenment

Here's the final Terraform module that can create 1, 2, shforteen-teen, shfifty-five or as many routes that an Azure Route Table can create (that 400 for those playing at home). The Route Table itself just needs the same headings as are listen in the example module below and you're good to use Excel's power to speed up adding Routes.

locals {
  vnet_routes = csvdecode(file("${path.module}/vnet_routes.csv"))
}
resource "azurerm_route" "vnet_routes" {
  for_each = { for routes in local.vnet_routes  : routes.route_name => routes }
  name                   = each.value.route_name
  resource_group_name    = each.value.resource_group_name
  route_table_name       = each.value.route_table_name
  address_prefix         = each.value.address_prefix
  next_hop_type          = each.value.next_hop_type
  next_hop_in_ip_address = (each.value.route_next_hop_type == "VirtualAppliance") == true ? each.value.route_next_hop_in_ip_address : null
}

To explain it more, here's a line by like break down of what's happening:

# Here is a local value what sets the name 'vnet_routes' so we can call that multiple times later.
# The CSV file needs to be stored in the same location ${path.module} as the main.tf of whatever you call your file that contains the Route module.
locals {
  vnet_routes = csvdecode(file("${path.module}/vnet_routes.csv"))
}
# Here's the actual route module.
resource "azurerm_route" "vnet_routes" {
  # This is a for_each loop that references the CSV file from the local value.
  # I've also set it to assign a key of the route_name (from the CSV file) to each route, making a map (key/value pair) of the data.
  # This lets me change or destroy routes without having to re-create all of the routes, as you would with a list.
  for_each = { for routes in local.vnet_routes  : routes.route_name => routes }
  # These are the configuration arguments for a Azure Route Table Route.
  # These values are added from the CSV file.
  name                   = each.value.route_name
  resource_group_name    = each.value.resource_group_name
  route_table_name       = each.value.route_table_name
  address_prefix         = each.value.address_prefix
  next_hop_type          = each.value.next_hop_type
  # This last line took me a while to get right.
  # Since not every type of Route requires all arguments, this last line looks up whether or not a next_hop_type of "VirtualAppliance" is set.
  # If that is not set, it will set this to "null", meaning it won't pass though.
  # Therefore this module isn't limited to just a specific set of Route types.
  next_hop_in_ip_address = (each.value.route_next_hop_type == "VirtualAppliance") == true ? each.value.route_next_hop_in_ip_address : null
}

I hope you've found the heroes journey as enjoyable as I have. With that, you can now also spend less time Terraforming, more time Netflix and Chill…🤔… Cheers


Discussion 💬