🙌

複数の Hub-spoke アーキテクチャで spoke-to-spoke を実現する (ExpressRoute 利用)

2022/07/26に公開約13,100字

ExpressRoute 折り返しで multi hub-spoke の構成を試す

今回は ExpressRoute circuit を利用して 2 つの hub を接続します。
言い方としては完全閉域での接続となります。

メリットとしては Azure Route Server を利用せずとてもシンプルな構成です。
デメリットとしては帯域が ExpressRoute circuit および ExpressRoute Gateway の SKU に依存します。

multiple hub-spoke architecture pattern with ExpressRoute

hub と spoke との VNet Peering において "Use the remote virtual network's gateway or Route Server" オプションを有効化することで spoke のアドレス空間が ExpressRoute の BGP に乗ることになります。
そのため、お互いの ExpressRoute 接続において hub と 2 つの spoke の合計 3 つの経路が広報され動的ルーティングにより学習するため、この構成でも spoke-to-spoke の接続が実現します。
前回同様、spoke10 と spoke20 の間は通信ができませんが、UDR などで容易に到達性を持たせられるため省略しています。

動作確認のスクリーンショットはこちらです。
裏に隠れているのが vm-spoke10 自体の Server Manager、左下が vm-hub00、右下が vm-hub100、その上に vm-spoke110 と vm-spoke120 という感じです。

remote desktop connection for multiple hub-spoke with ExpressRoute

vm-spoke10 の effective routes はどのようになっているか

結局ここが重要なので、確認しておきます。
vm-spoke10 の NIC である nic-spoke10 において、effective routes から表示させます。
10.10.0.0/16 が vm-spoke10 のある VNet の IP アドレス空間となっています。
10.0.0.0/16 は直接つながっている hub となっているので Next Hop Type が VNet peering となっています。
そのほか 2 番目の hub である 10.100.0.0/16 やその先にある spoke たちである 10.110.0.0/16 と 10.120.0.0/16 は Virtual network gateway となっています。
これは ExpressRoute 経由で折り返し接続なのでまぁ妥当かと思います。
この場合の Next Hop IP Address は Azure 内部のどこかの IP アドレスがそのまま表示されてしまっているのであまり気にする必要はありません。(10.2.146.76 と 10.2.146.77)

Source State Address Prefixes Next Hop Type Next Hop IP Address User Defined Route Name
Default Active 10.10.0.0/16 Virtual network - -
Default Active 10.0.0.0/16 VNet peering - -
Virtual network gateway Active 10.100.0.0/16 Virtual network gateway 10.2.146.76 -
Virtual network gateway Active 10.100.0.0/16 Virtual network gateway 10.2.146.77 -
Virtual network gateway Active 10.120.0.0/16 Virtual network gateway 10.2.146.76 -
Virtual network gateway Active 10.120.0.0/16 Virtual network gateway 10.2.146.77 -
Virtual network gateway Active 10.110.0.0/16 Virtual network gateway 10.2.146.76 -
Virtual network gateway Active 10.110.0.0/16 Virtual network gateway 10.2.146.77 -

tracert の結果はどうなるか

事前に、tracert に反応させるため、それぞれの Windows Server では Windows Firewall with Advanced Security を無効化しておきます。

まず、となりの hub にある vm-hub00 までは 1 hop で見えてきます。

> tracert -d 10.0.0.5

Tracing route to 10.0.0.5 over a maximum of 30 hops

  1     2 ms     1 ms    <1 ms  10.0.0.5

Trace complete.

別の hub およびその spoke たちに対しては、2 hop で見えてきます。
RTT がやたら長いですが、これは検証環境の VM たちは East Asia、ExpressRoute circuit が Japan East にあるためで、今回はあまり気にしなくてよいです。
途中に反応しないコンポーネントがあるようですが、おそらく ExpressRoute を構成する何かだと思います。
こちらもあまり気にするポイントではありません。
いずれにせよ無駄な経路を通らず、意図とおりに通信できているように見えます。

> tracert -d 10.100.0.5

Tracing route to 10.100.0.5 over a maximum of 30 hops

  1     *        *        *     Request timed out.
  2   105 ms   106 ms   105 ms  10.100.0.5

Trace complete.
> tracert -d 10.110.0.5

Tracing route to 10.110.0.5 over a maximum of 30 hops

  1     *        *        *     Request timed out.
  2   113 ms   110 ms   110 ms  10.110.0.5

Trace complete.
> tracert -d 10.120.0.5

Tracing route to 10.120.0.5 over a maximum of 30 hops

  1     *        *        *     Request timed out.
  2   106 ms   105 ms   106 ms  10.120.0.5

Trace complete.

Bicep ファイルはこちらです。

main.bicep
main.bicep
param location01 string = 'eastasia'
param location02 string = 'eastasia'

param kvName string
param kvRGName string
param secretName string

param cct01Name string
param cct01RgName string

resource kv 'Microsoft.KeyVault/vaults@2021-10-01' existing = {
  name: kvName
  scope: resourceGroup(kvRGName)
}

resource vnet00 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-hub00'
  location: location01
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.0.0.0/24'
        }
      }
      {
        name: 'AzureBastionSubnet'
        properties: {
          addressPrefix: '10.0.100.0/24'
        }
      }
      {
        name: 'GatewaySubnet'
        properties: {
          addressPrefix: '10.0.200.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

resource spoke10 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-spoke10'
  location: location01
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.10.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.10.0.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

module peering_hub0010 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub00-spoke10'
  params: {
    vnet01Name: vnet00.name
    vnet02Name: spoke10.name
    useRemoteGateways: true
    remoteGatewayName: ergw01.name
  }
}

resource spoke20 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-spoke20'
  location: location01
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.20.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.20.0.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

module peering_hub0020 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub00-spoke20'
  params: {
    vnet01Name: vnet00.name
    vnet02Name: spoke20.name
    useRemoteGateways: true
    remoteGatewayName: ergw01.name
  }
}

var ergw01Name = 'ergw-hub00'
module ergw01 '../lib/ergw_single.bicep' = {
  name: ergw01Name
  params: {
    location: location01
    vnetName: vnet00.name
    gatewayName: ergw01Name
  }
}

module conn_cct01_ergw01 '../lib/connection-ergw.bicep' = {
  name: 'conn-${cct01Name}-${ergw01Name}'
  params: {
    cctName: cct01Name
    cctRGName: cct01RgName
    ergwName: ergw01.name
  }
}

var bast01Name = 'bast-hub00'
module bast01 '../lib/bastion.bicep' = {
  name: bast01Name
  params: {
    location: location01
    bastionName: bast01Name
    vnetName: vnet00.name
  }
}

var vm01Name = 'vm-hub00'
module vm_loc01 '../lib/ws2019.bicep' = {
  name: vm01Name
  params: {
    location: location01
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: vnet00::defaultsubnet.id
    vmName: vm01Name
  }
}

var vm02Name = 'vm-spoke10'
module vm_spoke01 '../lib/ws2019.bicep' = {
  name: vm02Name
  params: {
    location: location01
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: spoke10::defaultsubnet.id
    vmName: vm02Name
  }
}

var vm03Name = 'vm-spoke20'
module vm_spoke02 '../lib/ws2019.bicep' = {
  name: vm03Name
  params: {
    location: location01
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: spoke20::defaultsubnet.id
    vmName: vm03Name
  }
}

resource vnet100 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-hub100'
  location: location02
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.100.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.100.0.0/24'
        }
      }
      {
        name: 'AzureBastionSubnet'
        properties: {
          addressPrefix: '10.100.100.0/24'
        }
      }
      {
        name: 'GatewaySubnet'
        properties: {
          addressPrefix: '10.100.200.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

resource spoke110 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-spoke110'
  location: location02
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.110.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.110.0.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

module peering_hub100110 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub100-spoke110'
  params: {
    vnet01Name: vnet100.name
    vnet02Name: spoke110.name
    useRemoteGateways: true
    remoteGatewayName: ergw01.name
  }
}

resource spoke120 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-spoke120'
  location: location02
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.120.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.120.0.0/24'
        }
      }
    ]
  }

  resource defaultsubnet 'subnets' existing = {
    name: 'default'
  }
}

module peering_hub100120 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub100-spoke120'
  params: {
    vnet01Name: vnet100.name
    vnet02Name: spoke120.name
    useRemoteGateways: true
    remoteGatewayName: ergw01.name
  }
}

var ergw02Name = 'ergw-hub100'
module ergw02 '../lib/ergw_single.bicep' = {
  name: ergw02Name
  params: {
    location: location02
    vnetName: vnet100.name
    gatewayName: ergw02Name
  }
}

module conn_cct01_ergw02 '../lib/connection-ergw.bicep' = {
  name: 'conn-${cct01Name}-${ergw02Name}'
  params: {
    cctName: cct01Name
    cctRGName: cct01RgName
    ergwName: ergw02.name
  }
}

var bast02Name = 'bast-hub100'
module bast02 '../lib/bastion.bicep' = {
  name: bast02Name
  params: {
    location: location02
    bastionName: bast02Name
    vnetName: vnet100.name
  }
}

var vm11Name = 'vm-hub100'
module vm_loc02 '../lib/ws2019.bicep' = {
  name: vm11Name
  params: {
    location: location02
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: vnet100::defaultsubnet.id
    vmName: vm11Name
  }
}

var vm12Name = 'vm-spoke110'
module vm_spoke11 '../lib/ws2019.bicep' = {
  name: vm12Name
  params: {
    location: location02
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: spoke110::defaultsubnet.id
    vmName: vm12Name
  }
}

var vm13Name = 'vm-spoke120'
module vm_spoke12 '../lib/ws2019.bicep' = {
  name: vm13Name
  params: {
    location: location02
    adminUsername: 'ikko'
    adminPassword: kv.getSecret(secretName)
    subnetId: spoke120::defaultsubnet.id
    vmName: vm13Name
  }
}

ExpressRoute Gateway を作る Bicep ファイルはこちらです。
VPN Gateway と被っている部分が多いため、共通化できないかは考えます。

ergw_single.bicep
param location string = 'eastasia'
param gatewayName string = 'ergw01'
param vnetName string

resource vnet01 'Microsoft.Network/virtualNetworks@2022-01-01' existing =  {
  name: vnetName

  resource GatewaySubnet 'subnets' existing = {
    name: 'GatewaySubnet'
  }
}

resource pip01 'Microsoft.Network/publicIPAddresses@2021-08-01' = {
  name: 'pip-${gatewayName}'
  location: location
  sku: {
    name: 'Standard'
    tier: 'Regional'
  }
  zones: [
    '1'
    '2'
    '3'
  ]
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource ergw01 'Microsoft.Network/virtualNetworkGateways@2021-08-01' = {
  name: gatewayName
  location: location
  properties: {
    sku: {
      name: 'ErGw1AZ'
      tier: 'ErGw1AZ'
    }
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          publicIPAddress: { id: pip01.id }
          subnet: { id: vnet01::GatewaySubnet.id }
        }
      }
    ]
    gatewayType: 'ExpressRoute'
  }
}

output ergwName string = ergw01.name
output publicIpName string = pip01.name

connection-ergw.bicep はこのようになっていて、ergw.location を利用したいがためにまた helper を利用しています。
まるでイケてないので今後の Bicep の更新でいい感じにできないか模索します。

connection-ergw.bicep
param cctName string
param cctRGName string
param ergwName string
param connectionName string = ''

var _connectionName = !empty(connectionName) ? connectionName : 'conn-${cctName}-${ergwName}'

resource ergw 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = {
  name: ergwName
}

module connection 'connection-ergw-helper.bicep' = {
  name: '${_connectionName}-helper'
  params: {
    cctName: cctName
    cctRGName: cctRGName
    connectionName: _connectionName
    ergwName: ergwName
    location: ergw.location
  }
}
connection-ergw-helper.bicep
param cctName string
param cctRGName string
param ergwName string
param connectionName string
param location string

resource cct 'Microsoft.Network/expressRouteCircuits@2022-01-01' existing = {
  name: cctName
  scope: resourceGroup(cctRGName)
}

resource ergw 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = {
  name: ergwName
}

resource conn 'Microsoft.Network/connections@2022-01-01' = {
  name: connectionName
  location: location
  properties: {
    connectionType: 'ExpressRoute'
    virtualNetworkGateway1: {
      id: ergw.id
    }
    peer: {
      id: cct.id
    }
  }
}

Discussion

ログインするとコメントできます