I’ve been trying to improve my ability using loops in Terraform.
I have a module for creating a route table that takes an inline list of routes.
Child module looks like this:
main.tf
resource "azurerm_route_table" "this" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
bgp_route_propagation_enabled = false
}
resource "azurerm_route" "routes" {
for_each = { for route in var.routes : route.name => route }
name = each.value.name
resource_group_name = azurerm_route_table.this.resource_group_name
route_table_name = azurerm_route_table.this.name
address_prefix = each.value.address_prefix
next_hop_type = each.value.next_hop_type
next_hop_in_ip_address = each.value.next_hop_in_ip_address
}
Child variables look like this:
variables.tf
variable "name" {
type = string
description = "(Required) The name of the Route Table."
}
variable "resource_group_name" {
description = "(Required) Resource Group name of the Route Table to be created"
type = string
nullable = false
}
variable "location" {
description = "(Required) Location of the Route Table to be created"
type = string
nullable = false
}
variable "bgp_route_propagation_enabled" {
description = "(Optional) Boolean flag that controls propagation of routes learned by BGP on the Route Table."
type = bool
default = true
}
variable "routes" {
description = "List of objects that represent the configuration of each added Route."
type = list(object({
name = string
address_prefix = string
next_hop_type = string
next_hop_in_ip_address = optional(string)
}))
default = []
validation {
condition = can(regex("^[VirtualNetworkGateway]|[VnetLocal]|[Internet]|[VirtualAppliance]|[None]$", var.routes[0].next_hop_type))
error_message = "Next Hop Type error: Must be one of the following. "VirtualNetworkGateway", "VnetLocal", "Internet", "VirtualAppliance" "None". Defaults to None."
}
}
variable "tags" {
description = "(Required) Tags for the resource"
type = map(string)
}
I’ve also included outputs of the route table id, name and the route names as well (I think?).
Child outputs
outputs.tf
output "id" {
value = azurerm_route_table.this.id
description = "The Route Table configuration ID."
}
output "name" {
value = azurerm_route_table.this.name
description = "The name of the Route Table."
}
output "routes" {
value = azurerm_route_table.this.route
description = "Blocks containing configuration of each route."
}
I’m trying to get the calling parent module to also output these outputs to the console. But the route name is returning something odd?
parent module:
main.tf
module "route_table" {
source = "../"
name = "route-table-001" # NOTE: Route table name is free text, best to create something meaningful.
resource_group_name = "rg-rtb-uks-001"
location = "uksouth"
routes = [
{
name = "internet",
address_prefix = "0.0.0.0/0",
next_hop_type = "Internet",
},
{
name = "firewall",
address_prefix = "10.0.0.0/16",
next_hop_type = "VirtualAppliance",
next_hop_in_ip_address = "10.0.0.4"
},
{
name = "route2", # NOTE: Route name is free text, best to create something meaningful.
address_prefix = "10.10.0.0/16",
next_hop_type = "VirtualAppliance",
next_hop_in_ip_address = "10.10.0.4"
}
]
bgp_route_propagation_enabled = false
tags = {}
}
parent outputs.tf
outputs.tf
output "id" {
value = module.route_table.id
description = "The Route Table configuration ID."
}
output "name" {
value = module.route_table.name
description = "The name of the Route Table."
}
output "routes" {
value = [module.route_table[*].routes[*].name]
description = "Blocks containing configuration of each route."
}
When I run this, I it creates the route table with the routes in it fine, but the output for the route names looks like a Terraform command syntax – toList([])
?
module.route_table.azurerm_route_table.this: Creation complete after 3s [id=/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001]
module.route_table.azurerm_route.routes["route2"]: Creating...
module.route_table.azurerm_route.routes["firewall"]: Creating...
module.route_table.azurerm_route.routes["internet"]: Creating...
module.route_table.azurerm_route.routes["internet"]: Creation complete after 3s [id=/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/internet]
module.route_table.azurerm_route.routes["route2"]: Creation complete after 6s [id=/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/route2]
module.route_table.azurerm_route.routes["firewall"]: Creation complete after 9s [id=/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/firewall]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001"
name = "route-table-001"
routes = [
tolist([]),
]
When I run the destroy, the structure of the routes in the table look ok, each route clearly has a name.
Terraform will perform the following actions:
# module.route_table.azurerm_route.routes["firewall"] will be destroyed
- resource "azurerm_route" "routes" {
- address_prefix = "10.0.0.0/16" -> null
- id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/firewall" -> null
- name = "firewall" -> null
- next_hop_in_ip_address = "10.0.0.4" -> null
- next_hop_type = "VirtualAppliance" -> null
- resource_group_name = "rg-rtb-uks-001" -> null
- route_table_name = "route-table-001" -> null
}
# module.route_table.azurerm_route.routes["internet"] will be destroyed
- resource "azurerm_route" "routes" {
- address_prefix = "0.0.0.0/0" -> null
- id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/internet" -> null
- name = "internet" -> null
- next_hop_type = "Internet" -> null
- resource_group_name = "rg-rtb-uks-001" -> null
- route_table_name = "route-table-001" -> null
# (1 unchanged attribute hidden)
}
# module.route_table.azurerm_route.routes["route2"] will be destroyed
- resource "azurerm_route" "routes" {
- address_prefix = "10.10.0.0/16" -> null
- id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001/routes/route2" -> null
- name = "route2" -> null
- next_hop_in_ip_address = "10.10.0.4" -> null
- next_hop_type = "VirtualAppliance" -> null
- resource_group_name = "rg-rtb-uks-001" -> null
- route_table_name = "route-table-001" -> null
}
# module.route_table.azurerm_route_table.this will be destroyed
- resource "azurerm_route_table" "this" {
- bgp_route_propagation_enabled = true -> null
- disable_bgp_route_propagation = false -> null
- id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001" -> null
- location = "uksouth" -> null
- name = "route-table-001" -> null
- resource_group_name = "rg-rtb-uks-001" -> null
- route = [
- {
- address_prefix = "0.0.0.0/0"
- name = "internet"
- next_hop_type = "Internet"
# (1 unchanged attribute hidden)
},
- {
- address_prefix = "10.0.0.0/16"
- name = "firewall"
- next_hop_in_ip_address = "10.0.0.4"
- next_hop_type = "VirtualAppliance"
},
- {
- address_prefix = "10.10.0.0/16"
- name = "route2"
- next_hop_in_ip_address = "10.10.0.4"
- next_hop_type = "VirtualAppliance"
},
] -> null
- subnets = [] -> null
- tags = {
- "classification" = null
- "costcentre" = null
- "createdon" = null
- "data_classification" = null
- "deployedby" = null
- "environment" = null
- "owner_contact" = null
- "region" = null
} -> null
}
Plan: 0 to add, 0 to change, 4 to destroy.
Changes to Outputs:
- id = "/subscriptions/e286703f-8ba4-4a0d-bd44-8fb115bdebcd/resourceGroups/rg-rtb-uks-001/providers/Microsoft.Network/routeTables/route-table-001" -> null
- name = "route-table-001" -> null
- routes = [
- [
- "internet",
- "firewall",
- "route2",
],
] -> null
Could anyone clue me in to the nuance I’ve missed on this?
Is
routes = [
tolist([]),
]
trying to tell me to do something?
Thanks.
2
Answers
Setting my outputs on the root module to this works.
I can then as @marko-e suggested reference this from my child module using
The output now looks like this:
Thanks.
You are creating the routes with
for_each
, which means the outputs will be key value pairs. There is a built-invalues
function which can be used in this case. For example:should return the value of the
route
attribute for all the key values, which is denoted with the[*]
, which is called splat expression.In the root module (i.e, parent module) the output can then just be used without the splat syntax:
The way the output in the root module was defined originally signals to terraform you want all elements of a list, but you are not calling the module with the
count
meta-argument, so it seems terraform tries to cast it to a list, but there are no elements in that list, hence thetolist([])
output.