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 targetinghashicorp/azurerm ~> 4.0.
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.
If this module saves you time, please consider supporting it:
- β Star the repository on GitHub.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
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. |
| 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, andedge_zoneare immutable β changing any of them replaces the load balancer and cascades to every child that references itsid.- 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_ipandenable_tcp_resetarguments are deprecated on v4.x; this module uses the currentfloating_ip_enabled/tcp_reset_enablednames. azurerm_lb_nat_ruleandazurerm_lb_nat_pooleach require aresource_group_name; the module supplies the load balancer's automatically.- HA ports are
frontend_port = 0/backend_port = 0withprotocol = "All"on a Standard internal load balancer.
Network Contributoron the target resource group (create/update the load balancer and all child records), or a custom role grantingMicrosoft.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.
- An existing resource group in a supported US Azure region.
- The
Microsoft.Networkresource 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.
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
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.
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 |
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. AStaticprivate 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
httplater never re-indexeshttps.
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_pathis required forHttp/Httpsprobes and ignored forTcp.probe_thresholdsets 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'snetwork_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_snatdefaults totruein this module. Egress is expressed by an explicit outbound rule with a controlledallocated_outbound_portsbudget, 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 viabackend_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 theGatewaySKU 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, andmodule.lb.backend_pool_ids["web"]β the scale set's backend association.
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)
}))| 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 |
- Keystone + keyed children. One
azurerm_lb.thisowns the frontends (rendered as adynamicblock). Every other resource is afor_eachmap 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.tfresolves those to Resource IDs. Callers wire the topology without touching Azure Resource IDs, and Terraform's dependency graph orders parent before child implicitly (nodepends_on). - Immutability traps.
name,resource_group_name,location,sku,sku_tier, andedge_zoneare force-new on the load balancer. A backend pool'svirtual_network_idand a NAT rule/pool'sresource_group_namebehave 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_snatdefaults totrueon load balancing rules; provide anoutbound_rulesentry (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 currentfloating_ip_enabled/tcp_reset_enabledfields. - 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.
| 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 |
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/applyfrom CI against real credentials.
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-typedobject()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.
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" }
| 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. |
- Provider resources:
azurerm_lb,azurerm_lb_backend_address_pool,azurerm_lb_probe,azurerm_lb_rule,azurerm_lb_nat_rule,azurerm_lb_nat_pool,azurerm_lb_outbound_rule,azurerm_lb_backend_address_pool_address - Azure docs: Azure Load Balancer overview
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-public-ip,terraform-azurerm-virtual-network,terraform-azurerm-virtual-machine-scale-set,terraform-azurerm-monitor-diagnostic-setting - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."