Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Load Balancer Terraform Module

A composite module that provisions an Azure Load Balancer (azurerm_lb) together with its backend pools, health probes, load balancing rules, inbound NAT rules and pools, outbound rules, and explicit backend addresses β€” hardened by default and targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources

🧩 Overview

This module manages a regional or cross-region Azure Load Balancer and every child record that shares its lifecycle:

  • 🌐 Frontends β€” one or more public or internal frontend IP configurations, zone-redundant by default on the Standard SKU.
  • 🎯 Backend pools β€” keyed address pools, optionally VNet-scoped or configured with Gateway tunnel interfaces.
  • ❀️ Health probes β€” TCP, HTTP, or HTTPS probes referenced by rules.
  • βš–οΈ Load balancing rules β€” including HA ports, floating IP, and explicit outbound-SNAT control.
  • β†ͺ️ Inbound NAT rules and pools β€” single-port forwards and port-range pools for scale sets.
  • ⬆️ Outbound rules β€” explicit, port-controlled egress preferred over implicit SNAT.
  • πŸ“Œ Backend addresses β€” register targets by private IP or by another load balancer's frontend for cross-region topologies.

πŸ’‘ Why it matters: A load balancer is where public reachability, health, and egress design meet. Choosing the Standard SKU (closed to inbound until a network security group allows it), preferring explicit outbound rules over implicit SNAT, and keeping frontends zone-redundant turns a set of easy-to-misconfigure primitives into one reviewable, secure-by-default unit.

❀️ Support this project

If this module saves you time, please consider supporting it:

πŸ—ΊοΈ Where this fits in the family

flowchart LR
  rg["terraform-azurerm-resource-group"]
  pip["terraform-azurerm-public-ip"]
  snet["terraform-azurerm-subnet"]
  lb["terraform-azurerm-load-balancer"]
  vmss["terraform-azurerm-virtual-machine-scale-set"]
  diag["terraform-azurerm-monitor-diagnostic-setting"]

  rg -->|"resource_group_name"| lb
  pip -->|"public frontend public_ip_address_id"| lb
  snet -->|"internal frontend subnet_id"| lb
  lb -->|"backend_pool_ids"| vmss
  lb -->|"id for diagnostics"| diag

  classDef mod fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef ext fill:#eef2f7,stroke:#b8c2cc,color:#1b1b1b;
  class lb mod;
  class rg,pip,snet,vmss,diag ext;
Loading

The load balancer consumes a resource group, a public IP (public frontend) or a subnet (internal frontend), and fronts a backend compute tier. Diagnostics and RBAC are layered on by sibling modules against its id.

🧬 What this module builds

flowchart TB
  subgraph module["terraform-azurerm-load-balancer"]
    lb["azurerm_lb (this)"]
    pool["azurerm_lb_backend_address_pool"]
    probe["azurerm_lb_probe"]
    rule["azurerm_lb_rule"]
    nat["azurerm_lb_nat_rule"]
    natpool["azurerm_lb_nat_pool"]
    outbound["azurerm_lb_outbound_rule"]
    addr["azurerm_lb_backend_address_pool_address"]
  end

  lb -->|"loadbalancer_id"| pool
  lb -->|"loadbalancer_id"| probe
  lb -->|"loadbalancer_id"| rule
  lb -->|"loadbalancer_id"| nat
  lb -->|"loadbalancer_id"| natpool
  lb -->|"loadbalancer_id"| outbound
  pool -->|"backend_address_pool_id"| addr
  pool -->|"backend_address_pool_ids"| rule
  probe -->|"probe_id"| rule
  pool -->|"backend_address_pool_id"| outbound

  classDef keystone fill:#004578,stroke:#002a48,color:#ffffff;
  classDef child fill:#eef2f7,stroke:#b8c2cc,color:#1b1b1b;
  class lb keystone;
  class pool,probe,rule,nat,natpool,outbound,addr child;
Loading

Resource inventory (8 resources):

Resource Cardinality Role
azurerm_lb single (this) The keystone load balancer and its frontend IP configurations.
azurerm_lb_backend_address_pool for_each Backend address pools.
azurerm_lb_probe for_each Health probes.
azurerm_lb_rule for_each Load balancing rules.
azurerm_lb_nat_rule for_each Inbound NAT rules.
azurerm_lb_nat_pool for_each Inbound NAT pools (scale sets).
azurerm_lb_outbound_rule for_each Explicit outbound (egress) rules.
azurerm_lb_backend_address_pool_address for_each Explicit backend targets by IP or frontend.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider hashicorp/azurerm pinned ~> 4.0
Provider block None in this module β€” the caller configures provider "azurerm" { features {} }, auth, and subscription.

Schema notes that bite (verified against the live provider schema):

  • name, resource_group_name, location, sku, sku_tier, and edge_zone are immutable β€” changing any of them replaces the load balancer and cascades to every child that references its id.
  • The Standard SKU is closed to inbound by default: traffic reaches a backend only after a network security group on the subnet/NIC allows it. This is why the module defaults sku = "Standard".
  • The Basic SKU is retiring (announced end of support) and is open by default; it does not support availability zones, outbound rules, or HA ports. Choose it only for legacy compatibility.
  • SKU must match the public IP: a Standard load balancer requires a Standard public IP; mixing SKUs is rejected at apply.
  • The provider's enable_floating_ip and enable_tcp_reset arguments are deprecated on v4.x; this module uses the current floating_ip_enabled / tcp_reset_enabled names.
  • azurerm_lb_nat_rule and azurerm_lb_nat_pool each require a resource_group_name; the module supplies the load balancer's automatically.
  • HA ports are frontend_port = 0 / backend_port = 0 with protocol = "All" on a Standard internal load balancer.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Network Contributor on the target resource group (create/update the load balancer and all child records), or a custom role granting Microsoft.Network/loadBalancers/* scoped to the resource group.
  • Read on any referenced public IP / subnet (Microsoft.Network/publicIPAddresses/read, Microsoft.Network/virtualNetworks/subnets/read) at their scope so the frontends can bind them β€” least privilege, at the smallest scope that works.

Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.Network resource provider registered on the target subscription.
  • For a public frontend: an existing Standard-SKU public IP (or public IP prefix) β€” the SKU must match the load balancer.
  • For an internal frontend: an existing VNet and subnet.
  • Availability zones: a zone-redundant or zonal frontend requires a region that offers availability zones.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; the module declares none of these.

πŸ“ Module Structure

terraform-azurerm-load-balancer/
β”œβ”€β”€ providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
β”œβ”€β”€ variables.tf   # deeply-typed object() schemas + tags/timeouts tail
β”œβ”€β”€ main.tf        # azurerm_lb.this + 7 for_each children; dynamic + try()
β”œβ”€β”€ outputs.tf     # id first, then name, then child id maps
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # the cross-module contract
β”œβ”€β”€ LICENSE        # MIT
└── .gitignore     # canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-web-prod"
  resource_group_name = "rg-network-prod"
  location            = "eastus"

  frontend_ip_configurations = {
    public = {
      name                 = "public-fe"
      public_ip_address_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/publicIPAddresses/pip-web"
    }
  }

  backend_pools = { web = { name = "web-pool" } }
  probes        = { https = { name = "https", port = 443 } }

  rules = {
    https = {
      name                           = "https"
      frontend_ip_configuration_name = "public-fe"
      protocol                       = "Tcp"
      frontend_port                  = 443
      backend_port                   = 443
      backend_pool_keys              = ["web"]
      probe_key                      = "https"
    }
  }

  tags = { environment = "prod", workload = "web" }
}

ℹ️ The caller owns the provider "azurerm" { features {} } block, authentication, and the subscription. The module never declares provider config.

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group (name)
location string caller / resource group (location)
frontend_ip_configurations[*].public_ip_address_id string terraform-azurerm-public-ip (id)
frontend_ip_configurations[*].subnet_id string terraform-azurerm-virtual-network / -subnet (subnet id)

Emits

Output Description Consumed by
id Load balancer Resource ID (first) diagnostics, RBAC scope, downstream modules
name Load balancer name diagnostics / tagging
sku Load balancer SKU SKU-match checks
frontend_ip_configuration map: frontend name β†’ resolved attributes DNS, NAT wiring
private_ip_addresses list of assigned private IPs (internal frontends) DNS / documentation
backend_pool_ids map: key β†’ backend pool ID VM / VMSS association
probe_ids map: key β†’ probe ID downstream
rule_ids map: key β†’ rule ID downstream
nat_rule_ids map: key β†’ NAT rule ID downstream
nat_pool_ids map: key β†’ NAT pool ID VMSS NAT association
outbound_rule_ids map: key β†’ outbound rule ID downstream
backend_pool_address_ids map: key β†’ backend address ID downstream

πŸ“š Example Library

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "lb_eastus_frontend_ip_configuration" {
  description = "frontend ip configuration of an existing lb eastus that these examples reference but do not create."
  type        = map(string)
}

variable "lb_westus_frontend_ip_configuration" {
  description = "frontend ip configuration of an existing lb westus that these examples reference but do not create."
  type        = map(string)
}
1 Β· Minimal public load balancer
module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-web"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    public = {
      name                 = "public-fe"
      public_ip_address_id = azurerm_public_ip.web.id
    }
  }

  backend_pools = { web = { name = "web-pool" } }
  probes        = { tcp443 = { name = "tcp443", port = 443 } }

  rules = {
    https = {
      name                           = "https"
      frontend_ip_configuration_name = "public-fe"
      protocol                       = "Tcp"
      frontend_port                  = 443
      backend_port                   = 443
      backend_pool_keys              = ["web"]
      probe_key                      = "tcp443"
    }
  }
}

πŸ”’ With no opt-out typed, this is a Standard SKU load balancer β€” closed to inbound until a network security group on the backend allows the traffic.

2 Β· Internal (private) load balancer
module "lb_internal" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-internal"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    internal = {
      name                          = "internal-fe"
      subnet_id                     = azurerm_subnet.app.id
      private_ip_address            = "10.10.1.10"
      private_ip_address_allocation = "Static"
    }
  }

  backend_pools = { app = { name = "app-pool" } }
  probes        = { tcp8080 = { name = "tcp8080", port = 8080 } }

  rules = {
    app = {
      name                           = "app"
      frontend_ip_configuration_name = "internal-fe"
      protocol                       = "Tcp"
      frontend_port                  = 8080
      backend_port                   = 8080
      backend_pool_keys              = ["app"]
      probe_key                      = "tcp8080"
    }
  }
}

πŸ’‘ An internal frontend supplies a subnet_id. A Static private IP must fall inside that subnet's address range.

3 Β· Multiple load balancing rules on one frontend
module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-multi"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    public = { name = "public-fe", public_ip_address_id = azurerm_public_ip.web.id }
  }
  backend_pools = { web = { name = "web-pool" } }
  probes = {
    http  = { name = "http", port = 80, protocol = "Http", request_path = "/healthz" }
    https = { name = "https", port = 443 }
  }

  rules = {
    http = {
      name = "http", frontend_ip_configuration_name = "public-fe"
      protocol = "Tcp", frontend_port = 80, backend_port = 80
      backend_pool_keys = ["web"], probe_key = "http"
    }
    https = {
      name = "https", frontend_ip_configuration_name = "public-fe"
      protocol = "Tcp", frontend_port = 443, backend_port = 443
      backend_pool_keys = ["web"], probe_key = "https"
    }
  }
}

ℹ️ Each rule is a stable map key, so removing http later never re-indexes https.

4 Β· HA ports (all-protocol, all-port)
module "lb_haports" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-nva"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    internal = { name = "internal-fe", subnet_id = azurerm_subnet.nva.id }
  }
  backend_pools = { nva = { name = "nva-pool" } }
  probes        = { tcp = { name = "tcp", port = 22 } }

  rules = {
    ha = {
      name                           = "ha-ports"
      frontend_ip_configuration_name = "internal-fe"
      protocol                       = "All"
      frontend_port                  = 0
      backend_port                   = 0
      backend_pool_keys              = ["nva"]
      probe_key                      = "tcp"
    }
  }
}

⚠️ HA ports (frontend_port/backend_port = 0, protocol = "All") require a Standard internal load balancer. They are commonly used to front network virtual appliances.

5 Β· Health probe variants (TCP / HTTP / HTTPS)
probes = {
  tcp = {
    name = "tcp", port = 443, protocol = "Tcp"
    interval_in_seconds = 5, number_of_probes = 2
  }
  http = {
    name = "http", port = 80, protocol = "Http", request_path = "/healthz"
  }
  https = {
    name = "https", port = 443, protocol = "Https", request_path = "/health"
    probe_threshold = 3
  }
}

πŸ’‘ request_path is required for Http/Https probes and ignored for Tcp. probe_threshold sets how many consecutive results flip health state.

6 Β· Inbound NAT rule (single port forward)
module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-jump"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    public = { name = "public-fe", public_ip_address_id = azurerm_public_ip.jump.id }
  }

  nat_rules = {
    ssh = {
      name                           = "ssh"
      frontend_ip_configuration_name = "public-fe"
      protocol                       = "Tcp"
      frontend_port                  = 2222
      backend_port                   = 22
    }
  }
}

πŸ”’ Expose management ports through discrete NAT rules rather than broad inbound rules, and pair them with a restrictive network security group.

7 Β· Inbound NAT pool for a scale set (port range)
nat_pools = {
  ssh = {
    name                           = "ssh-pool"
    frontend_ip_configuration_name = "public-fe"
    protocol                       = "Tcp"
    frontend_port_start            = 50000
    frontend_port_end              = 50119
    backend_port                   = 22
  }
}

ℹ️ A NAT pool maps a frontend port range onto scale set instances. Reference module.lb.nat_pool_ids["ssh"] from the scale set's network_profile.

8 Β· Explicit outbound rule (egress) with SNAT disabled on the inbound rule
module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-egress"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    public = { name = "public-fe", public_ip_address_id = azurerm_public_ip.egress.id }
  }
  backend_pools = { web = { name = "web-pool" } }
  probes        = { https = { name = "https", port = 443 } }

  rules = {
    https = {
      name = "https", frontend_ip_configuration_name = "public-fe"
      protocol = "Tcp", frontend_port = 443, backend_port = 443
      backend_pool_keys = ["web"], probe_key = "https"
      disable_outbound_snat = true # default; egress handled explicitly below
    }
  }

  outbound_rules = {
    egress = {
      name                     = "egress"
      backend_pool_key         = "web"
      protocol                 = "All"
      frontend_names           = ["public-fe"]
      allocated_outbound_ports = 1024
      idle_timeout_in_minutes  = 15
    }
  }
}

πŸ’‘ disable_outbound_snat defaults to true in this module. Egress is expressed by an explicit outbound rule with a controlled allocated_outbound_ports budget, avoiding SNAT port exhaustion.

9 Β· Zone-redundant vs zonal frontend
frontend_ip_configurations = {
  # Zone-redundant: omit zones (Standard SKU default)
  zr = { name = "zr-fe", public_ip_address_id = azurerm_public_ip.zr.id }

  # Zonal: pin a single zone (public IP must be in the same zone)
  zonal = { name = "zonal-fe", public_ip_address_id = azurerm_public_ip.zonal.id, zones = ["1"] }
}

⚠️ A zonal frontend and its public IP must share the same zone. Availability zones require a region that offers them.

10 Β· Multiple public frontends on one load balancer
frontend_ip_configurations = {
  blue  = { name = "blue-fe", public_ip_address_id = azurerm_public_ip.blue.id }
  green = { name = "green-fe", public_ip_address_id = azurerm_public_ip.green.id }
}

rules = {
  blue = {
    name = "blue", frontend_ip_configuration_name = "blue-fe"
    protocol = "Tcp", frontend_port = 443, backend_port = 443
    backend_pool_keys = ["blue"], probe_key = "https"
  }
  green = {
    name = "green", frontend_ip_configuration_name = "green-fe"
    protocol = "Tcp", frontend_port = 443, backend_port = 443
    backend_pool_keys = ["green"], probe_key = "https"
  }
}

ℹ️ A single load balancer is either public or internal β€” it cannot mix public and private frontends. Use multiple frontends of the same type for blue/green or multi-tenant fronting.

11 Β· Register backend targets by IP address
module "lb_internal" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-app"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = {
    internal = { name = "internal-fe", subnet_id = azurerm_subnet.app.id }
  }
  backend_pools = { app = { name = "app-pool" } }

  backend_pool_addresses = {
    node1 = { name = "node1", backend_pool_key = "app", virtual_network_id = azurerm_virtual_network.hub.id, ip_address = "10.10.1.11" }
    node2 = { name = "node2", backend_pool_key = "app", virtual_network_id = azurerm_virtual_network.hub.id, ip_address = "10.10.1.12" }
  }
}

πŸ’‘ Backend addresses register targets by private IP within a VNet, useful when membership is not driven by NIC association.

12 Β· Cross-region (Global) load balancer
module "lb_global" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-global"
  resource_group_name = "rg-global"
  location            = "eastus"
  sku                 = "Standard"
  sku_tier            = "Global"

  frontend_ip_configurations = {
    global = { name = "global-fe", public_ip_address_id = azurerm_public_ip.global.id }
  }
  backend_pools = { regions = { name = "regional-lbs" } }

  backend_pool_addresses = {
    eastus = { name = "eastus", backend_pool_key = "regions", backend_address_ip_configuration_id = var.lb_eastus_frontend_ip_configuration["public-fe"].id }
    westus = { name = "westus", backend_pool_key = "regions", backend_address_ip_configuration_id = var.lb_westus_frontend_ip_configuration["public-fe"].id }
  }
}

ℹ️ sku_tier = "Global" builds a cross-region load balancer whose backend pool references regional Standard load balancer frontends via backend_address_ip_configuration_id. Requires a global-tier public IP.

13 Β· Gateway load balancer with tunnel interfaces
module "lb_gateway" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-gw"
  resource_group_name = "rg-net"
  location            = "eastus"
  sku                 = "Gateway"

  frontend_ip_configurations = {
    internal = { name = "gw-fe", subnet_id = azurerm_subnet.gw.id }
  }

  backend_pools = {
    nva = {
      name             = "nva-pool"
      tunnel_interfaces = {
        internal = { identifier = 800, port = 2000, protocol = "VXLAN", type = "Internal" }
        external = { identifier = 801, port = 2001, protocol = "VXLAN", type = "External" }
      }
    }
  }
}

⚠️ Tunnel interfaces are valid only on the Gateway SKU and are used to chain third-party network virtual appliances.

14 Β· Rules at scale with for_each
locals {
  services = {
    web   = { port = 443 }
    api   = { port = 8443 }
    grpc  = { port = 9443 }
    admin = { port = 7443 }
  }
}

module "lb" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  name                = "lb-scale"
  resource_group_name = "rg-net"
  location            = "eastus"

  frontend_ip_configurations = { public = { name = "public-fe", public_ip_address_id = azurerm_public_ip.scale.id } }
  backend_pools              = { pool = { name = "pool" } }
  probes                     = { for k, v in local.services : k => { name = k, port = v.port } }

  rules = {
    for k, v in local.services : k => {
      name                           = k
      frontend_ip_configuration_name = "public-fe"
      protocol                       = "Tcp"
      frontend_port                  = v.port
      backend_port                   = v.port
      backend_pool_keys              = ["pool"]
      probe_key                      = k
    }
  }
}

πŸ’‘ Because children are keyed maps, generating probes and rules from one source map keeps them aligned and free of re-indexing churn.

15 Β· πŸ—οΈ End-to-end composition (resource group + VNet + public IP + LB β†’ VMSS)
provider "azurerm" {
  features {}
}

module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-web-prod"
  location = "eastus"
}

module "vnet" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
  name                = "vnet-web"
  resource_group_name = module.rg.name
  location            = module.rg.location
  address_space       = ["10.20.0.0/16"]
  subnets = {
    web = { name = "web", address_prefixes = ["10.20.1.0/24"] }
  }
}

module "pip" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-public-ip.git?ref=v1.0.0"
  name                = "pip-web"
  resource_group_name = module.rg.name
  location            = module.rg.location
  allocation_method   = "Static"
  sku                 = "Standard" # must match the load balancer SKU
}

module "lb" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"
  name                = "lb-web-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location

  frontend_ip_configurations = {
    public = { name = "public-fe", public_ip_address_id = module.pip.id }
  }
  backend_pools = { web = { name = "web-pool" } }
  probes        = { https = { name = "https", port = 443 } }

  rules = {
    https = {
      name                           = "https"
      frontend_ip_configuration_name = "public-fe"
      protocol                       = "Tcp"
      frontend_port                  = 443
      backend_port                   = 443
      backend_pool_keys              = ["web"]
      probe_key                      = "https"
    }
  }

  outbound_rules = {
    egress = { name = "egress", backend_pool_key = "web", protocol = "All", frontend_names = ["public-fe"] }
  }

  tags = { environment = "prod", workload = "web" }
}

# The scale set joins the load balancer's backend pool by ID:
#   load_balancer_backend_address_pool_ids = [module.lb.backend_pool_ids["web"]]

πŸ—οΈ Real sibling outputs flow into inputs: module.rg.name β†’ resource_group_name, module.pip.id β†’ the public frontend, and module.lb.backend_pool_ids["web"] β†’ the scale set's backend association.

πŸ“₯ Inputs

Identity & keystone

Name Type Default Description
name string β€” (required) Load balancer name (immutable).
resource_group_name string β€” (required) Existing resource group name (immutable).
location string β€” (required) Azure region (immutable).
sku string "Standard" Standard | Basic | Gateway.
sku_tier string "Regional" Regional | Global.
edge_zone string null Optional Edge Zone (immutable).
frontend_ip_configurations map(object) β€” (required, β‰₯1) Public/internal frontends.

Child collections

Name Type Default Description
backend_pools map(object) {} Backend address pools.
probes map(object) {} Health probes.
rules map(object) {} Load balancing rules.
nat_rules map(object) {} Inbound NAT rules.
nat_pools map(object) {} Inbound NAT pools.
outbound_rules map(object) {} Outbound (egress) rules.
backend_pool_addresses map(object) {} Explicit backend targets.

Universal tail

Name Type Default Description
tags map(string) {} Tags applied to the load balancer.
timeouts object null Optional create/read/update/delete timeouts.
Full object() schemas
frontend_ip_configurations = map(object({
  name                          = string
  subnet_id                     = optional(string)
  private_ip_address            = optional(string)
  private_ip_address_allocation = optional(string, "Dynamic") # Static | Dynamic
  private_ip_address_version    = optional(string, "IPv4")    # IPv4 | IPv6
  public_ip_address_id          = optional(string)
  public_ip_prefix_id           = optional(string)
  gateway_load_balancer_frontend_ip_configuration_id = optional(string)
  zones                         = optional(set(string))       # null = zone-redundant on Standard
}))

backend_pools = map(object({
  name               = string
  synchronous_mode   = optional(string)
  virtual_network_id = optional(string)
  tunnel_interfaces = optional(map(object({
    identifier = number
    port       = number
    protocol   = string # None | Native | VXLAN
    type       = string # Internal | External
  })), {})
}))

probes = map(object({
  name                = string
  port                = number
  protocol            = optional(string, "Tcp") # Tcp | Http | Https
  request_path        = optional(string)
  interval_in_seconds = optional(number, 15)
  number_of_probes    = optional(number, 2)
  probe_threshold     = optional(number)
}))

rules = map(object({
  name                           = string
  frontend_ip_configuration_name = string
  protocol                       = string # Tcp | Udp | All
  frontend_port                  = number
  backend_port                   = number
  backend_pool_keys              = optional(list(string), [])
  probe_key                      = optional(string)
  disable_outbound_snat          = optional(bool, true)
  floating_ip_enabled            = optional(bool, false)
  tcp_reset_enabled              = optional(bool, true)
  idle_timeout_in_minutes        = optional(number)
  load_distribution              = optional(string) # Default | SourceIP | SourceIPProtocol
}))

nat_rules = map(object({
  name                           = string
  frontend_ip_configuration_name = string
  protocol                       = string # Tcp | Udp | All
  backend_port                   = number
  frontend_port                  = optional(number)
  frontend_port_start            = optional(number)
  frontend_port_end              = optional(number)
  backend_pool_key               = optional(string)
  floating_ip_enabled            = optional(bool, false)
  tcp_reset_enabled              = optional(bool, true)
  idle_timeout_in_minutes        = optional(number)
}))

nat_pools = map(object({
  name                           = string
  frontend_ip_configuration_name = string
  protocol                       = string # Tcp | Udp | All
  frontend_port_start            = number
  frontend_port_end              = number
  backend_port                   = number
  floating_ip_enabled            = optional(bool, false)
  tcp_reset_enabled              = optional(bool, true)
  idle_timeout_in_minutes        = optional(number)
}))

outbound_rules = map(object({
  name                     = string
  backend_pool_key         = string
  protocol                 = string # Tcp | Udp | All
  frontend_names           = list(string)
  allocated_outbound_ports = optional(number)
  idle_timeout_in_minutes  = optional(number)
  tcp_reset_enabled        = optional(bool, true)
}))

backend_pool_addresses = map(object({
  name                                = string
  backend_pool_key                    = string
  virtual_network_id                  = optional(string)
  ip_address                          = optional(string)
  backend_address_ip_configuration_id = optional(string)
}))

🧾 Outputs

Output Description Notes
id Load balancer Resource ID emitted first
name Load balancer name
location Azure region, in the canonical form Azure uses. Read from the resource, not var.location.
sku Load balancer SKU
frontend_ip_configuration map: frontend name β†’ resolved attributes (id, private_ip_address, allocation, subnet_id, public_ip_address_id, zones) private_ip_address populated for internal frontends
private_ip_addresses list of private IPs assigned to internal frontends
backend_pool_ids map: key β†’ backend pool ID
probe_ids map: key β†’ probe ID
rule_ids map: key β†’ rule ID
nat_rule_ids map: key β†’ NAT rule ID
nat_pool_ids map: key β†’ NAT pool ID
outbound_rule_ids map: key β†’ outbound rule ID
backend_pool_address_ids map: key β†’ backend address ID

🧠 Architecture Notes

  • Keystone + keyed children. One azurerm_lb.this owns the frontends (rendered as a dynamic block). Every other resource is a for_each map keyed by a stable identifier, so removing one rule or pool never re-creates the rest.
  • Reference by key, not by ID. Rules, NAT rules, outbound rules, and pool addresses point at backend pools and probes using their in-module map keys; main.tf resolves those to Resource IDs. Callers wire the topology without touching Azure Resource IDs, and Terraform's dependency graph orders parent before child implicitly (no depends_on).
  • Immutability traps. name, resource_group_name, location, sku, sku_tier, and edge_zone are force-new on the load balancer. A backend pool's virtual_network_id and a NAT rule/pool's resource_group_name behave similarly. Treat SKU and region changes as replacements, not edits.
  • SKU coupling. The load balancer SKU and its public IP SKU must match. The module keeps sku = "Standard" as the default and expects a Standard public IP.
  • Egress is explicit. disable_outbound_snat defaults to true on load balancing rules; provide an outbound_rules entry (or an external NAT gateway) so SNAT is deliberate and port-budgeted.
  • Deprecated argument avoidance. The provider still exposes enable_floating_ip / enable_tcp_reset, but this module renders only the current floating_ip_enabled / tcp_reset_enabled fields.
  • Tags. Only the load balancer carries tags; the child records do not support tags, so the module does not expose a tag input on them.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller types it)
Inbound exposure sku = "Standard" β€” closed to inbound until an NSG allows it sku = "Basic" (open by default; retiring)
Outbound SNAT rules[*].disable_outbound_snat = true (egress is explicit) set to false to use implicit SNAT
Availability frontends zone-redundant by default on Standard pin zones = ["1"] for a zonal frontend
TCP reset on idle tcp_reset_enabled = true on rules / NAT / outbound set to false
Floating IP floating_ip_enabled = false set to true for direct server return
Private IP allocation Dynamic unless a Static address is supplied supply private_ip_address + Static

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module by tag: ?ref=v1.0.0 β€” never a branch.
  • This library is plan-only during authoring. A human runs terraform plan / apply from CI against real credentials.

πŸ§ͺ Testing

The offline proof gate is the authoring contract:

  • terraform init -backend=false β€” resolves the pinned provider without a backend.
  • terraform validate β€” proves the configuration is internally consistent and type-correct against the pinned provider schema. The deeply-typed object() inputs surface a mistyped enum or a missing required field here, before any Azure call.
  • terraform fmt -check β€” enforces canonical formatting.

Neither validate nor fmt contacts Azure. Only terraform plan (run by a human, from CI, against credentials) exercises the ARM API β€” SKU-match rejections, zone availability, and address-range conflicts surface there.

πŸ’¬ Example Output

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

id                        = "/subscriptions/00000000-.../providers/Microsoft.Network/loadBalancers/lb-web-prod"
name                      = "lb-web-prod"
sku                       = "Standard"
frontend_ip_configuration = {
  "public-fe" = {
    "id"                   = ".../frontendIPConfigurations/public-fe"
    "private_ip_address"   = ""
    "public_ip_address_id" = ".../publicIPAddresses/pip-web"
    "zones"                = toset([])
  }
}
backend_pool_ids = { "web" = ".../backendAddressPools/web-pool" }
probe_ids        = { "https" = ".../probes/https" }
rule_ids         = { "https" = ".../loadBalancingRules/https" }

πŸ” Troubleshooting

Symptom Cause Fix
SkuMismatch at apply Public IP SKU differs from the load balancer SKU Use a Standard public IP with the default Standard load balancer.
Backends unreachable from the internet Standard SKU is closed until an NSG allows traffic Add an inbound NSG rule on the backend subnet/NIC for the load-balanced port.
SNAT port exhaustion / dropped outbound Implicit SNAT budget too small Add an outbound_rules entry with an explicit allocated_outbound_ports budget, or attach a NAT gateway.
Plan wants to replace the whole load balancer An immutable field changed (sku, location, name, resource_group_name, edge_zone) Revert the field, or accept the replacement deliberately.
key "..." does not exist on a rule/outbound rule backend_pool_keys / backend_pool_key / probe_key references a missing map key Add the pool/probe to backend_pools / probes, or correct the key.
Zonal frontend fails to create Frontend zone and public IP zone differ, or region lacks zones Align the zones, or omit zones for zone-redundant on Standard.
Provider fails to initialize in isolation Missing caller-side provider "azurerm" { features {} } Add the features {} block in the root module β€” modules never carry it.

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."