Skip to content

OPNET-773: network: add BGP-based VIP management enhancement proposal#1982

Open
mkowalski wants to merge 6 commits into
openshift:masterfrom
mkowalski:OPNET-595-bgp-vip-management
Open

OPNET-773: network: add BGP-based VIP management enhancement proposal#1982
mkowalski wants to merge 6 commits into
openshift:masterfrom
mkowalski:OPNET-595-bgp-vip-management

Conversation

@mkowalski

Copy link
Copy Markdown
Contributor

Introduce an enhancement proposal for replacing keepalived/VRRP-based VIP management with a BGP-based approach using kube-vip (Routing Table Mode) and frr-k8s deployed as static pods on bare metal clusters.

Closes: https://redhat.atlassian.net/browse/OPNET-773
Epic: https://redhat.atlassian.net/browse/OPNET-595

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Apr 23, 2026
@openshift-ci-robot

openshift-ci-robot commented Apr 23, 2026

Copy link
Copy Markdown

@mkowalski: This pull request references OPNET-773 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Introduce an enhancement proposal for replacing keepalived/VRRP-based VIP management with a BGP-based approach using kube-vip (Routing Table Mode) and frr-k8s deployed as static pods on bare metal clusters.

Closes: https://redhat.atlassian.net/browse/OPNET-773
Epic: https://redhat.atlassian.net/browse/OPNET-595

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from dougbtv and tssurya April 23, 2026 13:07
@openshift-ci

openshift-ci Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign abhat for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mkowalski
mkowalski force-pushed the OPNET-595-bgp-vip-management branch 4 times, most recently from cff5714 to 4d7317d Compare April 23, 2026 15:48
Introduce an enhancement proposal for replacing keepalived/VRRP-based
VIP management with a BGP-based approach using kube-vip (Routing Table
Mode) and frr-k8s deployed as static pods on bare metal clusters.

Closes: https://redhat.atlassian.net/browse/OPNET-773
Epic: https://redhat.atlassian.net/browse/OPNET-595
@mkowalski
mkowalski force-pushed the OPNET-595-bgp-vip-management branch from 4d7317d to a4a5e10 Compare April 23, 2026 15:49
@mkowalski

Copy link
Copy Markdown
Contributor Author

/cc @cybertron
/uncc @dougbtv

@openshift-ci
openshift-ci Bot requested review from cybertron and removed request for dougbtv April 23, 2026 16:47
@mkowalski

Copy link
Copy Markdown
Contributor Author

/cc @jcaamano
/cc @fedepaol

@openshift-ci
openshift-ci Bot requested review from fedepaol and jcaamano April 23, 2026 16:47

#### Single-node Deployments or MicroShift

For single-node OpenShift (SNO), BGP-based VIP management is applicable but has

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does SNO use keepalived today? We prevent keepalived from starting with less than two nodes: https://github.com/openshift/baremetal-runtimecfg/blob/dca8cb1be0cce1ce404dfd407ead1326a3c8bb40/pkg/monitor/dynkeepalived.go#L105

I suppose SNO could disable unicast though, which would at least allow it to run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SNO does not use keepalived today. But I would like SNO to be able to use BGP tomorrow.


## Summary

This enhancement proposes replacing the current keepalived/VRRP-based Virtual IP

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/replacing/augmenting/

- **Bootstrap phase:** The installer generates a per-node `frr.conf` for each
host. If `host.bgpPeers` is set, that host's `frr.conf` uses the
host-specific peers; otherwise, it uses the global `bgpVIPConfig.peers`.
MCO renders the correct `frr.conf` for each node via per-node MachineConfig.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per-node MachineConfig

This is not a thing today. How do you intend to apply per-node configs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point. As we are vibe-coding here, what we got is to follow the pattern we have in keepalived, i.e. sidecar/init container which will render the configuration for the specific node (equivalent of keepalived-monitor).

The good part is that we will only need it for bootstrap because at runtime we already have the FRRConfiguration CRD which has its own nodeSelector support.

Will update the enhancement doc soon.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address: "ipmi://192.168.111.101"
username: admin
password: password
bgpPeers:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of where this field is, it will be baremetal-specific. Do you anticipate needing per-host configs on other platforms?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sadly yes. There is nothing so much baremetal-specific here. Whoever has the BGP feature, needs the ability to configure this. Probably will make more sense to move this section somewhere else (otherwise the API will be full of copy-pasta)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked and I am not sure we should improve the current state. Not all the platforms even contain the hosts[] array, e.g. vSphere does not seem to have it. Same for platform: none.

I feel at this point it should stay in platform.baremetal.hosts[].bgpPeers and once the per-host configuration is solved in general in our API, we can piggyback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partitioned nodes. Since the API server runs on all control plane nodes, API
requests will succeed regardless of which node receives them -- the client
reaches a valid kube-apiserver instance either way. This is a transient
condition, not a data-loss scenario.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true for the API VIP, but not for the ingress VIP. We'll probably want something like the fault mode for keepalived that will prevent it from ever taking the VIP if there is no ingress service running on the node.

@mkowalski mkowalski Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. For Ingress VIP we want to have a health check. I guess that would be haproxy port? From the kubevip docs (https://kube-vip.io/docs/usage/bgp-health-check/) it seems to me this is doable. In case the check fails, we would not be announcing the route.

This needs a bit of additional tooling because kubevip on its own supports one VIP per instance. So we'd need one process for API and one process for Ingress. The one for Ingress would also need a sidecar that would be probing the haproxy port because kubevip's check works for BGP mode, but our desired mode is Routing Table Mode (so that frr-k8s does the actual BGP).

It slightly increases complexity.

@mkowalski mkowalski Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ee48126

+ we will need a patch in kubevip (but this can be downstream-only)

@mkowalski mkowalski Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the POC I ran, this is not an issue any more. Kube-vip has health-check mode (feature already merged upstream) and will only announce Ingress VIP from the node running the router.


## Graduation Criteria

### Dev Preview -> Tech Preview

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we actually planning a dev preview for this feature?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, the whole DP TP GA is just a corporate lingo but we probably go our usual TP GA unless something changes and DP is enforceable at the time when this feature comes to its existence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I retract the comment above. I actually want to do DevPreview first. Maybe we will graduate to GA directly, but the first stage will be DP and not TP

mkowalski added 4 commits June 1, 2026 10:35
…ate rendering

Replace the incorrect claim of 'per-node MachineConfig' for delivering
per-node FRR configuration with a runtime template rendering approach
using an FRR config renderer sidecar in the frr-k8s static pod.

This follows the established keepalived/baremetal-runtimecfg pattern:
MCO delivers a single, identical MachineConfig to all master nodes
containing a Go template and a JSON peer mapping file keyed by hostname.
The sidecar discovers the local node's hostname and primary IP at
startup, resolves the correct BGP peer list from the mapping, and
renders the final frr.conf.

Addresses review feedback from @cybertron regarding per-node
MachineConfig not being a thing today.
… BGP peers

Document that hosts[].bgpPeers is inherently baremetal-specific since
only baremetal has a hosts[] array. Other on-prem platforms will use
post-bootstrap FRRConfiguration CRs with nodeSelector for per-node
peer overrides.

Define BGPPeerConfig and BGPVIPConfig as reusable Go types in a shared
package, with each platform struct referencing them independently --
following the established apiVIPs/ingressVIPs/PlatformLoadBalancerType
precedent.

Add open question about the long-term cross-platform per-node peer
override strategy for non-baremetal platforms.

Addresses review feedback from @cybertron regarding baremetal-specific
API placement.
…istinction

Add a service readiness check for the ingress VIP in the kube-vip
section: kube-vip periodically probes the local haproxy stats port
(29445) and relinquishes leadership if the ingress controller is not
running, equivalent to keepalived's vrrp_script mechanism.

Update the split-brain analysis to distinguish API VIP (benign ECMP,
kube-apiserver runs on all control plane nodes) from ingress VIP
(mitigated by the readiness check withdrawing the BGP route on nodes
without a healthy ingress controller).

Addresses review feedback from @cybertron regarding ingress VIP
fault mode.
…lth check

Restructure kube-vip deployment as two separate static pods:

- kube-vip-api.yaml: manages the API VIP from bootstrap, uses the
  built-in Kubernetes API backend health check to gate route presence
  in table 198.
- kube-vip-ingress.yaml: manages the Ingress VIP post-bootstrap,
  deployed by CNO via MCO MachineConfig update. Uses a downstream
  HTTP backend health check enhancement to probe the local haproxy
  stats port (29445) instead of the Kubernetes API.

Two instances are required because kube-vip's address env var accepts
only a single IP -- there is no upstream support for multiple control
plane VIPs in one instance.

Document the downstream kube-vip enhancement: a ~50-line change that
adds a backend_health_check_url env var. When set, the Routing Table
Mode reconciliation loop performs an HTTP GET instead of Kubernetes
API discovery. This is needed because upstream Entry.Check() in
pkg/backend/backend.go is hardcoded to client.DiscoveryClient.
ServerVersion(), which only works against a real Kubernetes API server.

Update the installation workflow, static pod startup ordering,
split-brain analysis, alternatives section, and cross-team
dependencies table to reflect the two-instance model.
The design was implemented across all affected repositories and
validated end to end (github.com/mkowalski/bgp-vip-demo). Corrections:
health-gated ECMP is the actual routing-table mode advertisement model;
the CRD handover carries sessions while advertisement stays on gated
table-direct redistribution; ingress health is the router endpoint on
1936; the node kubeconfig identity is the MCO node-bootstrapper
ServiceAccount; DaemonSet avoidance is role-based (NodeRestriction
forbids the label approach); the bootstrap static pod is FRR-only; FRR
needs the 10.7 zebra import-table fix backported; the peer file schema
matches runtimecfg's FRRPeerMapping verbatim.
@openshift-ci

openshift-ci Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@mkowalski: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@yuqi-zhang yuqi-zhang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a lot - not too familiar with the networking context, so left some questions from MCO/API POV


- **Static pod manifest generation**: The installer will generate the initial
static pod manifest for frr-k8s and place it in
`/etc/kubernetes/manifests/` on the bootstrap node and initial control plane

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, this static pod needs to run on the bootstrap node itself?

For the initial control plane nodes I think it would make more sense to have the bootstrap MCO path render if, it's already doing it for the in-cluster version. I don't think we would need to explicitly have the installer generate anything for them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it on bootstrap node. Today bootstrap node runs keepalived and holds the API VIP. Tomorrow bootstrap node needs to run FRR to hold the API VIP.

Running bootstrap node without API VIP coming from FRR or keepalived would be a monster redesign.

`/etc/kubernetes/manifests/` on the bootstrap node and initial control plane
nodes. Post-bootstrap, the Machine Config Operator (MCO) owns these
manifests. MCO will render updated MachineConfig resources containing the
frr-k8s static pod manifest for control plane nodes, ensuring the manifests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note is that every update, the MCO will re-write the static pod definition before it does the other parts of the update (including pulling the OS). This has caused issues with the coredns static pod since it will immediately restart and cause some network unavailability. Do you foresee similar issues with this static pod restarting mid update?

This also means that each node will be updating this pod independently when it's their turn to update, but I assume that's fine and the old and new pods are compatible with each other.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, no incompatibility on that front. There are reboots expected during upgrade. There are network instabilities expected during upgrade. But we do the same reboots/restarts with keepalived now which runs as static pod.

provisioning path); metrics delivery will be revisited for the Tech
Preview observability criteria. Peer data reaches MCO from the
installer-generated `bgp-vip-config` ConfigMap: the bootstrap render
reads the ConfigMap manifest from the installer asset directory, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this sequence. Why is the bootstrap workflow different? Could the bootstrap-time MCO not consume the install config object and render the necessary configuration?

Also, even though we think of controllerconfig as an internal-use API, technically all OpenShift API is customer facing, so we don't really have an "internal API field"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FRR-k8s is a fully-fledge operator, not just one container/process. It owns CRDs and bunch of stuff which require k8s API.

Bootstrap FRR on the other hand needs to run before we have k8s API (because it will give node the IP address which will be used by k8s API). So it needs to be stripped-down version of FRR-k8s.

Also, even though we think of controllerconfig as an internal-use API, technically all OpenShift API is customer facing, so we don't really have an "internal API field"

Agreed with that 10/10

- CNO waits for the `FRRNodeState` CR on each node to report that the
CRD-based configuration has been applied and BGP sessions are
established.
- Once verified, CNO updates the MCO MachineConfig to remove the static

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok I think this partially answers some of my earlier questions. This workflow is technically not a supported operation (we don't allow drifts between install time and in-cluster configs). Even if there is no race condition (i.e. we're sure the MCO has done a render and completed everything before the CNO does this update) we would prefer not to have to do this operation for an immediate update.

One possible mechanism is for the bootstrap generated configs to be populated via the bootstrap ignition path instead (not in MachineConfigs, not tracked) and then have some daemon logic (?) to remove it once the MCD runs and applies the new configs. Not sure about that though - we try to ensure that the bootstrap-time MCO has all the necessary information to generate the final configs on the firstboot of the control plane nodes. Is that not possible here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to digest this comment and get back to it later. I don't really understand the "one possible mechanism [...]" way.

- "64512:100"
```

Proposed API additions to the platform-specific bare metal configuration:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these only install time configurations? If I try to switch to BGP in a running cluster is there any equivalent implementation for this? Or I guess this is only a install-time feature?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now install-time feature. In the future, maybe changeable


The per-host peer override works as follows:

- **Bootstrap phase:** MCO delivers a single, identical MachineConfig to all

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, I'm not sure this should be a machineconfig

// This field is only populated when vipManagement is "BGP".
// +openshift:enable:FeatureGate=BGPBasedVIPManagement
// +optional
BGPVIPStatus *BGPVIPStatus `json:"bgpVIPStatus,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field doesn't seem to be there in openshift/api#2923 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually outdated.

During the POC implementation I realized that we have frr-k8s CRDs BGPSessionState/FRRNodeState which hold the status.

That would mean I need CNO to continuously copy-paste status between those CRDs and BareMetalPlatformStatus.

}
```

**Phase 2 -- Sidecar renders per-node config at runtime:**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely clear, is it that:

  1. MCO bootstrap renders a general config and static pod manifests
  2. MCO in-cluster renders no config and the same static pod manifests

Or something different?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installer contains full config for every node. MCO gets this full config and places it on every node (because we don't have per-node MachineConfig). Sidecar reads the full config and renders per-node config.

This is a workaround for not being able to place different files on different nodes at install-time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants