farroar

Efficient variables in Terraform

I’m always looking for a way to reduce the amount of variables I need to get something done. Good IaC will get some ‘thing’ done, great IaC will feel more intent based and be re-useable. 

The goal is to build some basic infrastructure. In this case that would be a VNet

and subnets to go along with it. There’s a lot of things to consider when you’re building infrastructure to support connectivity.

Common Build Items

  • Network Security Groups
  • Route Tables
  • VNet Peering
  • Subnet Delegation
  • Private Endpoints & Private Link Services
  • Reserved subnet types (GatewaySubnet, AzureBastionSubnet, RouteServerSubnet)
  • Virtual Network Gateways/ ExpressRoute circuits

This might seem to be a bit of an ambitious list for what I’m calling basic infrastructure, but these are all things that need to be considered if you want to build flexible code that can be reused. You may never need to worry about ExpressRoute circuits or Virtual Network Gateways but I’m sure you’ll need to consider route tables and NSGs. Or at least be able to consider them later without having to dig back into your modules.

When I’m putting stuff together, I try to remind myself of all the times I’ve needed to add something after the fact. Be as full featured with your code as you can.

There’s nothing wrong with just building exactly what you need and only that, but IMO you are coding yourself into a corner.

Here’s some context on what I’m blabbing on about:

Terraform Traditional Method

Focusing here on using a variables file (terraform.tfvars) to define what you want.

primary_firewall_route = "10.107.0.4"
location = "eastus"
org_name = "Contoso"

hub_subscription_name = "hub"
hub_vnet_cidr = ["10.107.0.0/16"]
hub_inside_subent_prefix = "10.107.0.0/24"
hub_inside_subnet_name = "inside-subnet"
hub_outside_subnet_name = "outside-subnet"
hub_outside_subnet_prefix ="10.107.1.0/24"

workload_vnet_cidr = ["10.108.0.0/16"]
workload_server_subnet_prefix = "10.108.0.0/24"
Workload_server_subnet_name = "servers-subnet"
Workload_services_subnet_prefix = "10.108.1.0/24"
Workload_services_subnet_name = "services-subnet"
deploy_bastion = false

This is pretty straightforward, right? Got some Vnets and subnets that go along with them. A few other variables that help label things.

From just looking at these variables, you know that the resulting code isn’t flexible. The variable names don’t really allow for variability , and if I want to add more things like Network Security Groups, and rules corresponding then this will just get more and more messy. Here’s a step in the right direction:

Improved Declarations

primary_firewall_route = "10.107.0.4"
location = "eastus"
org_name = "Contoso"

hub_subscription_name = "transitHub"
hub_vnet_cidr =  ["10.107.0.0/16"]
hub_subnets = [
  {
    name = "inside-subnet"
    prefix = "10.107.0.0/24"
  },
  {
    name = "outside-subnet"
    prefix = "10.107.1.0/24"
  }
]

workload_subscription_name = "workloads"
workload_vnet_cidr         = ["10.108.0.0/16"]
workload_subnets = [
  {
    name = "servers-subnet"
    prefix = "10.108.0.0/24"
  },
  {
    name = "services-subnet"
    prefix = "10.108.1.0/24"
  }
]
deploy_bastion = false

Better, and we can now add additional subnets without having to change code. But still, this is really limiting and again, the code is going to be customized to the variables.

This is how I approach it these days. I use JSON to define my variables since I can use code to build the variable files easily (another post on that later) and it just flows better IMO.

In this example, I’m defining a whole lot more infrastructure by nesting. I just need to make sure that my code knows how to spider and loop through all of this.

{
  "environment_vars": {
    "org_name": "Contoso",
    "location": "eastus",
    "subscription_name": "hub",
    "environment": "hub"
  },
  "vnets": [
   {
    "vnet_name": "hub-vnet",
    "vnet_cidr": "10.248.0.0/24",
    "deploy_bastion": true,
    "deploy_route_server": false,
    "deploy_gateway": true,
    "gateway" : {
      "type": "vpn",
      "sku": "VpnGw1AZ",
      "active_active": false
    }
  ]
  },
  "subnets": [
    {
      "name": "inside",
      "vnet": "hub_vnet",
      "prefix": "10.248.0.0/28",
      "route_table": true,
      "nsg": true,
      "security_group_rules": [
        {
          "name": "AllowAllInbound",
          "properties": {
            "priority": 100,
            "access": "Allow",
            "direction": "Inbound",
            "protocol": "*",
            "sourcePortRange": "*",
            "destinationPortRange": "*",
            "sourceAddressPrefix": "*",
            "destinationAddressPrefix": "*"
          }
        },
        {
          "name": "AllowAllOutbound",
          "properties": {
            "priority": 100,
            "access": "Allow",
            "direction": "Outbound",
            "protocol": "*",
            "sourcePortRange": "*",
            "destinationPortRange": "*",
            "sourceAddressPrefix": "*",
            "destinationAddressPrefix": "*"
          }
        }
      ]
    },
    {
      "name": "outside",
      "vnet": "hub_vnet",
      "prefix": "10.248.0.16/28",
      "route_table": true,
      "nsg": true,
      "security_group_rules": [
        {
          "name": "AllowAllInbound",
          "properties": {
            "priority": 100,
            "access": "Allow",
            "direction": "Inbound",
            "protocol": "*",
            "sourcePortRange": "*",
            "destinationPortRange": "*",
            "sourceAddressPrefix": "*",
            "destinationAddressPrefix": "*"
          }
        },
        {
          "name": "AllowAllOutbound",
          "properties": {
            "priority": 100,
            "access": "Allow",
            "direction": "Outbound",
            "protocol": "*",
            "sourcePortRange": "*",
            "destinationPortRange": "*",
            "sourceAddressPrefix": "*",
            "destinationAddressPrefix": "*"
          }
        }
      ]
    }
  ]
}

Unpacking that

That might be a lot to unpack at first glance. What we have is three major elements:

  • Environment_Vars
  • Vnet
  • Subnets   

And from each element, I can expand and add lists and objects as needed over time. This method grows with me. If a new feature is added in my Terraform modules, it is easy to just tack it on here without re-organizing anything. And, I can hand this off to the security team for them to make their adjustments.

This layout adds a lot of flexibility. I can spin up as many vnets and subnets as I want, I can create cooresponding security groups and list out all of the rule sets. I can use some boolean options for deploying various things.

Of course, this is just the variables. What does the code look like? Well, to deploy the VNet’s and Subnets:


locals {
  subnet_map = { for s in var.subnets: s.name => s }
  subnet_set = toset([for s in var.subnets: s.name])
}

resource "azurerm_virtual_network" "hub_vnet" {
  name                = "${var.environment_vars.org_name}-${var.environment_vars.location}-${var.environment_vars.environment}-network-rg"
  location            = azurerm_resource_group.hub_network_rg.location
  resource_group_name = azurerm_resource_group.hub_network_rg.name
  address_space       = [var.vnet.vnet_cidr]
}

resource "azurerm_subnet" "subnet" {
  for_each             = local.subnet_set
  name                 = (each.value == "AzureBastionSubnet" || each.value == "RouteServerSubnet" || each.value =="GatewaySubnet" ? each.value : "${each.value}-subnet")
  resurce_group_name  = azurerm_resource_group.hub_network_rg.name
  virtual_network_name = azurerm_virtual_network.hub_vnet.name
  address_prefixes     = [local.subnet_map[each.value].prefix]
}

There’s more to dig into here with how the logic works, but that will be for a later post. But, the end lesson here is that being efficient with how you layout what feeds your code will give you more flexibility with how you use your code.