ハブアンドスポークな仮想ネットワークと Virtual WAN な仮想ネットワークを接続する

azure
Published: 2025-08-14

はじめに

現在の Azure には従来のハブアンドスポークな仮想ネットワークと、Virtual WAN で構成された仮想ネットワークの2つが存在しています。とすると、従来のハブアンドスポークな仮想ネットワークに接続しているスポークな仮想ネットワークと、Virtual WAN に接続しているスポークネットワーク間で通信したくなることもあるでしょう。

通信したい2つのスポーク仮想ネットワーク

両者のスポークが1つずつであれば、スポーク同士を直接 VNET ピアリングすればよいです。ですが、ハブアンドスポーク構成と Virtual WAN を採用しているという事は、それぞれのネットワークには多くのスポーク仮想ネットワークが存在しているでしょう。通信したいスポーク同士を都度 VNET ピアリングで接続するのは骨が折れます。やはりこここは2つのネットワークを接続してしまいたいところです。

とはいえ、やったことがないので実際に試してみました。実際に環境を作成する際に利用した Bicep は以下の通りです。細かい設定値はテンプレートを参照ください。

Bicepテンプレート

param adminUsername string
param adminPassword string

resource hub 'Microsoft.Network/virtualNetworks@2024-05-01' = {
  name: 'hub'
  location: resourceGroup().location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'GatewaySubnet'
        properties: {
          addressPrefix: '10.0.0.0/24'
          routeTable: {
            id: hubGwRt.id
          }
        }
      }
      {
        name: 'AzureFirewallSubnet'
        properties: {
          addressPrefix: '10.0.1.0/25'
          routeTable: {
            id: hubFwRt.id
          }
        }
      }
      {
        name: 'AzureFirewallManagementSubnet'
        properties: {
          addressPrefix: '10.0.1.128/25'
        }
      }
    ]
  }
}

resource hubFwRt 'Microsoft.Network/routeTables@2024-07-01' = {
  name: 'hubRouteTable'
  location: resourceGroup().location
  properties: {
    routes: [
      {
        name: 'default'
        properties: {
          addressPrefix: '0.0.0.0/0'
          nextHopType: 'Internet'
        }
      }
    ]
  }
}

resource hubGwRt 'Microsoft.Network/routeTables@2024-07-01' = {
  name: 'hubGwRouteTable'
  location: resourceGroup().location
  properties: {
    routes: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.1.0.0/24'
          nextHopType: 'VirtualAppliance'
          nextHopIpAddress: '10.0.1.4'
        }
      }
    ]
  }
}

resource hubErGw 'Microsoft.Network/virtualNetworkGateways@2024-07-01' = {
  name: 'hubErGw'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'Standard'
      tier: 'Standard'
    }
    gatewayType: 'ExpressRoute'
    ipConfigurations: [
      {
        name: 'hubErGwIpConfig'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${hub.id}/subnets/GatewaySubnet'
          }
        }
      }
    ]
  }
  dependsOn: [
    hubFirewall
  ]
}

resource hubVpnGwPip1 'Microsoft.Network/publicIPAddresses@2024-07-01' = {
  name: 'hubVpnGwPip1'
  location: resourceGroup().location
  sku: {
    name: 'Standard'
    tier: 'Regional'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource hubVpnGwPip2 'Microsoft.Network/publicIPAddresses@2024-07-01' = {
  name: 'hubVpnGwPip2'
  location: resourceGroup().location
  sku: {
    name: 'Standard'
    tier: 'Regional'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource hubVpnGw 'Microsoft.Network/virtualNetworkGateways@2024-07-01' = {
  name: 'hubVpnGw'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'VpnGw1'
      tier: 'VpnGw1'
    }
    enableBgp: false
    activeActive: true
    gatewayType: 'Vpn'
    vpnType: 'RouteBased'
    ipConfigurations: [
      {
        name: 'hubVpnGwIpConfig1'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${hub.id}/subnets/GatewaySubnet'
          }
          publicIPAddress: {
            id: hubVpnGwPip1.id
          }
        }
      }
      {
        name: 'hubVpnGwIpConfig2'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${hub.id}/subnets/GatewaySubnet'
          }
          publicIPAddress: {
            id: hubVpnGwPip2.id
          }
        }
      }
    ]
  }
  dependsOn: [
    hubFirewall
  ]
}

resource hubVpnLgw1 'Microsoft.Network/localNetworkGateways@2024-07-01' = {
  name: 'hubVpnLgw1'
  location: resourceGroup().location
  properties: {
    localNetworkAddressSpace: {
      addressPrefixes: [
        '192.168.0.0/16'
      ]
    }
    gatewayIpAddress: vwanVpnGw.properties.ipConfigurations[0].publicIpAddress
  }
}

resource hubVpnLgw2 'Microsoft.Network/localNetworkGateways@2024-07-01' = {
  name: 'hubVpnLgw2'
  location: resourceGroup().location
  properties: {
    localNetworkAddressSpace: {
      addressPrefixes: [
        '192.168.0.0/16'
      ]
    }
    gatewayIpAddress: vwanVpnGw.properties.ipConfigurations[1].publicIpAddress
  }
}

resource hubVpnGwConn1 'Microsoft.Network/connections@2024-07-01' = {
  name: 'hubVpnGwConn1'
  location: resourceGroup().location
  properties: {
    connectionType: 'IPsec'
    localNetworkGateway2: {
      id: hubVpnLgw1.id
    }
    virtualNetworkGateway1: {
      id: hubVpnGw.id
    }
    sharedKey: adminPassword
    enableBgp: false
    connectionProtocol: 'IKEv2'
  }
}

resource hubVpnGwConn2 'Microsoft.Network/connections@2024-07-01' = {
  name: 'hubVpnGwConn2'
  location: resourceGroup().location
  properties: {
    connectionType: 'IPsec'
    localNetworkGateway2: {
      id: hubVpnLgw2.id
    }
    virtualNetworkGateway1: {
      id: hubVpnGw.id
    }
    sharedKey: adminPassword
    enableBgp: false
    connectionProtocol: 'IKEv2'
  }
}

resource spoke 'Microsoft.Network/virtualNetworks@2024-05-01' = {
  name: 'spoke'
  location: resourceGroup().location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.1.0.0/24'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '10.1.0.0/25'
          routeTable: {
            id: spokeRt.id
          }
        }
      }
    ]
  }
}

resource spokeVmNic 'Microsoft.Network/networkInterfaces@2024-07-01' = {
  name: 'spokeVmNic'
  location: resourceGroup().location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${spoke.id}/subnets/default'
          }
        }
      }
    ]
  }
}

resource spokeVm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
  name: 'spokeVm'
  location: resourceGroup().location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B1s'
    }
    storageProfile: {
      imageReference: {
        publisher: 'Canonical'
        offer: '0001-com-ubuntu-server-jammy'
        sku: '22_04-lts-gen2'
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'StandardSSD_LRS'
        }
      }
    }
    osProfile: {
      computerName: 'spokeVm'
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: spokeVmNic.id
        }
      ]
    }
    diagnosticsProfile: {
      bootDiagnostics: {
        enabled: true
      }
    }
  }
}

resource hub2spokePeering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-07-01' = {
  name: 'hub-to-spoke'
  parent: hub
  properties: {
    allowVirtualNetworkAccess: true
    allowForwardedTraffic: true
    allowGatewayTransit: true
    useRemoteGateways: false
    remoteVirtualNetwork: {
      id: spoke.id
    }
  }
  dependsOn: [
    hubErGw
    hubVpnGw
  ]
}

resource spoke2hubPeering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-07-01' = {
  name: 'spoke-to-hub'
  parent: spoke
  properties: {
    allowVirtualNetworkAccess: true
    allowForwardedTraffic: true
    allowGatewayTransit: false
    useRemoteGateways: true
    remoteVirtualNetwork: {
      id: hub.id
    }
  }
  dependsOn: [
    hubErGw
    hubVpnGw
  ]
}

resource spokeRt 'Microsoft.Network/routeTables@2024-07-01' = {
  name: 'spokeRouteTable'
  location: resourceGroup().location
  properties: {
    disableBgpRoutePropagation: true
    routes: [
      {
        name: 'default'
        properties: {
          addressPrefix: '0.0.0.0/0'
          nextHopType: 'VirtualAppliance'
          nextHopIpAddress: hubFirewall.properties.ipConfigurations[0].properties.privateIPAddress
        }
      }
    ]
  }
}

resource fwpolicy 'Microsoft.Network/firewallPolicies@2024-07-01' = {
  name: 'fwPolicy'
  location: resourceGroup().location
  properties: {
    sku: {
      tier: 'Standard'
    }
    threatIntelMode: 'Alert'
  }
}

resource fwPolicyNetworkRuleCollection 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2024-07-01' = {
  name: 'networkRuleCollection'
  parent: fwpolicy
  properties: {
    priority: 100
    ruleCollections: [
      {
        ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
        name: 'networkRuleCollection'
        priority: 100
        action: {
          type: 'Allow'
        }
        rules: [
          {
            ruleType: 'NetworkRule'
            ipProtocols: [
              'Any'
            ]
            name: 'allowAll'
            description: 'Allow all traffic'
            sourceAddresses: [
              '*'
            ]
            destinationAddresses: [
              '*'
            ]
            destinationPorts: [
              '*'
            ]
          }
        ]
      }
    ]
  }
}

resource hubFwPip 'Microsoft.Network/publicIPAddresses@2024-07-01' = {
  name: 'hubFwPip'
  location: resourceGroup().location
  sku: {
    name: 'Standard'
    tier: 'Regional'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource hubFwMgmtPip 'Microsoft.Network/publicIPAddresses@2024-07-01' = {
  name: 'hubFwMgmtPip'
  location: resourceGroup().location
  sku: {
    name: 'Standard'
    tier: 'Regional'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource hubFirewall 'Microsoft.Network/azureFirewalls@2024-05-01' = {
  name: 'hubFirewall'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'AZFW_VNet'
      tier: 'Standard'
    }
    firewallPolicy: {
      id: fwpolicy.id
    }
    ipConfigurations: [
      {
        name: 'firewallIpConfig'
        properties: {
          publicIPAddress: {
            id: hubFwPip.id
          }
          subnet: {
            id: '${hub.id}/subnets/AzureFirewallSubnet'
          }
        }
      }
    ]
    managementIpConfiguration: {
      name: 'managementIpConfig'
      properties: {
        subnet: {
          id: '${hub.id}/subnets/AzureFirewallManagementSubnet'
        }
        publicIPAddress: {
          id: hubFwMgmtPip.id
        }
      }
    }
  }
}

// vwan

resource vwanSpoke 'Microsoft.Network/virtualNetworks@2024-05-01' = {
  name: 'vwanSpoke'
  location: resourceGroup().location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '192.168.1.0/24'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '192.168.1.0/24'
        }
      }
    ]
  }
}

resource vwanSpokeVmNic 'Microsoft.Network/networkInterfaces@2024-07-01' = {
  name: 'vwanSpokeVmNic'
  location: resourceGroup().location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: '${vwanSpoke.id}/subnets/default'
          }
        }
      }
    ]
  }
}

resource vwanSpokeVm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
  name: 'vwanSpokeVm'
  location: resourceGroup().location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B1s'
    }
    storageProfile: {
      imageReference: {
        publisher: 'Canonical'
        offer: '0001-com-ubuntu-server-jammy'
        sku: '22_04-lts-gen2'
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'StandardSSD_LRS'
        }
      }
    }
    osProfile: {
      computerName: 'vwanSpokeVm'
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: vwanSpokeVmNic.id
        }
      ]
    }
    diagnosticsProfile: {
      bootDiagnostics: {
        enabled: true
      }
    }
  }
}

resource vwan 'Microsoft.Network/virtualWans@2024-07-01' = {
  name: 'vwan'
  location: resourceGroup().location
  properties: {
    allowBranchToBranchTraffic: true
    disableVpnEncryption: false
    type: 'Standard'
  }
}

resource vhub 'Microsoft.Network/virtualHubs@2024-07-01' = {
  name: 'vhub'
  location: resourceGroup().location
  properties: {
    virtualWan: {
      id: vwan.id
    }
    virtualRouterAutoScaleConfiguration: {
      minCapacity: 2
    }
    addressPrefix: '192.168.0.0/24'
  }
}

resource vwanErGw 'Microsoft.Network/expressRouteGateways@2024-07-01' = {
  name: 'vwanErGw'
  location: resourceGroup().location
  properties: {
    virtualHub: {
      id: vhub.id
    }
    autoScaleConfiguration: {
      bounds: {
        min: 1
      }
    }
  }
}

resource vwanVpnGw 'Microsoft.Network/vpnGateways@2024-07-01' = {
  name: 'vwanVpnGw'
  location: resourceGroup().location
  properties: {
    virtualHub: {
      id: vhub.id
    }
    bgpSettings: {
      asn: 65515
    }
    vpnGatewayScaleUnit: 1
  }
  dependsOn: [
    vwanErGw
  ]
}

resource vwanVpnSite1 'Microsoft.Network/vpnSites@2024-07-01' = {
  name: 'hubVpnGw1'
  location: resourceGroup().location
  properties: {
    deviceProperties: {
      deviceModel: 'Azure VPN Gateway'
      deviceVendor: 'Microsoft'
      linkSpeedInMbps: 100
    }
    virtualWan: {
      id: vwan.id
    }
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
        '10.1.0.0/16'
      ]
    }
    vpnSiteLinks: [
      {
        name: 'hubVpnGw1Link'
        properties: {
          ipAddress: hubVpnGwPip1.properties.ipAddress
          linkProperties: {
            linkProviderName: 'Microsoft'
            linkSpeedInMbps: 100
          }
        }
      }
    ]
  }
}

resource vwanVpnSite2 'Microsoft.Network/vpnSites@2024-07-01' = {
  name: 'hubVpnGw2'
  location: resourceGroup().location
  properties: {
    deviceProperties: {
      deviceModel: 'Azure VPN Gateway'
      deviceVendor: 'Microsoft'
      linkSpeedInMbps: 100
    }
    virtualWan: {
      id: vwan.id
    }
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
        '10.1.0.0/16'
      ]
    }
    vpnSiteLinks: [
      {
        name: 'hubVpnGw2Link'
        properties: {
          ipAddress: hubVpnGwPip2.properties.ipAddress
          linkProperties: {
            linkProviderName: 'Microsoft'
            linkSpeedInMbps: 100
          }
        }
      }
    ]
  }
}

resource vwanFirewall 'Microsoft.Network/azureFirewalls@2024-05-01' = {
  name: 'vwanFirewall'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'AZFW_Hub'
      tier: 'Standard'
    }
    firewallPolicy: {
      id: fwpolicy.id
    }
    hubIPAddresses: {
      publicIPs: {
        count: 1
      }
    }
    virtualHub: {
      id: vhub.id
    }
  }
  dependsOn: [
    vwanErGw
    vwanVpnGw
  ]
}

resource vhub2vwanSpokeConnection 'Microsoft.Network/virtualHubs/hubVirtualNetworkConnections@2024-07-01' = {
  name: 'vhub-to-vwanSpoke'
  parent: vhub
  properties: {
    remoteVirtualNetwork: {
      id: vwanSpoke.id
    }
    enableInternetSecurity: true
  }
  dependsOn: [
    vwanErGw
  ]
}

resource vhubRoutingIntent 'Microsoft.Network/virtualHubs/routingIntent@2024-07-01' = {
  name: 'vhubRoutingIntent'
  parent: vhub
  properties: {
    routingPolicies: [
      {
        name: 'PublicTraffic'
        destinations: [
          'Internet'
        ]
        nextHop: vwanFirewall.id
      }
      {
        name: 'PrivateTraffic'
        destinations: [
          'PrivateTraffic'
        ]
        nextHop: vwanFirewall.id
      }
    ]
  }
}

組めない構成

思いつく最初の方法は、ハブ仮想ネットワークを仮想ハブに接続する方法です。ですが、この構成は組めませんでした。

できない構成

仮想仮想ネットワークゲートウェイが存在する仮想ネットワークと仮想ハブを接続しようとすると次のエラーがでます。

ポータル上のエラー

仮想ハブに仮想ネットワーク接続を作る際に allowRemoteVnetToUseHubVnetGateways を false にできればよさそうですが、PowerShell のコマンドには該当の設定を false にするオプションが見当たりません。「API 直叩きならなんとかなるか?」と以下の様に試してみたものの false で設定してもエラーになってしまいました。

$hub = Get-AzVirtualNetwork -ResourceGroupName test0808vwan -Name hub
$rt1 = Get-AzVHubRouteTable -ResourceGroupName test0808vwan -VirtualHubName vhub -Name "defaultRouteTable"

$body = @"
{
  "properties": {
    "remoteVirtualNetwork": {
      "id": "$($hub.id)"
    },
    "allowRemoteVnetToUseHubVnetGateways": false,
    "allowHubToRemoteVnetTransit": true,
    "enableInternetSecurity": false,
    "routingConfiguration": {
      "associatedRouteTable": {
        "id": "$($rt1.Id)"
      },
      "propagatedRouteTables": {
        "labels": [
          "default"
        ],
        "ids": [
          {
            "id": "$($rt1.Id)"
          }
        ]
      },
      "vnetRoutes": {
        "staticRoutesConfig": {
          "vnetLocalRouteOverrideCriteria": "Equal"
        },
        "staticRoutes": [
          {
            "name": "route1",
            "addressPrefixes": [
            "10.1.0.0/24",
            ],
            "nextHopIpAddress": "10.0.1.4"
          }
        ]
      }
    }
  }
}

StatusCode : 400
Content    : {
   "error": {
     "code": "RemoteVnetAlreadyHasGateways",
     "message": "HubVirtualNetworkConnection /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0808vwan/providers/Microsoft.Network/virtualHubs/vhub/hubVirtualNetworkConnections/vhub-to-hub cannot have AllowRemoteVnetToUseHubVnetGateways flag set to true, because remote virtual network /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0808vwan/providers/Microsoft.Network/virtualNetworks/hub already has a gateway /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/TEST0808VWAN/providers/Microsoft.Network/virtualNetworkGateways/HUBERGW configured. Remove gateway from the remote virtual network and retry.",
     "details": []
   }
"@

API のプロパティをよく見てみると「allowRemoteVnetToUseHubVnetGateways」は廃止になっていました。万策尽きた感じです。

HubVirtualNetworkConnectionProperties

代替案

ハブ同士を VNET ピアリングで直結できないのであれば、VPN Gateway を利用して S2S VPN で接続します。互いに相手を一つの VPN 拠点として扱うイメージです。

できる構成

「普通のハブに S2S VPN で拠点を接続する」作業と「仮想ハブに S2S VPN で拠点を接続する」作業をやるだけですので、特殊な設定は不要です。注意したほうが良い点は以下の3点です

  1. 静的ルーティングを使う
  2. VPN Gateway を Active/Active にする
  3. GatewaySubnet を /27 以上にする

静的ルーティングを使う

S2S VPN を設計するときの悩みが、BGP を利用するかどうかです。今回の構成では静的ルートを採用しました。というのも、BGP では上手く動作しませんでした。なぜだか 普通のハブの VPN Gateway が、仮想ハブから学習した経路を仮想ネットワークに広報してくれず…

また、設計的にも静的ルートの方が良い点があります。BGP を利用するには普通のハブの VPN Gateway の AS 番号を65515以外にものにする必要があります。Virtual WAN 側の VPN Gateway の AS 番号が65515で固定なためです。普通のハブの VPN Gateway の AS 番号を65515以外のものにしてしまうと、Route Server が使えなかったり、ExpressRoute とのトランジットルートができなくなったりと「できないこと」が生じます。

ExpressRoute と VPN の間のトランジット ルーティングの場合は、Azure VPN Gateway の ASN が 65515 に設定されている必要があります。

https://learn.microsoft.com/ja-jp/azure/expressroute/how-to-configure-coexisting-gateway-portal

Azure VPN ゲートウェイは、アクティブ-アクティブ モードで構成し、ASN が 65515 に設定されている必要があります。 ルート サーバーと通信するために VPN ゲートウェイで BGP を有効にする必要はありません。

https://learn.microsoft.com/ja-jp/azure/route-server/expressroute-vpn-support

普通のハブの VPN Gateway の構成

普通のハブの VPN Gateway は Active/Active にする必要があります。

アクティブ/アクティブ モード - 必ずアクティブ/アクティブ モードを選択してください。 この設定は、VPN Gateway 仮想ネットワーク ゲートウェイが Virtual WAN に接続するために必要です。

https://learn.microsoft.com/ja-jp/azure/virtual-wan/connect-virtual-network-gateway-vwan

そのため、普通のハブの VPN Gateway が持つ2つのパブリック IP アドレスを、仮想ハブの VPN サイトとして登録することになります。

resource vwanVpnSite1 'Microsoft.Network/vpnSites@2024-07-01' = {
  name: 'hubVpnGw1'
  location: resourceGroup().location
  properties: {
    deviceProperties: {
      deviceModel: 'Azure VPN Gateway'
      deviceVendor: 'Microsoft'
      linkSpeedInMbps: 100
    }
    virtualWan: {
      id: vwan.id
    }
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
        '10.1.0.0/16'
      ]
    }
    vpnSiteLinks: [
      {
        name: 'hubVpnGw1Link'
        properties: {
          ipAddress: hubVpnGwPip1.properties.ipAddress
          linkProperties: {
            linkProviderName: 'Microsoft'
            linkSpeedInMbps: 100
          }
        }
      }
    ]
  }
}

resource vwanVpnSite2 'Microsoft.Network/vpnSites@2024-07-01' = {
  name: 'hubVpnGw2'
  location: resourceGroup().location
  properties: {
    deviceProperties: {
      deviceModel: 'Azure VPN Gateway'
      deviceVendor: 'Microsoft'
      linkSpeedInMbps: 100
    }
    virtualWan: {
      id: vwan.id
    }
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
        '10.1.0.0/16'
      ]
    }
    vpnSiteLinks: [
      {
        name: 'hubVpnGw2Link'
        properties: {
          ipAddress: hubVpnGwPip2.properties.ipAddress
          linkProperties: {
            linkProviderName: 'Microsoft'
            linkSpeedInMbps: 100
          }
        }
      }
    ]
  }
}

GatewaySubnet を /27 以上にする

普通のハブには ExpressRoute Gateway が存在するケースが多いと思います。その場合、普通のハブに VPN Gateway を追加するためには、GatewaySubnet のサイズが /27 またはより短いプレフィックスである必要があります。

ゲートウェイ サブネットは /27 またはそれより短いプレフィックス (/26、/25 など) でなければなりません。そうでないと、ExpressRoute 仮想ネットワーク ゲートウェイを追加したときに、エラー メッセージが表示されます。

https://learn.microsoft.com/ja-jp/azure/expressroute/expressroute-howto-coexist-resource-manager?tabs=new-virtual-network#limits-and-limitations

動作確認

普通のハブのスポーク仮想ネットワーク上に起動した仮想マシンの有効なルートは次の通りです。通信を Azure Firewall 経由にするために、BGP 伝搬を無効にしたルートテーブルを適用しているので、VPN Gateway の先に存在する仮想ハブ側のアドレス帯は有効なルートに乗ってきません。

略> $rts = Get-AzEffectiveRouteTable -ResourceGroupName "test0813vwan3" -NetworkInterfaceName "spokeVmNic"
略> $rts | ft AddressPrefix, NextHopIpAddress, NextHopType ,State

AddressPrefix NextHopIpAddress NextHopType      State
------------- ---------------- -----------      -----
{10.1.0.0/24} {}               VnetLocal        Active
{10.0.0.0/16} {}               VNetPeering      Active
{0.0.0.0/0}   {}               Internet         Invalid
{0.0.0.0/0}   {10.0.1.4}       VirtualAppliance Active

仮想ハブ側のスポーク仮想ネットワーク上に起動した仮想マシンの有効なルートは次の通りです。ルーティングインテントを有効化しているので、0.0.0.0/0と10.0.0.0/8、172.16.0.0/12、192.168.0.0/16の4つのアドレス帯が、Azure Firewall の IP アドレスである 192.168.0.132に向いています。

> $rts = Get-AzEffectiveRouteTable -ResourceGroupName "test0813vwan3" -NetworkInterfaceName "vwanspokeVmNic"
> $rts | ft AddressPrefix, NextHopIpAddress, NextHopType ,State

AddressPrefix    NextHopIpAddress NextHopType           State
-------------    ---------------- -----------           -----
{192.168.1.0/24} {}               VnetLocal             Active
{192.168.0.0/24} {}               VNetPeering           Active
{192.168.0.0/16} {192.168.0.132}  VirtualNetworkGateway Active
{0.0.0.0/0}      {192.168.0.132}  VirtualNetworkGateway Active
{10.0.0.0/8}     {192.168.0.132}  VirtualNetworkGateway Active
{172.16.0.0/12}  {192.168.0.132}  VirtualNetworkGateway Active

仮想ハブの Default のルートテーブルは次の通りです。ルーティングインテントを有効化しているので、4つのアドレス帯宛ての通信が Azure Firewall に吸い込まれていることが分かります。

> Get-AzVhubEffectiveRoutes -virtualWanResourceType "RouteTable" -resourceId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0813vwan3/providers/Microsoft.Network/virtualHubs/vhub/hubRouteTables/defaultRouteTable" -vhubId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0813vwan3/providers/Microsoft.Network/virtualHubs/vhub"

addressPrefixes : {0.0.0.0/0}
nextHops        : azureFirewalls/vwanFirewall
nextHopType     : Azure Firewall
routeOrigin     : virtualHubs/vhub/hubRouteTables/defaultRouteTable
asPath          : 

addressPrefixes : {10.0.0.0/8}
nextHops        : azureFirewalls/vwanFirewall
nextHopType     : Azure Firewall
routeOrigin     : virtualHubs/vhub/hubRouteTables/defaultRouteTable
asPath          : 

addressPrefixes : {172.16.0.0/12}
nextHops        : azureFirewalls/vwanFirewall
nextHopType     : Azure Firewall
routeOrigin     : virtualHubs/vhub/hubRouteTables/defaultRouteTable
asPath          : 

addressPrefixes : {192.168.0.0/16}
nextHops        : azureFirewalls/vwanFirewall
nextHopType     : Azure Firewall
routeOrigin     : virtualHubs/vhub/hubRouteTables/defaultRouteTable
asPath          :  

仮想ハブ内の Azure Firewall のルーティングは次の通りです。VPN サイトで静的に設定したアドレス2つが仮想ハブ内の VPN ゲートウェイに向いていることが分かります。

Get-AzVhubEffectiveRoutes -virtualWanResourceType "AzureFirewalls" -resourceId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0813vwan3/providers/Microsoft.Network/azureFirewalls/vwanFirewall" -vhubId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test0813vwan3/providers/Microsoft.Network/virtualHubs/vhub"

addressPrefixes : {10.0.0.0/16}
nextHops        : vpnGateways/vwanVpnGw
nextHopType     : VPN_S2S_Gateway
routeOrigin     : vpnGateways/vwanVpnGw
asPath          : 

addressPrefixes : {10.1.0.0/16}
nextHops        : vpnGateways/vwanVpnGw
nextHopType     : VPN_S2S_Gateway
routeOrigin     : vpnGateways/vwanVpnGw
asPath          : 

addressPrefixes : {192.168.1.0/24}
nextHops        : virtualHubs/vhub/hubVirtualNetworkConnections/vhub-to-vwanSpoke
nextHopType     : Virtual Network Connection
routeOrigin     : virtualHubs/vhub/hubVirtualNetworkConnections/vhub-to-vwanSpoke
asPath          : 

addressPrefixes : {0.0.0.0/0}
nextHops        : 
nextHopType     : Internet
routeOrigin     : 
asPath          : 

自作関数Get-AzVhubEffectiveRoutesの詳細

function Get-AzVhubEffectiveRoutes {
  param (
    [Parameter(Mandatory=$true)][string]$virtualWanResourceType,
    [Parameter(Mandatory=$true)][string]$resourceId,
    [Parameter(Mandatory=$true)][string]$vhubId
  )


  $body = @{
    "VirtualWanResourceType"=$virtualWanResourceType
    "ResourceId"=$resourceId
  } | convertto-json


  $url = $vhubId + "/effectiveRoutes?api-version=2022-05-01"
  write-host $url
  $res = Invoke-AzRest -Path $url -Method POST -Payload $body
  $operationUrl = $res.Headers.GetValues('Azure-AsyncOperation')
  $operationUrl =  $operationUrl[0] -replace "https://management.azure.com",""

  do {
    $routes = Invoke-Azrest -path $operationUrl -method Get
  } until ( ($routes.Content | convertfrom-json).status -ne "InProgress" )

  ($routes.Content | convertfrom-json).properties.output.value | Select-Object addressPrefixes, `
          @{Label="nextHops"; Expression={[regex]::Match($_.nextHops,"Microsoft.Network/(.*)").Groups[1].value}}, `
          @{Label="nextHopType"; Expression={$_.nextHopType}}, `
          @{Label="routeOrigin"; Expression={[regex]::Match($_.routeOrigin,"Microsoft.Network/(.*)").Groups[1].value}}, `
          @{Label="asPath"; Expression={$_.asPath}} | fl *

}

2つのスポーク間の仮想マシン間で SSH を試すと無事に通ります。大成功。

# 仮想ハブ側の仮想マシンから普通のハブ側に SSH
username@vwanSpokeVm:~$ ssh username@10.1.0.4
# SSH 接続後に ss コマンドを叩くと、仮想ハブ側の仮想マシンから SSH されていることが分かる
username@spokeVm:~$ ss -tnla
State   Recv-Q   Send-Q     Local Address:Port     Peer Address:Port   Process  
LISTEN  0        4096       127.0.0.53%lo:53            0.0.0.0:*               
LISTEN  0        128              0.0.0.0:22            0.0.0.0:*               
ESTAB   0        52              10.1.0.4:22        192.168.1.4:50816           
LISTEN  0        128                 [::]:22               [::]:*           

普通のハブの Azure Firewall と仮想ハブの Azure Firewall のログに上記の SSH 通信が記録されていますので、ちゃんと両方の Firewall を経由してそうです。

> $query = "AZFWNetworkRule | where SourcePort == '50816' | extend fw=split(_ResourceId, '/')[-1]| project TimeGenerated,fw,Action,SourceIp,SourcePort,DestinationIp,DestinationPort,Protocol"
> $result = Invoke-AzOperationalInsightsQuery -WorkspaceId "bd6681ca-a981-464a-91e8-3f28d39f91fa" -Query $query
> $result.Results                                                                                                

TimeGenerated   : 2025-08-13T14:36:59.930295Z
fw              : vwanfirewall
Action          : Allow
SourceIp        : 192.168.1.4
SourcePort      : 50816
DestinationIp   : 10.1.0.4
DestinationPort : 22
Protocol        : TCP

TimeGenerated   : 2025-08-13T14:36:59.980775Z
fw              : hubfirewall
Action          : Allow
SourceIp        : 192.168.1.4
SourcePort      : 50816
DestinationIp   : 10.1.0.4
DestinationPort : 22
Protocol        : TCP

おわりに

普通のハブアンドスポークな仮想ネットワークと Virtual WAN を利用したハブアンドスポークな仮想ネットワークを S2S VPN で接続してみました。「そりゃそうだろ」という話ではありますが普通につながりました。

Note

  • 当サイトは個人のブログです。このブログに示されている見解や意見は個人的なものであり、所属組織の見解や意見を表明するものではありません。
  • 公開情報を踏まえて正確な情報を掲載するよう努めますが、その内容の完全性や正確性、有用性、安全性、最新性について一切保証しません。
  • 添付文章やリンク先などを含む本サイトの内容は作成時点でのものであり、予告なく変更される場合があります。