🤔

Hub - Spoke で Spoke 間はなぜつながらないのか

2022/07/16に公開

Hub - Spoke の Spoke 間はそのままではつながらない

よく見る Hub - Spoke アーキテクチャにおいて、Spoke 間はそのままでは通信ができません。
なんでか、というとルーティングがないから、に尽きるんですが、一応紹介しておきます。

effective routes for NIC

これは Spoke に置いた仮想マシンの Network Interface から 有効なルート と進んだ画面です。
10.0.0.0/16 が Hub で、10.10.0.0/16 は所属している VNet 自身です。
ですので、もちろんこれらの IP アドレス帯に対しては通信可能なのですが、Spoke02 にあたる 10.20.0.0/16 がありません。
前提として、Hub - Spoke の間の VNet Peering では リモート仮想ネットワークのゲートウェイまたはルート サーバーを使用する は選択された状態です。
そのため Hub には VPN Gateway がおいてあります。
「リモートゲートウェイ」という用語からするとゲートウェイが Hub に向いていれば折り返しの通信ができそうなもんですが、そういう動きにはなっていません。

Spoke 間の通信を実現するためには

Spoke 間の通信を実現するためにはいくつかの方法がありますが、なんらかパケットをフォワーディングしてくれるインスタンスを Hub に置き、それに向かって UDR (User Defined Route、ユーザ定義ルート)を書く方法が定番です。
このフォワーディングしてくれる何かの例は以下のとおりです。

  • Azure Firewall
    • Hub - Spoke アーキテクチャの紹介ではよく Hub に Azure Firewall がおかれているのでこれに向けて UDR を書く
    • Azure Firewall で Spoke 間の通信も制御できるのでよい
  • NVA
    • Cisco や Juniper、Fortinet などの NVA を配置して、上記同様に UDR を書く
    • Azure Firewall より機能的に優れている面も多いが、どう冗長化を実現するかでベンダごとに少し苦労する

あまりおススメはできないが、一応ということで以下の選択肢も書いておきます。

  • Azure VM
    • あまりおススメできるものではないかもしれないですが、なんでもない Azure VM を PC ルータとして設定する
    • 原理上何でもできるかもしれないですが、まぁ運用負荷も高いのであまり見ない
  • VNet Gateway
    • サポートされなかったかもしれないので書くのが少しはばかられるのですが、一応 VNet Gateway を next-hop とする UDR を書けばうごくっちゃあ動きます
    • あまりおススメはしたくない

記載する UDR の経路としては、Spoke01 から見た Spoke02 を向けるのが最低限の設定例ですが、0.0.0.0/0 を向けてしまうのも構成によっては可能です。

そのほかの実現例としては、VNet Peering ではなく、VPN Gateway をそれぞれの VNet におき、VPN + BGP で接続する、という方法ですが、VNet 間の上限帯域幅が VPN Gateway の SKU に依存するため、あまりおススメはしていないです。

また、UDR だと Spoke01 からみた Spoke02 の IP アドレス空間が増えたときに UDR を修正する必要があるのでいやだなぁ、という時には、ARS (Azure Route Server) を使うこともできます。
ARS を使う場合には、1) ARS 自体を Spoke のそれぞれにデプロイすること、2) Hub に BGP をしゃべれるインスタンスを用意し、各 Spoke へのルーティングを BGP で流し込む、ということになり、これはこれでややハードルが高くなります。
技術的なハードルのほか、ARS のコストがやや高いこと、また ARS 自体がやや新しいこともあり構成としてあまり見たことはないです。
コストは 価格 - Route Server から見られますが、2022年07月 時点だと 61.320円/時間 (44763.6円/月) となっています。
ちなみに ARS は上記でいうところの なんらかパケットをフォワーディングしてくれるインスタンス にはなってくれません。[1]


久々にちゃんと Bicep で検証環境を作りました。
(どちらかというとこちらがメインの目的だったりする)
ここに記録がてら張っておきます。
このままだと UDR がないので Spoke 間は通信ができないねぇ、という状況までです。

これがメインの Bicep ファイルです。
もうちょっと外に出したい気もしますが今のところこれくらいで。
lib にいくつかの Bicep ファイルがおいてあってそれも使ってます。
subnet を後で参照するため existing キーワードを使って中間オブジェクトみたいなものを使ってますが、正確に言うと existing なものではなくて順序的に VNet のほうが先に作られるので一応動く、程度のあまりいい方法ではないと思っていて、そのうち Bicep 側の新機能が出てきていい感じにできるはずです。[2]

もしこれらのファイルをそのまま使うのであれば、adminUsernamexxxxxxxxxxxx から適当に変更し、実行時に adminPassword を与えてあげてください。
実行方法は az deployment group create -g <resource group name> -f main.bicep です。

main.bicep
main.bicep
param location string = 'eastasia'

resource vnet_hub 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet-hub01'
  location: location
  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_hub01 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = {
  parent: vnet_hub
  name: 'default'
}

resource bastionsubnet_hub01 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = {
  parent: vnet_hub
  name: 'AzureBastionSubnet'
}

resource gatewaysubnet_hub01 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = {
  parent: vnet_hub
  name: 'GatewaySubnet'
}

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

resource defaultsubnet_spoke01 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = {
  parent: vnet_spoke01
  name: 'default'
}

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

resource defaultsubnet_spoke02 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = {
  parent: vnet_spoke02
  name: 'default'
}

module peering_hub_spoke01 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub-spoke01'
  params: {
    vnet01Name: vnet_hub.name
    vnet02Name: vnet_spoke01.name
    useRemoteGateways: true
    remoteGatewayName: vpngw01.name
  }
}

module peering_hub_spoke02 '../lib/vnet-peering.bicep' = {
  name: 'peering-hub-spoke02'
  params: {
    vnet01Name: vnet_hub.name
    vnet02Name: vnet_spoke02.name
    useRemoteGateways: true
    remoteGatewayName: vpngw01.name
  }
}

module vpngw01 '../lib/vpngw_act-act.bicep' = {
  name: 'vpngw01'
  params: {
    location: location
    gatewaySubnetId: gatewaysubnet_hub01.id
  }
}

module bast01 '../lib/bastion.bicep' = {
  name: 'bast01'
  params: {
    location: location
    bastionSubnetId: bastionsubnet_hub01.id
  }
}

param hubVmName string = 'vm-hub01'
@secure()
param adminPassword string

module vm_hub '../lib/ws2019.bicep' = {
  name: hubVmName
  params: {
    location: location
    subnetId: defaultsubnet_hub01.id
    vmName: hubVmName
    adminPassword: adminPassword
  }
}

param spoke01VmName string = 'vm-spoke01'
module vm_spoke01 '../lib/ws2019.bicep' = {
  name: spoke01VmName
  params: {
    location: location
    subnetId: defaultsubnet_spoke01.id
    vmName: spoke01VmName
    adminPassword: adminPassword
  }
}

param spoke02VmName string = 'vm-spoke02'
module vm_spoke02 '../lib/ws2019.bicep' = {
  name: spoke02VmName
  params: {
    location: location
    subnetId: defaultsubnet_spoke02.id
    vmName: spoke02VmName
    adminPassword: adminPassword
  }
}

ここからが lib フォルダ配下にある Bicep ファイルたちです。
典型的なデプロイは適当にまとめてあります。
VNet Peering、なんとなく Act-Act の VPN Gateway、Bastion、smalldisk な Windows Server 2019 です。
なるべくデフォルト値を採用し、最小記述量にとどめた予定です。

vnet-peering.bicep
vnet-peering.bicep
param vnet01Name string
param vnet02Name string
param useRemoteGateways bool = false
param remoteGatewayName string = ''

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

resource vnet02 'Microsoft.Network/virtualNetworks@2022-01-01' existing = {
  name: vnet02Name
}

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

var vnet01Suffix = replace(vnet01Name, 'vnet-', '')
var vnet02Suffix = replace(vnet02Name, 'vnet-', '')

resource peering_hub_spoke01 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = {
  parent: vnet01
  name: '${vnet01Suffix}-to-${vnet02Suffix}'
  properties: {
    allowForwardedTraffic: true
    allowGatewayTransit: true
    allowVirtualNetworkAccess: true
    remoteVirtualNetwork: { id: vnet02.id }
  }
}

resource peering_spoke01_hub 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = {
  parent: vnet02
  name: '${vnet02Suffix}-to-${vnet01Suffix}'
  properties: {
    allowForwardedTraffic: true
    allowVirtualNetworkAccess: true
    remoteVirtualNetwork: { id: vnet01.id }
    useRemoteGateways: useRemoteGateways
  }
  dependsOn: useRemoteGateways ? [ gw ] : []
}
vpngw_act-act.bicep
vpngw_act-act.bicep
param location string = 'eastasia'
param gatewaySubnetId string

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

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

resource vpngw01 'Microsoft.Network/virtualNetworkGateways@2021-08-01' = {
  name: 'vpngw01'
  location: location
  properties: {
    sku: {
      name: 'VpnGw1AZ'
      tier: 'VpnGw1AZ'
    }
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          publicIPAddress: { id: pip01.id }
          subnet: { id: gatewaySubnetId }
        }
      }
      {
        name: 'ipconfig2'
        properties: {
          publicIPAddress: { id: pip02.id }
          subnet: { id: gatewaySubnetId }
        }
      }
    ]
    gatewayType: 'Vpn'
    vpnType: 'RouteBased'
    activeActive: true
  }
}
bastion.bicep
bastion.bicep
param location string = 'eastasia'
param bastionSubnetId string

resource pip 'Microsoft.Network/publicIPAddresses@2022-01-01' = {
  name: 'pip-bast01'
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource bast01 'Microsoft.Network/bastionHosts@2022-01-01' = {
  name: 'bast01'
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    scaleUnits: 2
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          subnet: { id: bastionSubnetId }
          publicIPAddress: { id: pip.id }
        }
      }
    ]
  }
}
ws2019.bicep
ws2019.bicep
param location string = 'eastasia'
param subnetId string
param vmNameSuffix string
param adminUsername string = 'xxxxxxxxxxxx'
@secure()
param adminPassword string

resource nic 'Microsoft.Network/networkInterfaces@2022-01-01' = {
  name: 'nic-${vmNameSuffix}'
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          subnet: {
            id: subnetId
          }
          privateIPAllocationMethod: 'Dynamic'
        }
      }
    ]
  }
}

resource vm 'Microsoft.Compute/virtualMachines@2022-03-01' = {
  name: 'vm-${vmNameSuffix}'
  location: location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B2ms'
    }
    storageProfile: {
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'StandardSSD_LRS'
        }
        deleteOption: 'Delete'
      }
      imageReference: {
        publisher: 'MicrosoftWindowsServer'
        offer: 'WindowsServer'
        sku: '2019-datacenter-smalldisk-g2'
        version: 'latest'
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: nic.id
          properties: {
            deleteOption: 'Delete'
          }
        }
      ]
    }
    osProfile: {
      computerName: vmNameSuffix
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    diagnosticsProfile: {
      bootDiagnostics: {
        enabled: true
      }
    }
  }
}

resource shutdown 'Microsoft.DevTestLab/schedules@2018-09-15' = {
  name: 'shutdown-computevm-vm-${vmNameSuffix}'
  location: location
  properties: {
    status: 'Enabled'
    taskType: 'ComputeVmShutdownTask'
    dailyRecurrence: {
      time: '00:00'
    }
    timeZoneId: 'Tokyo Standard Time'
    targetResourceId: vm.id
    notificationSettings: {
      status: 'Disabled'
    }
  }
}

脚注
  1. https://learn.microsoft.com/azure/route-server/route-server-faq#can-i-peer-two-route-servers-in-two-peered-virtual-networks-and-enable-the-nvas-connected-to-the-route-servers-to-talk-to-each-other ↩︎

  2. https://github.com/Azure/bicep/discussions/7574 ↩︎

Microsoft (有志)

Discussion