BGP on Cilium: Peering Kubernetes with a Leaf‑Spine Datacenter
BGP is not just the routing protocol that powers the Internet — it has become the standard control plane inside modern data centers.
Today’s data centers are typically built using a leaf–spine architecture, where BGP is responsible for distributing reachability information between racks, spines, and endpoints. And when your endpoints are Kubernetes Pods, it makes perfect sense for Kubernetes networking to speak BGP as well.
That’s exactly where Cilium comes in.
In this post, we’ll walk through a hands‑on lab where we enable BGP on Cilium, peer Kubernetes nodes directly with a virtual leaf–spine fabric, and verify real end‑to‑end Pod connectivity across racks.
Lab Overview
In this lab we build a small but realistic virtual data center:
A core router (spine)
Two Top‑of‑Rack (ToR) switches
A Kubernetes cluster with:
1 control‑plane node
3 worker nodes
Nodes logically split across two racks
Cilium as the CNI, running in native routing mode
BGP peering between Kubernetes nodes and ToR switches
The goal is simple:
Kubernetes Pods in different racks should be reachable using routes learned dynamically via BGP.
Why BGP with Cilium?
Cilium’s BGP support allows Kubernetes nodes to advertise Pod CIDRs directly into your data center fabric.
That means:
No overlays required
No static routes
No NAT between racks
Your DC fabric becomes Pod‑aware
With the BGP v2 control plane (introduced in Cilium 1.16), this is configured entirely via Kubernetes CRDs — clean, declarative, and GitOps‑friendly.
Topology
At a high level, the topology looks like this:
A spine router peers with two ToR switches
Each ToR switch peers with Kubernetes nodes in its rack
Kubernetes nodes advertise their Pod CIDRs using BGP
Each rack maps to its own ASN:
Rack 0 → AS 65010
Rack 1 → AS 65011
Core → AS 65000
This mirrors how real data centers are commonly built.
Kubernetes Cluster Setup (Kind)
We deploy Kubernetes using kind, with CNI disabled so that Cilium can be installed manually.
❯ kubectl rollout status ds/netshoot -w
daemon set"netshoot" successfully rolled out
❯ k get pods
NAME READY STATUS RESTARTS AGE
netshoot-ffssl 1/1 Running 0 95s
netshoot-q7l9l 1/1 Running 0 95s
netshoot-rnm8n 1/1 Running 0 95s
We then:
Pick a source Pod in rack0
Pick a destination Pod in rack1
Ping across racks
❯ SRC_POD=$(kubectl get pods -o wide | grep "kind-worker "| awk '{ print($1); }')❯ DST_IP=$(kubectl get pods -o wide | grep worker3 | awk '{ print($6); }')❯ kubectl exec -it $SRC_POD -- ping -c 10$DST_IPPING 10.1.1.142 (10.1.1.142) 56(84) bytes of data.
64 bytes from 10.1.1.142: icmp_seq=1ttl=58time=0.235 ms
64 bytes from 10.1.1.142: icmp_seq=2ttl=58time=0.149 ms
64 bytes from 10.1.1.142: icmp_seq=3ttl=58time=0.284 ms
64 bytes from 10.1.1.142: icmp_seq=4ttl=58time=0.188 ms
^C
--- 10.1.1.142 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3097ms
rtt min/avg/max/mdev = 0.149/0.214/0.284/0.050 ms
❯ kubectl exec -it $SRC_POD -- traceroute $DST_IPtraceroute to 10.1.1.142 (10.1.1.142), 30 hops max, 46 byte packets
1 10.1.3.115 (10.1.3.115) 0.008 ms 0.064 ms 0.008 ms
2 10.0.2.1 (10.0.2.1) 0.008 ms 0.009 ms 0.009 ms
3 10.0.0.0 (10.0.0.0) 0.109 ms 0.009 ms 0.008 ms
4 10.0.0.2 (10.0.0.2) 0.007 ms 0.009 ms 0.008 ms
5 10.0.4.2 (10.0.4.2) 0.008 ms 0.009 ms 0.008 ms
6 * * *
7 10.1.1.142 (10.1.1.142) 0.009 ms 0.009 ms 0.008 ms
And… success 🎉
Packets traverse:
Pod → Node → ToR → Spine → ToR → Node → Pod
All driven by BGP‑learned routes.
What We Achieved
By the end of this lab, we have:
A Kubernetes cluster integrated directly into a DC fabric
Dynamic Pod CIDR advertisement via BGP
Rack‑aware routing using node labels
No overlays, no tunnels, no hacks
This is exactly how Kubernetes networking should look in a modern data center.
Final Thoughts
Cilium’s BGP support is a huge step forward for:
Bare‑metal Kubernetes
On‑prem data centers
Hybrid cloud networking
If your network already speaks BGP — and it almost certainly does — Cilium lets Kubernetes become a first‑class citizen of that network.