Cilium L2 LoadBalancer на bare-metal k3s

Published: 2026-03-29

В облачном Kubernetes LoadBalancer-сервисы автоматически получают внешние IP от провайдера. На bare-metal k3s нет cloud controller. Cilium заполняет этот пробел с помощью L2 announcement — eBPF-альтернативы MetalLB.


Проблема

На bare-metal некому отвечать на ARP-запросы к IP LoadBalancer-сервисов:

bash$ kubectl get svc apisix -n ingress-apisix
NAME     TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)
apisix   LoadBalancer   10.96.45.23    <pending>     80:32000/TCP

<pending> — cloud controller не выделил IP.


L2 announcement в Cilium

CiliumLoadBalancerIPPool — диапазон IP:

yamlapiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
  name: dev-pool
spec:
  blocks:
    - cidr: "172.16.57.192/28"   # 14 IP для LoadBalancer dev-кластера

CiliumL2AnnouncementPolicy — какие ноды анонсируют какие сервисы:

yamlapiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  name: dev-l2-policy
spec:
  serviceSelector:
    matchLabels:
      app.kubernetes.io/managed-by: Helm
  nodeSelector:
    matchLabels:
      kubernetes.io/os: linux
  interfaces:
    - "eth0"
  externalIPs: true
  loadBalancerIPs: true

При создании LoadBalancer-сервиса Cilium выбирает IP из пула, назначает его и отвечает на ARP-запросы через eth0.


Расположение объектов

Оба объекта в spoke/:

fluxcd/projects/dev/kustomization/spoke/
├── cilium-lb-pool.yaml
├── cilium-l2-announcement.yaml
├── namespaces.yaml
└── kustomization.yaml

Применяются через dev-spoke Kustomization (не dev-apps) — это обычные Kubernetes-ресурсы, а не HelmRelease.


Yandex Cloud: L2 не нужен

На YC Managed Kubernetes (sre, loadgds, demo) внешние IP выдаёт облачный балансировщик. Объекты Cilium LB pool и L2 announcement намеренно отсутствуют в spoke-оверлеях этих окружений.


Назначить конкретный IP

yamlspec:
  values:
    service:
      loadBalancerIP: "172.16.57.193"

Без этого Cilium назначит следующий свободный IP из пула.


Предварительные требования

yamll2announcements:
  enabled: true
l2podAnnouncements:
  enabled: true
kubeProxyReplacement: true
k8sServiceHost: "API_SERVER_IP"
k8sServicePort: "6443"

k8sServiceHost обязателен при использовании kube-proxy replacement — Cilium напрямую обращается к API-серверу.


Проверка

bash# Статус пула
kubectl get ciliumloadbalancerippool -A

# Выделенные IP
kubectl get svc -A -o wide | grep LoadBalancer

# Политики L2-анонса
kubectl get ciliuml2announcementpolicy -A

# Детали пула с выделениями
kubectl describe ciliumloadbalancerippool dev-pool

После создания LoadBalancer-сервиса внешний IP появляется в течение нескольких секунд:

bashNAME     TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
apisix   LoadBalancer   10.96.45.23    172.16.57.193    80:32000/TCP

Cilium vs MetalLB

MetalLB был стандартом до появления нативного L2 в Cilium. Если Cilium уже отвечает за CNI и kube-proxy replacement, добавлять MetalLB — значит держать два сетевых контроллера на каждой ноде.

Cilium L2 MetalLB
Протокол ARP/NDP ARP/NDP или BGP
BGP CiliumBGPPeeringPolicy Да, нативно
Интеграция Часть Cilium Отдельный компонент
Замена kube-proxy Тот же компонент Отдельный