OVN-Kubernetes のノード再起動後にネットワーク断になる

Table of Contents

1. はじめに

みなさん、自宅 Kubernetes クラスタ運用してますか?

今回は、自宅サーバーで動かしている OVN-Kubernetes(Helm デプロイ)の環境で、ノードを再起動すると外部疎通が完全に断になるというなかなか厄介な問題に遭遇したので、原因の深掘りと解決までの記録をお話しします。

OVN-Kubernetes のセットアップについてはこちらの記事で紹介しているので、興味があればぜひ読んでみてください。

2. OVN-Kubernetes のブリッジ構成

NicToBridge の仕組み

OVN-Kubernetes は、ノードの物理 NIC(例: eth0)を OVS ブリッジ(breth0)に組み込んで使います。 この処理を行うのが NicToBridge() 関数です。

やっていることをざっくりまとめると:

  1. OVS ブリッジ breth0 を作成
  2. eth0breth0 のポートとして追加
  3. eth0other-config:transient=true を設定
  4. eth0 の IP アドレスとルートを breth0 に移行
// go-controller/pkg/util/nicstobridge.go (NicToBridge)
stdout, stderr, err := RunOVSVsctl(
    "--", "--may-exist", "add-br", bridge,
    "--", "br-set-external-id", bridge, "bridge-id", bridge,
    "--", "br-set-external-id", bridge, "bridge-uplink", iface,
    "--", "set", "bridge", bridge, "fail-mode=standalone",
    fmt.Sprintf("other_config:hwaddr=%s", ifaceLink.Attrs().HardwareAddr),
    "--", "--may-exist", "add-port", bridge, iface,
    "--", "set", "port", iface, "other-config:transient=true")

ここでポイントになるのが other-config:transient=true です。 これは「このポートは一時的なものなので、OVS 再起動時に削除してよい」というマーカーです。

正常な再起動フロー

正常に機能する場合、以下のような流れでノード再起動後もネットワークが復旧します。

  sequenceDiagram
    participant OVS as ovsdb-server
    participant NIC as eth0
    participant BR as breth0
    participant OVN as ovnkube-node

    Note over OVS: ノード再起動
    OVS->>OVS: --delete-transient-ports で起動
    OVS->>NIC: transient ポート(eth0)を削除
    Note over NIC: eth0 がブリッジから外れる
    NIC->>NIC: systemd-networkd が IP を設定
    Note over NIC: eth0 経由で外部疎通が復旧
    OVN->>OVN: API server に到達可能
    OVN->>BR: NicToBridge() で eth0 を再接続
    Note over BR: breth0 に IP を移行して通常運用に復帰

3. 発生した問題

さて、ここからが本題です。 実際にノードを再起動すると、外部疎通が完全に断になってしまいました

ネットワーク初期化のデッドロック

もし --delete-transient-ports が OVS の起動時に指定されていない場合、transient ポートは削除されずにそのまま残ります。

OVS のデータベースは永続化されているため、再起動後も eth0breth0 の transient ポートとして残ったままになります。 一方、IP アドレスは systemd-networkd によって eth0 に設定されますが、eth0 はブリッジのポートなのでパケットは全て breth0 に吸い込まれてしまいます。 しかし breth0 には IP が設定されていないため、外部からの通信は一切できません。

ここで ovnkube-node が NicToBridge() を実行して IP を breth0 に移行すればいいのですが、 ovnkube-node が起動するには API server への到達が必要です。でもネットワークが壊れているので API server に到達できない…

  sequenceDiagram
    participant OVS as ovsdb-server
    participant NIC as eth0
    participant BR as breth0
    participant OVN as ovnkube-node
    participant API as API server

    Note over OVS: ノード再起動
    OVS->>OVS: transient ポートを削除しない!
    Note over NIC: eth0 は breth0 のポートのまま
    NIC->>NIC: systemd-networkd が IP を設定
    Note over NIC: IP は eth0 にあるが<br/>パケットは breth0 に吸い込まれる
    Note over BR: breth0 に IP がない → 外部疎通断
    OVN->>API: API server に接続しようとする
    API--xOVN: ネットワーク断で到達不可
    Note over OVN: NicToBridge() を実行できない
    Note over OVS,API: デッドロック!

このデッドロック自体は 2018 年の issue #274 で報告され、 /etc/default--delete-transient-ports を書き込む仕組み(setupDefaultFile())で対処されていました。 しかし Helm デプロイ環境ではこの仕組みが機能しておらず、 issue #4731 で改めて報告され、2025 年 10 月にようやく修正されていたようです。

--delete-transient-ports が機能していなかった理由

other-config:transient=true が設定されているポートの削除は、 OVS の del_transient_ports() という関数によって行われます。

# utilities/ovs-ctl.in
del_transient_ports () {
    for port in `ovs-vsctl --bare -- --columns=name find port other_config:transient=true`; do
        ovs_vsctl -- del-port "$port"
    done
}

この関数は ovs-ctl start--delete-transient-ports オプションを渡すと呼ばれるものです。

問題は、このオプションが OVS の起動時に渡されていなかったことです。

NicToBridge() の中では、transient の設定後に setupDefaultFile() を呼んでいます。 この関数は /etc/default/openvswitch-switch--delete-transient-ports を書き込む設計です。

// go-controller/pkg/util/nicstobridge.go
func setupDefaultFile() {
    // ...
    if platform == ubuntu {
        defaultFile = ubuntuDefaultFile
        text = "OVS_CTL_OPTS=\"$OVS_CTL_OPTS --delete-transient-ports\""
    } else if platform == rhel {
        defaultFile = rhelDefaultFile
        text = "OPTIONS=--delete-transient-ports"
    }
    // defaultFile に text を追記...
}

この仕組みは systemd 経由で OVS を起動するホストネイティブなデプロイでは正しく動きます。 しかし、コンテナ化デプロイ(Helm)では以下の理由で完全に機能しません:

  1. setupDefaultFile()ovnkube-node コンテナ内の /etc/default に書くだけで、ホストには届かない
  2. OVS を起動するのは 別コンテナ(ovs-node)
  3. ovnkube.sh(OVS 起動スクリプト)は /etc/default を source しない

つまり setupDefaultFile() はコンテナ化環境では動作していなかったのです。

4. 解決

v1.2.0 での修正

この問題は OVN-Kubernetes v1.2.0 のコミット fba0fef66 (“ovs-node: Delete transient ports on startup”)で修正されました。

修正内容はシンプルで、ovnkube.sh の OVS 起動部分に --delete-transient-ports を直接ハードコードしています。

# dist/images/ovnkube.sh
# OVN-K marks NIC port as transient on startup when plugging it into ovs
# bridge. This is done so that on ovsdb-server resrtart, the NIC interface is
# detached from the bridge, which is necessary to restore connectivity
# through the NIC and make the node healthy. Marking the port as transient
# works only when we also start ovsdb-server with --delete-transient-ports.
#
# Note: once ovnkube is started, it will rewire the NIC port back into the
# bridge, and move IP configuration as necessary.
ovs_options="${ovs_options} --delete-transient-ports"

/usr/share/openvswitch/scripts/ovs-ctl start --no-ovs-vswitchd \
  --system-id=random ${ovs_options} ${USER_ARGS} "$@"

これにより、OVS(ovsdb-server)の起動時に transient ポートが自動で削除され、eth0 がブリッジから外れることでネットワークが復旧するようになります。

v1.1.0 → v1.2.0 へアップグレードすることで、この問題は解決しました。

5. 手動復旧メモ

もしデッドロックが発生してしまった場合、コンソールなどからVMを操作することができれば以下のコマンドで応急処置できます。

# breth0 に IP を付与して eth0 から削除
ip addr add <ADDR> dev breth0
ip addr del <ADDR> dev eth0

# デフォルトルートを breth0 経由に設定
ip route add default via <GW> dev breth0
ip route delete default via <GW> dev eth0

これで外部疎通が復旧するので、あとは ovnkube-node が API server に到達して通常の処理を再開できます。

6. まとめ

今回の問題は、OVN-Kubernetes の --delete-transient-ports の仕組みがコンテナ化デプロイに対応していなかったことが原因でした。

デッドロック自体は 2018 年に対処されていたものの、コンテナ環境では見落とされていたというのが面白いところです。 原因を追っていく中で、OVS の機能や OVN-Kubernetes の内部実装への理解が深まりました。 特に OVS の transient はこういう時に必要になるのかーと身に沁みて実感できましたね。

最後まで読んでいただき、ありがとうございました。

7. 参考資料