Demonstrates Cilium L2 announcement to assign external IPs to LoadBalancer services and have nodes answer ARP in a local Kind Kubernetes cluster.
This guide demonstrates Cilium’s L2 announcement feature and how it enables nodes to respond to ARP requests for LoadBalancer external IPs in a local Kubernetes cluster (Kind is used in this demo). The goal is to show how to assign external IPs to LoadBalancer services and have nodes announce those IPs on the host subnet so clients can reach services via ARP.
Goal: Assign external IPs from a node subnet to LoadBalancer services and have selected nodes respond to ARP for those IPs using Cilium L2 announcements.
Key CRDs used:
CiliumLoadBalancerIPPool: allocate external IPs for LoadBalancer services.
CiliumL2AnnouncementPolicy: control which nodes/interfaces respond to ARP for which services.
Before installing Cilium, the cluster nodes may appear NotReady because the CNI is missing:
Copy
kubectl get nodeNAME STATUS ROLES AGE VERSIONmy-cluster-control-plane NotReady control-plane 30s v1.32.2my-cluster-worker NotReady <none> 19s v1.32.2my-cluster-worker2 NotReady <none> 18s v1.32.2
To enable L2 announcements, enable both the L2 announcement feature and kube-proxy replacement in Cilium. A typical Helm upgrade/install invocation looks like this:
When enabling kube-proxy replacement, provide the API server host and port (k8sServiceHost and k8sServicePort) so Cilium can reach the Kubernetes API. You can also edit the chart values file and set these keys before installing via Helm.
A good workflow is to fetch the default chart values, edit them, and then install:
LAST DEPLOYED: Wed Jun 4 23:58:52 2025NAMESPACE: kube-systemSTATUS: deployedREVISION: 1NOTES:You have successfully installed Cilium with Hubble.Your release version is 1.17.2.
2. Deploy two simple applications with LoadBalancer services
Create a manifest named apps-and-svcs.yaml which deploys two HTTP echo applications and exposes each with a Service of type LoadBalancer.apps-and-svcs.yaml:
3. Provide external IPs to LoadBalancer services with Cilium IPAM
Create a CiliumLoadBalancerIPPool so Cilium can allocate external IPs for your services from an address block on your node subnet. In this demo the node subnet is 172.19.0.0/16 and we pick a small range:ipam.yaml:
Because these external IPs are on the same subnet as the host nodes, clients on that subnet will attempt to reach them via ARP. If no node responds to ARP for those IPs, traffic will not reach your services (curl will time out).Example failing request before L2 announcement is configured:
After the L2 announcement policy is active, clients on the same subnet can reach the LoadBalancer external IPs:
Copy
curl 172.19.0.240curl 172.19.0.241# "This is app2"
Verify the local ARP table shows the service IP entries mapped to the node MAC addresses:
Copy
arp -a | egrep "172.19.0.24[01]"# ? (172.19.0.240) at 02:42:ac:13:00:04 [ether] on br-2c6b6be1a367# ? (172.19.0.241) at 02:42:ac:13:00:03 [ether] on br-2c6b6be1a367
In Kind-based setups the MAC addresses correspond to the Docker/Kind bridge interfaces for node containers. You can inspect the node container interfaces to confirm which node IP/MAC answered ARP. Example:
Copy
docker ps# CONTAINER ID IMAGE NAMES# ... my-cluster-worker2# ... my-cluster-workerdocker exec my-cluster-worker2 ip addr show eth0# ... inet 172.19.0.4/16 ...# link/ether 02:42:ac:13:00:04
The link/ether value should match the ARP mapping for the external IP owned by that node.