skip to Main Content

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


  1. Chosen as BEST ANSWER

    Setting my outputs on the root module to this works.

    output "routes" {
      value       = [for route in azurerm_route.routes : route.name]
      description = "Blocks containing configuration of each route."
    }
    

    I can then as @marko-e suggested reference this from my child module using

    output "routes" {
      value       = module.route_table.routes
      description = "Blocks containing configuration of each route."
    }
    

    The output now looks like this:

    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 = [
      "firewall",
      "internet",
      "route2",
    ]
    

    Thanks.


  2. You are creating the routes with for_each, which means the outputs will be key value pairs. There is a built-in values function which can be used in this case. For example:

    output "routes" {
      value       = values(azurerm_route_table.this)[*].route
      description = "Blocks containing configuration of each route."
    }
    

    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:

    output "routes" {
      value       = module.route_table.routes
      description = "Blocks containing configuration of each route."
    }
    

    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 the tolist([]) output.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search