Managing Azure ExpressRoute with Terraform

Learn how to set up and manage Azure ExpressRoute using Terraform, including circuit and peering configurations

Azure ExpressRoute provides a private, dedicated, and high-bandwidth connection between your on-premises network and Azure. This guide demonstrates how to set up and manage ExpressRoute using Terraform.

Video Tutorial

Learn more about managing Azure ExpressRoute with Terraform in this comprehensive video tutorial:

Prerequisites

  • Azure CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Resource group created
  • Understanding of networking concepts
  • ExpressRoute service provider

Project Structure

terraform-azure-expressroute/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   └── expressroute/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── configs/
    └── peering.json

ExpressRoute Configuration

Create modules/expressroute/main.tf:

# ExpressRoute Circuit
resource "azurerm_express_route_circuit" "main" {
  name                  = "${var.project_name}-circuit"
  resource_group_name   = var.resource_group_name
  location              = var.location
  service_provider_name = "Equinix"
  peering_location      = "Silicon Valley"
  bandwidth_in_mbps     = 1000

  sku {
    tier   = "Premium"
    family = "MeteredData"
  }

  allow_classic_operations = false

  tags = var.tags
}

# ExpressRoute Circuit Authorization
resource "azurerm_express_route_circuit_authorization" "main" {
  name                       = "${var.project_name}-auth"
  express_route_circuit_name = azurerm_express_route_circuit.main.name
  resource_group_name        = var.resource_group_name
}

# Private Peering
# microsoft_peering_config and route_filter_id only apply to
# MicrosoftPeering (see the "microsoft" peering below) - AzurePrivatePeering
# has no Microsoft peering config or route filter.
resource "azurerm_express_route_circuit_peering" "private" {
  peering_type                  = "AzurePrivatePeering"
  express_route_circuit_name    = azurerm_express_route_circuit.main.name
  resource_group_name           = var.resource_group_name
  primary_peer_address_prefix   = "192.168.1.0/30"
  secondary_peer_address_prefix = "192.168.2.0/30"
  vlan_id                       = 100
  shared_key                    = var.peering_shared_key
  peer_asn                      = 65001
}

# Microsoft Peering
resource "azurerm_express_route_circuit_peering" "microsoft" {
  peering_type                  = "MicrosoftPeering"
  express_route_circuit_name    = azurerm_express_route_circuit.main.name
  resource_group_name           = var.resource_group_name
  primary_peer_address_prefix   = "192.168.3.0/30"
  secondary_peer_address_prefix = "192.168.4.0/30"
  vlan_id                       = 200
  shared_key                    = var.peering_shared_key
  peer_asn                      = 65001

  microsoft_peering_config {
    advertised_public_prefixes = ["123.0.0.0/24"]
    customer_asn              = 65001
    routing_registry_name     = "ARIN"
  }

  route_filter_id = azurerm_express_route_route_filter.main.id
}

# Route Filter
resource "azurerm_express_route_route_filter" "main" {
  name                = "${var.project_name}-filter"
  resource_group_name = var.resource_group_name
  location            = var.location

  rule {
    name        = "allow-o365"
    access      = "Allow"
    rule_type   = "Community"
    communities = ["12076:5010", "12076:5020"]
  }

  tags = var.tags
}

# ExpressRoute Gateway
resource "azurerm_virtual_network_gateway" "expressroute" {
  name                = "${var.project_name}-ergw"
  location            = var.location
  resource_group_name = var.resource_group_name
  type                = "ExpressRoute"
  sku                 = "ErGw2AZ"
  
  ip_configuration {
    name                          = "default"
    public_ip_address_id          = azurerm_public_ip.expressroute.id
    private_ip_address_allocation = "Dynamic"
    subnet_id                     = azurerm_subnet.gateway.id
  }

  tags = var.tags
}

# ExpressRoute Connection
resource "azurerm_virtual_network_gateway_connection" "expressroute" {
  name                = "${var.project_name}-connection"
  location            = var.location
  resource_group_name = var.resource_group_name

  type                           = "ExpressRoute"
  virtual_network_gateway_id     = azurerm_virtual_network_gateway.expressroute.id
  express_route_circuit_id       = azurerm_express_route_circuit.main.id
  routing_weight                = 10
  enable_bgp                    = true

  # FastPath bypasses the ExpressRoute gateway for supported SKUs (like the
  # ErGw2AZ gateway above) - it's a flag on the connection, not a separate
  # circuit connection resource.
  express_route_gateway_bypass = true

  tags = var.tags
}

# Global Reach
resource "azurerm_express_route_circuit_connection" "global" {
  name                = "${var.project_name}-global"
  peering_id          = azurerm_express_route_circuit_peering.private.id
  peer_peering_id     = var.peer_circuit_peering_id
  address_prefix      = "192.168.101.0/29"
}

Network Configuration

  1. Virtual Network Setup
resource "azurerm_virtual_network" "main" {
  name                = "${var.project_name}-vnet"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = ["10.0.0.0/16"]

  tags = var.tags
}

# The inline subnet block on azurerm_virtual_network is deprecated - use a
# standalone azurerm_subnet resource instead. The gateway subnet must be
# named exactly "GatewaySubnet" - Azure requires this literal name.
resource "azurerm_subnet" "gateway" {
  name                 = "GatewaySubnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.255.0/24"]
}

resource "azurerm_public_ip" "expressroute" {
  name                = "${var.project_name}-ergw-pip"
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = "Static"
  sku                 = "Standard"
  zones               = ["1", "2", "3"]

  tags = var.tags
}

Route Configuration

  1. Route Table
resource "azurerm_route_table" "main" {
  name                = "${var.project_name}-rt"
  location            = var.location
  resource_group_name = var.resource_group_name

  route {
    name                   = "ToOnPremise"
    address_prefix         = "192.168.0.0/16"
    next_hop_type         = "VirtualNetworkGateway"
  }

  tags = var.tags
}

# Associate the route table with the workload subnet whose traffic should
# route back on-premises via ExpressRoute (not the GatewaySubnet itself -
# Azure manages that subnet's routing directly). Point this at whichever
# subnet in your VNet actually needs the route.
resource "azurerm_subnet_route_table_association" "main" {
  subnet_id      = var.workload_subnet_id
  route_table_id = azurerm_route_table.main.id
}

Monitoring Configuration

  1. Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "expressroute" {
  name                       = "${var.project_name}-diag"
  target_resource_id        = azurerm_express_route_circuit.main.id
  log_analytics_workspace_id = var.log_analytics_workspace_id

  log {
    category = "PeeringRouteLog"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  metric {
    category = "AllMetrics"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }
}

resource "azurerm_monitor_metric_alert" "expressroute" {
  name                = "${var.project_name}-bandwidth-alert"
  resource_group_name = var.resource_group_name
  scopes              = [azurerm_express_route_circuit.main.id]
  description         = "Alert when bandwidth utilization is high"

  criteria {
    metric_namespace = "Microsoft.Network/expressRouteCircuits"
    metric_name      = "BitsInPerSecond"
    aggregation      = "Average"
    operator         = "GreaterThan"
    threshold        = 800000000
  }

  action {
    action_group_id = var.action_group_id
  }
}

Best Practices

  1. Performance

    • Use appropriate SKU
    • Enable FastPath
    • Configure BGP
    • Optimize routing
  2. High Availability

    • Use zone-redundant gateways
    • Configure redundant circuits
    • Implement failover
    • Monitor connections
  3. Security

    • Use private peering
    • Implement encryption
    • Configure authentication
    • Monitor traffic
  4. Cost Optimization

    • Choose appropriate bandwidth
    • Monitor usage
    • Optimize connections
    • Use appropriate tier

Conclusion

You’ve learned how to set up and manage Azure ExpressRoute using Terraform. This setup provides:

  • Private connectivity
  • High bandwidth
  • Global reach
  • Reliable performance

Remember to:

  • Monitor circuit status
  • Review peering settings
  • Update route filters
  • Maintain connections