Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure ExpressRoute Connection Terraform Module

Connects an ExpressRoute circuit peering to an ExpressRoute gateway in a Virtual WAN hub, optionally with custom routing β€” built on hashicorp/azurerm ~> 4.0.

Terraform azurerm Version Type Resources Secret-aware


🧩 Overview

  • πŸ”— Creates a single azurerm_express_route_connection β€” the join that carries traffic between an on-premises ExpressRoute circuit and a Virtual WAN hub's ExpressRoute gateway.
  • 🌐 References the ExpressRoute gateway (express_route_gateway_id) and the circuit peering (express_route_circuit_peering_id) by resource ID; it does not create either.
  • πŸ” Accepts an optional authorization_key for connecting to a circuit in another subscription β€” treated as a secret and never rendered in plan output.
  • 🧭 Exposes an optional routing block for associated/propagated route tables and inbound/outbound route maps, plus a range-validated routing_weight.
  • 🚦 Toggles for internet security, ExpressRoute gateway bypass (Fast Path), and Private Link Fast Path, each defaulting to the provider's locked-down value.

πŸ’‘ Why it matters: The connection is where an ExpressRoute circuit actually becomes usable from a Virtual WAN hub. Getting the peering ID, gateway ID, and β€” for cross-subscription circuits β€” the authorization key right is the difference between a hub that routes on-premises traffic and one that silently does not.


❀️ Support this project

If this module saves you time, please consider supporting its continued development:


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

flowchart LR
  gw["terraform-azurerm-express-route-gateway"]
  circ["terraform-azurerm-express-route-circuit (peering)"]
  me["terraform-azurerm-express-route-connection"]
  v["azurerm_express_route_connection"]
  gw -->|"express_route_gateway_id"| me
  circ -->|"express_route_circuit_peering_id"| me
  me -->|"creates"| v
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
  classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
  class me me;
  class v target;
  class gw,circ ext;
Loading

The connection is a leaf: an ExpressRoute gateway and a circuit peering feed their IDs in, and this module wires them together. Route tables and route maps referenced from the routing block are owned elsewhere and consumed by ID.


🧬 What this module builds

flowchart LR
  in_id["name / express_route_gateway_id / express_route_circuit_peering_id"]
  in_sec["authorization_key (sensitive) / internet_security_enabled"]
  in_route["routing_weight / routing (route tables + maps)"]
  res["azurerm_express_route_connection.this"]
  out_id["id / name"]
  in_id -->|"input"| res
  in_sec -->|"input"| res
  in_route -->|"input"| res
  res -->|"output"| out_id
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  class res me;
Loading

Resource inventory

Resource Cardinality Role
azurerm_express_route_connection.this single (keystone) The connection joining a circuit peering to an ExpressRoute gateway.

The optional routing and timeouts blocks are rendered as dynamic blocks β€” present only when the caller supplies them.


βœ… Provider / Versions

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

Schema notes that bite

  • name, express_route_circuit_peering_id, and express_route_gateway_id are force-new β€” changing any of them destroys and recreates the connection.
  • authorization_key is sensitive; it is required only for a circuit that lives in a different subscription than the gateway. Provision it out of band and pass a reference.
  • routing_weight must be between 0 and 32000 inclusive β€” enforced at parse time by a validation {} block, before any Azure call.
  • internet_security_enabled is the current argument name; the older enable_internet_security is deprecated and must not be used.

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege, at the smallest scope that works:

  • Microsoft.Network/expressRouteGateways/expressRouteConnections/write, /read, /delete on the gateway.
  • Microsoft.Network/expressRouteCircuits/peerings/read on the peering.

The built-in Network Contributor role covers these.


🧰 Azure Prerequisites

  • An existing ExpressRoute gateway in a Virtual WAN hub.
  • An existing circuit peering (azurerm_express_route_circuit_peering).
  • For a cross-subscription circuit, an authorization key generated on the circuit.
  • The Microsoft.Network resource provider registered on the subscription.

πŸ“ Module Structure

terraform-azurerm-express-route-connection/
β”œβ”€β”€ providers.tf    # required_version + azurerm ~> 4.0 pin; no provider block
β”œβ”€β”€ variables.tf    # deeply-typed inputs; sensitive authorization_key; timeouts tail
β”œβ”€β”€ main.tf         # keystone azurerm_express_route_connection.this + dynamic routing/timeouts
β”œβ”€β”€ outputs.tf      # id (first), then name
β”œβ”€β”€ README.md       # this document
β”œβ”€β”€ SCOPE.md        # cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore      # canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-hub-prod"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id
}

ℹ️ The caller owns the provider, its authentication, and the mandatory features {} block. This module never declares a provider {} block.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
express_route_gateway_id string terraform-azurerm-express-route-gateway
express_route_circuit_peering_id string terraform-azurerm-express-route-circuit (a peering)

Emits

Output Description Consumed by
id Resource ID of the ExpressRoute connection references
name Connection name references

πŸ“š Example Library

1 Β· Minimal connection

The smallest real call β€” a same-subscription circuit, no custom routing.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-hub-prod"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id
}
2 Β· Cross-subscription connection with an authorization key

When the circuit lives in a different subscription than the gateway, pass an authorization key generated on the circuit.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-cross-sub"
  express_route_gateway_id         = var.gateway_id
  express_route_circuit_peering_id = var.peering_id
  authorization_key                = var.er_authorization_key
}

πŸ”’ authorization_key is a secret. Provision it out of band (Key Vault, a CI secret store, or an out-of-band variable) and pass a reference β€” never commit the literal value. It is marked sensitive = true, so it is redacted from plan output.

3 Β· Custom routing weight

Bias path selection with a routing weight (0–32000).

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-weighted"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id
  routing_weight                   = 100
}

⚠️ routing_weight outside 0–32000 fails at plan with a clear message β€” it never reaches Azure.

4 Β· Associated route table

Point the connection at a specific hub route table for its associated route.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-assoc-rt"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id

  routing = {
    associated_route_table_id = var.default_route_table_id
  }
}
5 Β· Propagated route tables with labels

Propagate routes learned over the connection into one or more route tables, tagged with labels.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-propagate"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id

  routing = {
    associated_route_table_id = var.default_route_table_id
    propagated_route_table = {
      labels          = ["default"]
      route_table_ids = [var.default_route_table_id]
    }
  }
}
6 Β· Inbound and outbound route maps

Attach route maps to transform advertised and received prefixes.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-route-maps"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id

  routing = {
    associated_route_table_id = var.default_route_table_id
    inbound_route_map_id      = var.inbound_route_map_id
    outbound_route_map_id     = var.outbound_route_map_id
  }
}
7 Β· Internet security enabled

Route internet-bound traffic from the connection through the hub's security stack.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-internet-secured"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id
  internet_security_enabled        = true
}

ℹ️ internet_security_enabled is the current argument name; the deprecated enable_internet_security is not used by this module.

8 Β· ExpressRoute gateway bypass (Fast Path)

Enable Fast Path so data-plane traffic bypasses the Virtual WAN firewall hub gateway.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                                 = "erc-fastpath"
  express_route_gateway_id             = module.er_gateway.id
  express_route_circuit_peering_id     = module.er_peering.id
  express_route_gateway_bypass_enabled = true
}
9 Β· Private Link Fast Path

Enable Private Link Fast Path for connections that reach Private Link services over ExpressRoute.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-pl-fastpath"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id
  private_link_fast_path_enabled   = true
}
10 Β· Custom timeouts

Override the per-operation timeouts for a slow-to-provision connection.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-timeouts"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}
11 Β· Full routing plus Fast Path

Combine associated and propagated route tables, route maps, a routing weight, and Fast Path in one call.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                                 = "erc-full"
  express_route_gateway_id             = module.er_gateway.id
  express_route_circuit_peering_id     = module.er_peering.id
  routing_weight                       = 200
  express_route_gateway_bypass_enabled = true

  routing = {
    associated_route_table_id = var.default_route_table_id
    inbound_route_map_id      = var.inbound_route_map_id
    outbound_route_map_id     = var.outbound_route_map_id
    propagated_route_table = {
      labels          = ["default", "prod"]
      route_table_ids = [var.default_route_table_id, var.prod_route_table_id]
    }
  }
}
12 Β· Cross-subscription with full routing

A cross-subscription circuit that also propagates routes and biases path selection.

module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-cross-sub-routed"
  express_route_gateway_id         = var.gateway_id
  express_route_circuit_peering_id = var.peering_id
  authorization_key                = var.er_authorization_key
  routing_weight                   = 150

  routing = {
    associated_route_table_id = var.default_route_table_id
    propagated_route_table = {
      labels          = ["default"]
      route_table_ids = [var.default_route_table_id]
    }
  }
}

πŸ”’ Keep authorization_key out of source control β€” source it from a secret store and pass a reference.

13 Β· Two connections, one gateway (for_each at scale)

Wire several circuit peerings into the same gateway from one caller.

locals {
  circuits = {
    east = { peering_id = var.east_peering_id, weight = 100 }
    west = { peering_id = var.west_peering_id, weight = 100 }
  }
}

module "er_connection" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"
  for_each = local.circuits

  name                             = "erc-${each.key}"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = each.value.peering_id
  routing_weight                   = each.value.weight
}
14 Β· πŸ—οΈ End-to-end composition

Wire a resource group, an ExpressRoute gateway, and a circuit peering into this connection β€” the full path from empty subscription to a routed hub, including a cross-subscription authorization key.

provider "azurerm" {
  features {}
}

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

module "er_gateway" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-gateway.git?ref=v1.0.0"

  scale_units                  = 1
  name                = "ergw-hub-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
  virtual_hub_id      = var.virtual_hub_id
}

module "er_circuit" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-circuit.git?ref=v1.0.0"
  name                = "erc-circuit-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
  service_provider_name = var.service_provider
  peering_location      = "Washington DC"
}
module "er_peering" {
  source                     = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-circuit-peering.git?ref=v1.0.0"
  express_route_circuit_name = "erc-circuit-prod"
  resource_group_name        = module.rg.name
  peering_type               = "AzurePrivatePeering"
  vlan_id                    = 100
}


module "er_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-connection.git?ref=v1.0.0"

  name                             = "erc-hub-prod"
  express_route_gateway_id         = module.er_gateway.id
  express_route_circuit_peering_id = module.er_peering.id

  # Only needed when the circuit is in another subscription.
  authorization_key = var.er_authorization_key

  routing_weight = 100
  routing = {
    associated_route_table_id = var.default_route_table_id
    propagated_route_table = {
      labels          = ["default"]
      route_table_ids = [var.default_route_table_id]
    }
  }
}

πŸ”’ The authorization_key here comes from a variable sourced out of band. Treat it as a secret: never hard-code it, and never emit it downstream.


πŸ“₯ Inputs

Identity & links β€” name, express_route_gateway_id, express_route_circuit_peering_id (all required, all force-new). Secret β€” authorization_key (optional, sensitive). Behavior β€” internet_security_enabled, express_route_gateway_bypass_enabled, private_link_fast_path_enabled, routing_weight. Routing β€” routing (associated/propagated route tables + inbound/outbound route maps). Tail β€” timeouts. This resource does not support tags.

Full input schemas
Name Type Default Description
name string β€” (required) Connection name. Force-new.
express_route_circuit_peering_id string β€” (required) Resource ID of the circuit peering to connect. Force-new.
express_route_gateway_id string β€” (required) Resource ID of the ExpressRoute gateway. Force-new.
authorization_key string null Sensitive. Authorization key for a cross-subscription circuit; provision out of band.
internet_security_enabled bool null Whether internet security is enabled. null uses the provider default (false).
express_route_gateway_bypass_enabled bool null Fast Path for the Virtual WAN firewall hub. null uses the provider default (false).
private_link_fast_path_enabled bool null Private Link Fast Path. null uses the provider default (false).
routing_weight number null Routing weight, 0–32000. null uses the provider default (0). Range-validated.
routing object null Custom routing configuration (see below).
timeouts object null Per-operation timeouts (create, read, update, delete).

The routing object:

routing = object({
  associated_route_table_id = optional(string)
  inbound_route_map_id      = optional(string)
  outbound_route_map_id     = optional(string)
  propagated_route_table = optional(object({
    labels          = optional(list(string))
    route_table_ids = optional(list(string))
  }))
})

🧾 Outputs

Output Description Kind
id Resource ID of the ExpressRoute connection Passthrough
name Name of the connection, as created Passthrough
express_route_gateway_id Resource ID of the Virtual WAN ExpressRoute gateway this connection was attached to, as Azure returned it Passthrough
express_route_circuit_peering_id Resource ID of the circuit peering, as Azure returned it and re-rendered in canonical form Passthrough
routing_weight Effective routing weight, as Azure returned it Passthrough
internet_security_enabled Whether the hub advertises the default route 0.0.0.0/0 to this connection, as Azure returned it Passthrough
express_route_gateway_bypass_enabled Whether FastPath is enabled, taking the hub gateway out of the data path for this connection Passthrough
express_route_gateway_name Name of the ExpressRoute gateway parsed out of express_route_gateway_id Derived
resource_group_name Resource group the connection is created in, parsed out of the GATEWAY's ID Derived
subscription_id Subscription the connection is created in, parsed out of the GATEWAY's ID Derived
express_route_circuit_id Resource ID of the parent ExpressRoute circuit, derived by trimming the /peerings/NAME tail off express_route_circuit_peering_id Derived
express_route_circuit_name Name of the parent circuit, parsed out of the peering ID Derived
express_route_circuit_subscription_id Subscription holding the circuit, parsed out of the peering ID Derived
express_route_circuit_resource_group_name Resource group holding the circuit, parsed out of the peering ID Derived
peering_name Name of the circuit peering, parsed out of the peering ID -- in practice AzurePrivatePeering, MicrosoftPeering or AzurePublicPeering, because an ExpressRoute peering's resource name IS its peering type Derived
is_cross_subscription Whether the circuit and the gateway are in different subscriptions, compared case-insensitively Derived
cross_subscription_without_an_authorization_key True when the circuit is in a different subscription from the gateway and no authorization_key was supplied Derived
peering_id_uses_canonical_casing Whether the supplied express_route_circuit_peering_id spells the literal segments subscriptions, resourceGroups, providers, Microsoft.Network, expressRouteCircuits and peerings exactly as Azure does Derived
has_authorization_key Whether an authorization key was supplied Passthrough
authorization_key_is_stored_in_state_in_plaintext Constant true, and it is a correction rather than a reassurance Constant
authorization_can_be_revoked_by_the_circuit_owner Constant true, and the blast radius no Azure role on THIS resource limits Constant
one_authorization_serves_one_connection Constant true Constant
internet_security_is_not_a_simple_open_or_closed_toggle Constant true, and the reason this module keeps Azure's default instead of forcing a value Constant
internet_security_setting_is_pinned Whether this module sends an explicit value for internet_security_enabled rather than leaving it to the provider Derived
private_link_fast_path_enabled_has_no_effect Constant true Constant
arguments_removed_in_provider_v5 The arguments of this resource that carry a deprecation notice in the pinned v4.x line and are slated for removal in the provider's v5.0 major release Derived
has_custom_routing Whether a routing block was supplied Derived
associated_route_table_id The hub route table this connection is associated with, as Azure returned it Derived
propagated_route_table_ids Hub route tables this connection's routes propagate into, as Azure returned them Derived
propagated_route_table_labels Propagation labels applied to this connection, as Azure returned them Derived
uses_route_maps Whether an inbound or outbound route map was configured Derived
omitting_routing_adopts_azures_choice_silently Constant true Constant
force_new_arguments The arguments that cannot be changed in place Derived
arguments_that_update_in_place The arguments with a real update path on this resource Derived
fields_azure_returns_on_read The fields the provider's read populates from the Azure API, and therefore the only fields in which drift is detected at all Derived
read_writes_canonical_ids_into_force_new_fields Constant true, and the most likely cause of a replacement you did not ask for Constant
update_is_a_read_modify_write_of_the_live_resource Constant true Constant
destroy_removes_the_connection_only Constant true Constant
destroy_stops_traffic_on_this_path Constant true, and worth saying plainly because nothing in a plan does Constant
connection_id_is_derived_from_the_gateway Constant true Constant
connection_name_must_be_unique_within_the_gateway Constant true, and it follows directly from how the ID is composed rather than from any published quota Constant
tags_are_not_supported Constant true Constant
billed_by_the_circuit_not_by_this_record Constant true Constant

No secret is ever emitted; authorization_key is an input only.


🧠 Architecture Notes

  • Force-new fields. name, express_route_circuit_peering_id, and express_route_gateway_id all force replacement when changed. Renaming a connection or re-pointing it at a different gateway or peering destroys and recreates the resource β€” plan the change deliberately.
  • Secret handling. authorization_key is sensitive = true. It is required only for cross-subscription circuits, is redacted from plan and state display, and is never surfaced as an output. Provision it out of band and pass a reference.
  • The routing block. Rendered as a dynamic block that is present only when the caller supplies routing; every nested field is guarded with try(...), and propagated_route_table is itself a nested dynamic block. An empty call emits no routing configuration and lets the platform default apply.
  • features {} dependence. Like every module in this library, this one declares no provider {} block. The provider "azurerm" { features {} } block is the caller's responsibility; without it the provider will not initialize.

🧱 Design Principles

Concern Default (empty call) Opt-out (caller types it)
authorization_key Not sent (null); sensitive = true when supplied Pass a secret reference for cross-subscription circuits
internet_security_enabled Provider default (false) Set to true
express_route_gateway_bypass_enabled Provider default (false) Set to true
private_link_fast_path_enabled Provider default (false) Set to true
routing_weight Provider default (0); range-validated 0–32000 Set a value in range
routing None (platform default routing) Supply the routing object
tags Not supported by this resource β€”

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module at a tag β€” ?ref=v1.0.0 β€” never a branch. This library is plan-only; a human applies from CI after review. No cloud apply is performed during authoring or validation.


πŸ§ͺ Testing

  • terraform init -backend=false β€” resolves the provider without touching a backend.
  • terraform validate β€” proves the type contract: the deeply-typed routing object, the routing_weight range validation, and required-field presence all fail here, before any Azure call.
  • terraform fmt -check β€” enforces canonical formatting.
  • What only plan/apply against a real subscription exercises: force-new behavior on the identity fields, cross-subscription authorization-key acceptance, and actual route propagation into the hub.

πŸ’¬ Example Output

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

Outputs:

id                = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/expressRouteGateways/ergw-hub-prod/expressRouteConnections/erc-hub-prod"
name              = "erc-hub-prod"
authorization_key = <sensitive>

πŸ” Troubleshooting

Symptom Cause Fix
Connection provisions but stays disconnected Circuit is in another subscription and no authorization key was supplied Generate an authorization key on the circuit and pass it as authorization_key.
routing_weight must be between 0 and 32000 inclusive at plan routing_weight out of range Set a value in 0–32000, or leave it null for the provider default.
Plan shows the connection being destroyed and recreated A force-new field (name, express_route_gateway_id, or express_route_circuit_peering_id) changed Confirm the change is intended; these fields cannot be updated in place.
Provider fails to initialize Caller root module is missing provider "azurerm" { features {} } Add the features {} block to the caller's provider configuration.
Route propagation not taking effect routing block omitted or propagated_route_table not set Supply routing.propagated_route_table with the target route_table_ids and labels.

πŸ”— Related Docs

  • Provider resource: azurerm_express_route_connection
  • Sibling modules: terraform-azurerm-express-route-gateway, terraform-azurerm-express-route-circuit
  • This module's SCOPE.md β€” the cross-module contract.

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