Connects an ExpressRoute circuit peering to an ExpressRoute gateway in a Virtual WAN hub, optionally with custom routing β built on
hashicorp/azurerm ~> 4.0.
- π 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_keyfor connecting to a circuit in another subscription β treated as a secret and never rendered in plan output. - π§ Exposes an optional
routingblock for associated/propagated route tables and inbound/outbound route maps, plus a range-validatedrouting_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.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub.
- πΌ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
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.
| 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, andexpress_route_gateway_idare force-new β changing any of them destroys and recreates the connection.authorization_keyis 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_weightmust be between 0 and 32000 inclusive β enforced at parse time by avalidation {}block, before any Azure call.internet_security_enabledis the current argument name; the olderenable_internet_securityis deprecated and must not be used.
Least-privilege, at the smallest scope that works:
Microsoft.Network/expressRouteGateways/expressRouteConnections/write,/read,/deleteon the gateway.Microsoft.Network/expressRouteCircuits/peerings/readon the peering.
The built-in Network Contributor role covers these.
- 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.Networkresource provider registered on the subscription.
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
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 aprovider {}block.
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 |
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_keyis 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 markedsensitive = 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_weightoutside 0β32000 fails atplanwith 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_enabledis the current argument name; the deprecatedenable_internet_securityis 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_keyout 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_keyhere comes from a variable sourced out of band. Treat it as a secret: never hard-code it, and never emit it downstream.
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))
}))
})| 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.
- Force-new fields.
name,express_route_circuit_peering_id, andexpress_route_gateway_idall 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_keyissensitive = 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
routingblock. Rendered as adynamicblock that is present only when the caller suppliesrouting; every nested field is guarded withtry(...), andpropagated_route_tableis itself a nesteddynamicblock. An empty call emits no routing configuration and lets the platform default apply. features {}dependence. Like every module in this library, this one declares noprovider {}block. Theprovider "azurerm" { features {} }block is the caller's responsibility; without it the provider will not initialize.
| 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 | β |
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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.
terraform init -backend=falseβ resolves the provider without touching a backend.terraform validateβ proves the type contract: the deeply-typedroutingobject, therouting_weightrange validation, and required-field presence all fail here, before any Azure call.terraform fmt -checkβ enforces canonical formatting.- What only
plan/applyagainst a real subscription exercises: force-new behavior on the identity fields, cross-subscription authorization-key acceptance, and actual route propagation into the hub.
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>
| 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. |
- 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."