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)。本記事で扱わなかった方式についてもひととおりスライドにまとめたものがありますので、ご興味があれば見てみてください。
-
図にはShowNetアイコンを使用させていただきました https://github.com/interop-tokyo-shownet/shownet-icons ↩︎
Discussion