🍺

BGPで経路広告したKubeVirt VMのIPアドレスに外から接続する

に公開

はじめに

OpenShift Virtualization Advent Calender 2025 12日目の記事です。

OpenShift v4.20 (もしくはv4.19.14) 以降で、CNIプラグインOVN-KubernetesがBGP対応しました。具体的には、frr-k8sと連携して、デフォルトのPodネットワークやUser Defined NetworkのアドレスをBGPで広告できるようになりました。VRFを切って、そこから広告することもできます。また、BGPで外部から受け取った経路をインポートすることもできます。

背景

MetalLBという、オンプレミスでLoadBalancer Serviceを使えるようにする仕組みがあります。L3モードでMetalLBを構成すると、LoadBalancer ServiceのExternal IPを /32 の経路としてBGPで広告します。MetalLBでBGP広告する機能は、オープンソースのルーティングプロトコル制御ソフトウェアであるFRRouting (frr) を使います。

frrがルーティングエンジンとして有能すぎて、いろんなユーザーこれをMetalLB以外の目的で使いたくなってしまいました。そこで、

  • frrをMetalLBから切り離し、
  • frrに「frr-k8s」というAPI層をかぶせてKubernetesのAPI(カスタムリソースFRRConfiguration)経由でfrrのコンフィグができるようにし、
  • MetalLB以外からもfrrを利用できる
    ように進化していきました。

OpenShiftの標準CNIプラグインであるOVN-KubernetesのBGP対応では、このfrr-k8sを使うことで、PodネットワークをBGPで広告します。OVN-Kubernetes、MetalLBそれぞれがBGPを利用する際は、OVN-Kubernetesが設定するFRRConfiguratioinとMetalLBが設定するFRRConfigurationをfrr-k8sがいい感じにマージして、frrのコンフィグを生成します。

できること

OVN-KubernetesのBGPサポートでは、

  • デフォルトのPodネットワーク
  • プライマリのCluster User Defined Network (CUDN)
  • EgressIP

のアドレスをBGPで広告します。ノードのIPアドレスやAPI VIP、Ingress VIPは対象外ですのでご注意ください(これができれば完全にBGPベースのデータセンターにそのまま組み込めたのですが、残念)。

ServiceのIPアドレスをBGPで広告するときは、MetalLBのL3モードを使います。

動作確認

OpenShiftの設定

さっそく動きを見てみましょう。ここでは、図のようなネットワークにおいて[1]、L2 CUDNをプライマリネットワークにして仮想マシンを接続します。

まず準備として、OpenShiftでfrr-k8sを使えるようにします。MetalLB Operatorとインストールしている場合は、すでにfrr-k8sが使えるようになっています。そうでない場合は、カスタムリソース networks.operator.openshift.io に対して以下を設定します。

spec:
  additionalRoutingCapabilities:
    providers:
    - FRR
  defaultNetwork:
    ovnKubernetesConfig:
      routeAdvertisements: Enabled

本記事では触れませんが、VRF Liteを使う場合は下記も設定します。

spec:
  defaultNetwork:
    ovnKubernetesConfig:
      gatewayConfig:
        ipForwarding: Global
        routingViaHost: true

CUDN (Cluster User Defined Network) の作成

まず、Namespaceを作成します。k8s.ovn.org/primary-user-defined-network: ""のラベルをつけて、このNamespaceのプライマリネットワークをデフォルトPodネットワークではなくUser Defined Networkにします。

apiVersion: v1
kind: Namespace
metadata:
  name: proj2
  labels:
    k8s.ovn.org/primary-user-defined-network: ""

次に cudn2 という名前でL2 CUDNを作成します。クラスターの全ノードにまたがったひとつのL2ネットワークを作成し、それをPod/VMのプライマリネットワーク (1つ目のインターフェース、デフォルトゲートウェイに向く方) として使用します。

apiVersion: k8s.ovn.org/v1
kind: ClusterUserDefinedNetwork
metadata:
  labels:
    export: "true"
  name: cudn2
spec:
  namespaceSelector:
    matchExpressions:
    - key: kubernetes.io/metadata.name
      operator: In
      values:
      - proj2
  network:
    topology: Layer2
    layer2:
      role: Primary
      ipam:
        lifecycle: Persistent
      subnets:
      - "172.22.0.0/16"

仮想マシンの起動

仮想マシンを起動してみます。network bindingを(デフォルトPodネットワークに接続するときに設定する masquerade ではなく) l2bridge に設定しておきます。

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: fedora-1
spec:
  template:
    spec:
      domain:
        devices:
          interfaces:
          - name: default
            binding:
              name: l2bridge
      networks:
      - name: default
        pod: {}

VMのIPアドレスを確認します。VMをくるむvirt-launcher Podとしては、依然としてデフォルトPodネットワークのIPアドレスが見えています。

[ori@bastion ~]$ oc get pod -l kubevirt.io/domain=fedora-1 -o wide
NAME                           READY   STATUS    RESTARTS   AGE   IP            NODE   NOMINATED NODE   READINESS GATES
virt-launcher-fedora-1-pnhh4   2/2     Running   0          8h    10.128.7.62   wk3    <none>           1/1

が、デフォルトゲートウェイは(Podから見たときの)2番目のインターフェースに設定されており、こちらには期待通りcudn2のIPアドレスがついています。

[ori@bastion ~]$ oc get pod virt-launcher-fedora-1-pnhh4 -o yaml | yq '.metadata.annotations."k8s.v1.cni.cncf.io/network-status"'
[{
    "name": "ovn-kubernetes",
    "interface": "eth0",
    "ips": [
        "10.128.7.62"
    ],
    "mac": "0a:58:0a:80:07:3e",
    "dns": {}
},{
    "name": "ovn-kubernetes",
    "interface": "ovn-udn1",
    "ips": [
        "172.22.0.3"
    ],
    "mac": "0a:58:ac:16:00:03",
    "default": true,
    "dns": {}
}]

virt-launcher PodにはKubeVirt的な事情でいろんなインターフェースが見えます。

[ori@bastion ~]$ oc exec virt-launcher-fedora-1-pnhh4 -- ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0@if87: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue state UP group default
    link/ether 0a:58:0a:80:07:3e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.128.7.62/24 brd 10.128.7.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::858:aff:fe80:73e/64 scope link
       valid_lft forever preferred_lft forever
3: ovn-udn1-nic@if88: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue master k6t-ovn-udn1 state UP group default
    link/ether 1e:93:89:68:6a:6f brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::1c93:89ff:fe68:6a6f/64 scope link
       valid_lft forever preferred_lft forever
4: k6t-ovn-udn1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue state UP group default qlen 1000
    link/ether 1e:78:f8:aa:eb:c5 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::1c93:89ff:fe68:6a6f/64 scope link
       valid_lft forever preferred_lft forever
5: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc mq master k6t-ovn-udn1 state UP group default qlen 1000
    link/ether 1e:78:f8:aa:eb:c5 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::1c78:f8ff:feaa:ebc5/64 scope link
       valid_lft forever preferred_lft forever
6: ovn-udn1: <BROADCAST,NOARP> mtu 1400 qdisc noop state DOWN group default qlen 1000
    link/ether 0a:58:ac:16:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.22.0.3/16 brd 172.22.255.255 scope global ovn-udn1
       valid_lft forever preferred_lft forever
    inet6 fe80::858:acff:fe16:3/64 scope link
       valid_lft forever preferred_lft forever

デフォルトPodネットワークにつながっているのは eth0 です。VMのプライマリインターフェースは k6t-ovn-udn1 というブリッジにつながっています。

[ori@bastion ~]$ oc exec virt-launcher-fedora-1-pnhh4 -- ip link show type bridge
4: k6t-ovn-udn1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue state UP mode DEFAULT group default qlen 1000

ブリッジにつながるovn-udn1-nic というインターフェースがvirt-launcher PodのNetwork Namespaceのveth、tap0 がVMにつながるtapインターフェースです。

[ori@bastion ~]$ oc exec virt-launcher-fedora-1-pnhh4 -- ip link show master k6t-ovn-udn1
3: ovn-udn1-nic@if88: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue master k6t-ovn-udn1 state UP mode DEFAULT group default
    link/ether 1e:93:89:68:6a:6f brd ff:ff:ff:ff:ff:ff link-netnsid 0
5: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc mq master k6t-ovn-udn1 state UP mode DEFAULT group default qlen 1000
    link/ether 1e:78:f8:aa:eb:c5 brd ff:ff:ff:ff:ff:ff

VMとして見ると、インターフェースは1個しかついてなくて、cudn2のアドレスがついています。

[ori@bastion ~]$ oc get vmi fedora-1 -o yaml | yq .status.interfaces
- infoSource: domain, guest-agent
  interfaceName: enp1s0
  ipAddress: 172.22.0.3
  ipAddresses:
    - 172.22.0.3
    - fe80::858:acff:fe16:3
  linkState: up
  mac: 0a:58:ac:16:00:03
  name: default
  podInterfaceName: ovn-udn1
  queueCount: 4
[ori@bastion ~]$ oc virt console fedora-1
Successfully connected to fedora-1 console. Press Ctrl+] or Ctrl+5 to exit console.

[fedora@fedora-1 ~]$ ip -br addr show
lo               UNKNOWN        127.0.0.1/8 ::1/128
enp1s0           UP             172.22.0.3/16 fe80::858:acff:fe16:3/64

CUDNのアドレスをBGPで広告

frr-k8sの設定をします。カスタムリソース FRRConfiguration を使います。BGPのピア先のアドレスやAS番号を設定しています。

apiVersion: frrk8s.metallb.io/v1beta1
kind: FRRConfiguration
metadata:
  name: receive-all
  namespace: openshift-frr-k8s
  labels:
    routeAdvertisements: receive-all
spec:
  bgp:
    routers:
    - asn: 65801
      neighbors:
      - address: 172.18.20.1
        asn: 65102
        disableMP: true
        toReceive:
          allowed:
            mode: all

先ほどのFRRConfigurationとCUDNをラベルで指定して、経路広告の設定をします。カスタムリソース RouteAdvertisement を使います。

apiVersion: k8s.ovn.org/v1
kind: RouteAdvertisements
metadata:
  name: adv-cudn1
spec:
  advertisements:
  - PodNetwork
  networkSelectors:
  - networkSelectionType: ClusterUserDefinedNetworks
    clusterUserDefinedNetworkSelector:
      networkSelector:
        matchLabels:
          export: "true"
  frrConfigurationSelector:
    matchLabels:
      routeAdvertisements: receive-all
  nodeSelector: {}

以上でCUDN cudn2 をBGPで広告する設定は完了です。RouteAdvertisement で設定した FRRConfiguration と ClusterUserDefinedNetwork の設定から、OVN-Kubernetesが各ノードのfrrのコンフィグを生成します。

生成したコンフィグはカスタムリソース FRRNodeState から確認できます。下記はノード wk4 の FRRNodeState です。作成したCUDN (cudn2) と同じ名前のVRFを切って、そこでCUDN cudn2 のアドレスを広告していること、外からデフォルトVRFで受け取った経路はVRF cudn2 にリークしていることなどがわかります。

今回の例はL2 CUDNなので、各ノードがL2 CUDNのアドレスブロック全体を広告しています。L3 CUDNの場合は、ノードごとに担当するアドレスブロックがわかれますが、BGPで広告する際は各ノードが自ノードで担当するCIDRを広告します。

apiVersion: frrk8s.metallb.io/v1beta1
kind: FRRNodeState
metadata:
  creationTimestamp: "2025-11-02T14:31:54Z"
  generation: 1
  name: wk4
  resourceVersion: "54719308"
  uid: c38b100f-fb95-40a9-b646-db707b068eef
spec: {}
status:
  lastConversionResult: success
  lastReloadResult: success
  runningConfig: |
    Building configuration...

    Current configuration:
    !
    frr version 8.5.3
    frr defaults traditional
    hostname wk4
    log file /etc/frr/frr.log informational
    log timestamp precision 3
    service integrated-vtysh-config
    !
    router bgp 65801
     no bgp ebgp-requires-policy
     no bgp default ipv4-unicast
     bgp graceful-restart preserve-fw-state
     no bgp network import-check
     neighbor 172.18.20.1 remote-as 65102
     !
     address-family ipv4 unicast
      network 172.22.0.0/16
      neighbor 172.18.20.1 activate
      neighbor 172.18.20.1 route-map 172.18.20.1-in in
      neighbor 172.18.20.1 route-map 172.18.20.1-out out
      import vrf cudn2
     exit-address-family
     !
     address-family ipv6 unicast
      import vrf cudn2
     exit-address-family
    exit
    !
    router bgp 65801 vrf cudn2
     no bgp ebgp-requires-policy
     no bgp hard-administrative-reset
     no bgp default ipv4-unicast
     no bgp graceful-restart notification
     bgp graceful-restart preserve-fw-state
     no bgp network import-check
     !
     address-family ipv4 unicast
      import vrf default
     exit-address-family
     !
     address-family ipv6 unicast
      import vrf default
     exit-address-family
    exit
    !
    ip prefix-list 172.18.20.1-inpl-ipv4 seq 1 permit any
    ip prefix-list 172.18.20.1-allowed-ipv4 seq 1 permit 172.22.0.0/16
    !
    ipv6 prefix-list 172.18.20.1-allowed-ipv6 seq 1 deny any
    ipv6 prefix-list 172.18.20.1-inpl-ipv4 seq 2 permit any
    !
    route-map 172.18.20.1-out permit 1
     match ip address prefix-list 172.18.20.1-allowed-ipv4
    exit
    !
    route-map 172.18.20.1-out permit 2
     match ipv6 address prefix-list 172.18.20.1-allowed-ipv6
    exit
    !
    route-map 172.18.20.1-in permit 3
     match ip address prefix-list 172.18.20.1-inpl-ipv4
    exit
    !
    route-map 172.18.20.1-in permit 4
     match ipv6 address prefix-list 172.18.20.1-inpl-ipv4
    exit
    !
    ip nht resolve-via-default
    !
    ipv6 nht resolve-via-default
    !
    end

対向ルータ r2 の状況も見ておきましょう。まずBGPのピアですが、対向ルータ r2 が各ノードとピアを張っていることが確認できます。

vyos@r2.ovnk.setns.net:~$ show bgp summary

IPv4 Unicast Summary (VRF default):
BGP router identifier 172.18.0.2, local AS number 65102 vrf-id 0
BGP table version 2090
RIB entries 6, using 576 bytes of memory
Peers 9, using 181 KiB of memory

Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
172.18.0.1      4      65101     57432     57442     2090    0    0 05w3d13h            2        4 N/A
172.18.20.100   4      65801       613       619     2090    0    0 10:06:54            1        4 N/A
172.18.20.101   4      65801       615       621     2090    0    0 10:06:54            1        4 N/A
172.18.20.102   4      65801       613       619     2090    0    0 10:06:54            1        4 N/A
172.18.20.110   4      65801       613       619     2090    0    0 10:06:54            1        4 N/A
172.18.20.111   4      65801       612       619     2090    0    0 10:06:54            1        4 N/A
172.18.20.112   4      65801       613       619     2090    0    0 10:06:54            1        4 N/A
172.18.20.113   4      65801       617       626     2090    0    0 10:06:54            1        4 N/A
172.18.20.114   4      65801       617       626     2090    0    0 10:06:54            1        4 N/A

Total number of neighbors 9

BGPの経路は以下のとおりです。各ノードが広告したL2 CUDN cudn2 のアドレスブロック 172.22.0.0/16 を受け取っていることがわかります。

vyos@r2.ovnk.setns.net:~$ show ip bgp
BGP table version is 2090, local router ID is 172.18.0.2, vrf id 0
Default local pref 100, local AS 65102
Status codes:  s suppressed, d damped, h history, * valid, > best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

    Network          Next Hop            Metric LocPrf Weight Path
 *> 0.0.0.0/0        172.18.0.1               0             0 65101 i
 *> 172.18.20.0/24   0.0.0.0                  0         32768 i
 *> 172.18.30.0/24   172.18.0.1                             0 65101 65103 i
 *= 172.22.0.0/16    172.18.20.111            0             0 65801 i
 *=                  172.18.20.110            0             0 65801 i
 *=                  172.18.20.113            0             0 65801 i
 *>                  172.18.20.100            0             0 65801 i
 *=                  172.18.20.114            0             0 65801 i
 *=                  172.18.20.101            0             0 65801 i
 *=                  172.18.20.102            0             0 65801 i
 *=                  172.18.20.112            0             0 65801 i

Displayed  4 routes and 11 total paths

ノード wk4 上で稼働するfrrにvtyshで入ってみます。

[ori@bastion ~]$ oc -n openshift-frr-k8s exec -c frr frr-k8s-fv8k7 -- vtysh -c 'show ip bgp'
BGP table version is 4, local router ID is 172.18.20.114, vrf id 0
Default local pref 100, local AS 65801
Status codes:  s suppressed, d damped, h history, * valid, > best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

    Network          Next Hop            Metric LocPrf Weight Path
 *> 0.0.0.0/0        172.18.20.1                            0 65102 65101 i
 *> 172.18.20.0/24   172.18.20.1              0             0 65102 i
 *> 172.18.30.0/24   172.18.20.1                            0 65102 65101 65103 i
 *> 172.22.0.0/16    0.0.0.0                  0         32768 i

Displayed  4 routes and 4 total paths

CUDN cudn2 のCIDR 172.22.0.0/16 を広告し、またいくつか経路を受け取っていることがわかります。

ノード上のVRFの確認

OpenShiftの各ノードでは、CUDNと同名のVRFが作成されます。本記事のように、(VRF Liteを使わず)CUDNのアドレスをBGPで広告するだけだと、CUDNのVRFの経路をデフォルトVRFにリークさせるため、実際のところVRFを意識する必要はありません。が、念のためどのようなVRFができるかを確認しておきます。

以下はノード wk4 上で確認した結果です。VRF cudn2 がテーブル番号 1008 で作成されていることがわかります。番号はランダムなので、各ノードでテーブル番号がそろうわけではありません。

[core@wk4 ~]$ ip vrf show
Name              Table
-----------------------
cudn2             1008

ルーティングルールを見ると、L2 CUDNのCIDR 172.22.0.0/16 宛てのパケットはVRFのテーブル 1008 番を見に行く設定になっていることがわかります。

[core@wk4 ~]$ ip rule show
0:      from all lookup local
30:     from all fwmark 0x1745ec lookup 7
998:    from all to 10.200.0.0/16 lookup main proto static
998:    from all to 10.128.0.0/16 lookup main proto static
998:    from all to 169.254.0.0/17 lookup main proto static
1000:   from all lookup [l3mdev-table]
2000:   from all fwmark 0x1010 lookup 1008
2000:   from all to 169.254.0.42 lookup 1008
2000:   from all to 172.22.0.0/16 lookup 1008	# ← ここ
5999:   from all fwmark 0x3f0 lookup main
32766:  from all lookup main
32767:  from all lookup default

結果として、CUDN cudn2 宛てのパケットは、VRF cudn2 のルーティングテーブルにしたがって転送されます。

[core@wk4 ~]$ ip route show vrf cudn2
default via 172.18.20.1 dev br-ex mtu 1400
unreachable default metric 4278198272
10.200.0.0/16 via 169.254.0.4 dev br-ex mtu 1400
169.254.0.3 via 172.22.0.1 dev ovn-k8s-mp16
169.254.0.42 dev ovn-k8s-mp16 mtu 1400
172.18.30.0/24 nhid 288 via 172.18.20.1 dev br-ex proto bgp metric 20
172.22.0.0/16 dev ovn-k8s-mp16 proto kernel scope link src 172.22.0.2

外部から受け取った経路の確認

外部のルータから受け取った経路 172.18.30.0/24 がノードのFIBにインポートされていることも確認できます。

[core@wk4 ~]$ ip route show proto bgp
172.18.30.0/24 nhid 288 via 172.18.20.1 dev br-ex metric 20

クラスターの外からVMへの疎通確認

クラスターの外から、VMのIPアドレスにアクセスしてみます。

[test@testvm0 ~]$ ssh 172.22.0.3 hostname
fedora-1
[test@testvm0 ~]$ traceroute -n 172.22.0.3
traceroute to 172.22.0.3 (172.22.0.3), 30 hops max, 60 byte packets
 1  172.18.10.2  0.965 ms  0.912 ms  0.890 ms
 2  172.18.12.2  1.329 ms  1.009 ms  0.991 ms
 3  172.18.20.114  1.535 ms  1.493 ms  1.475 ms
 4  172.22.0.3  2.455 ms  2.375 ms  2.420 ms

VMのIPアドレスに直接アクセスできることがわかります。

まとめ

OVN-Kubernetesで、L2トポロジーのCluster User Defined NetworkのアドレスをBGPで経路広告し、外部からVMのIPアドレスに直接 (NodePortやIngressを経由せずに) アクセスできることを確認しました。

OVN-KubernetesのBGPサポートでは、CUDNの他に、デフォルトのPodネットワークやEgressIPを経路広告することができます。また複数のCUDNのVRFを異なるVLANから広告することもできます(VRF Lite)。本記事で扱わなかった方式についてもひととおりスライドにまとめたものがありますので、ご興味があれば見てみてください。

https://speakerdeck.com/orimanabu/openshiftnobgpsapoto-ovn-kubernetesbian

脚注
  1. 図にはShowNetアイコンを使用させていただきました https://github.com/interop-tokyo-shownet/shownet-icons ↩︎

GitHubで編集を提案

Discussion