Skip to content

Partage IP entre services avec Cilium Load Balancer

J’avais un besoin relativement simple, je voulais pouvoir exposer un service Kubernetes sur une VIP en mode LoadBalancer.
VIP gérée par Cilium bien sûr. J’ai une CNI suffisamment outillé pour ne pas avoir à ajouter d’autres choses. Non mais !

Cette opération peut être quand même super pratique pour plein de raisons :

  • Un serveur DNS hébergé dans Kubernetes (et pourquoi pas après tout ?)
  • Un collecteur de logs type rsyslog
  • Un service Web qui serait à même de gérer du protocole QUIC (HTTP/3) et HTTP(s)
  • Tout ce qui peut nécessiter une double stack protocolaire…

Pour poser un exemple concret, et surtout un cadre facile à déployer pour faire mes tests, je ne suis pas allé chercher très loin. CoreDNS comme serveur DNS répond largement au besoin de ce test.

Donc l’idée est simple : je veux pouvoir depuis l’ensemble de mes assets hors de mon cluster Kubernetes, faire des requêtes DNS vers le CoreDNS que je déploie pour mes tests. (Oui, je laisse le CoreDNS interne de Kubernetes tranquille.)

Pour rappel, le DNS est assez classiquement utilisé en mode UDP, mais il est aussi possible de le faire en mode TCP, et surtout, c’est nécessaire dans certains contextes comme le cas où il y a beaucoup d’enregistrements à retourner et que ça ne rentre pas dans un paquet UDP.

Donc voilà, je veux exposer mon service DNS sur une IP externe à mon cluster, et que celle-ci soit aussi bien accessible en UDP qu’en TCP.

Et c’est là que ça se complique.

Par défaut, et sauf si j’ai raté quelque chose, il n’est pas possible de positionner un service Kubernetes de type LoadBalancer en écoute sur le même port (dans mon cas le 53) et mode UDP ET TCP.

Je ne vais pas vous détailler l’installation de CoreDNS à coup de chart Helm, ce n’est pas le but. On considère donc que tout ce qu’il nous faut en termes de déploiement et de pod est déjà en place et que tout le monde vas bien.

Tout d’abord, il faut créer une VIP pour pouvoir adresser le service de CoreDNS en mode LoadBalancer.

Pour faire ça, on va chercher dans les classiques CiliumLoadBalancerIPPool pour la configuration de la VIP et CiliumL2AnnouncementPolicy pour traiter l’annonce ARP afin qu’elle soit vu sur le réseau :

apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
name: "cdns-vip"
namespace: coredns-external
spec:
blocks:
- cidr: "192.168.0.153/32"
serviceSelector:
matchLabels:
"io.kubernetes.service.namespace": "coredns-external"
"cilium.lbpool/name": "coredns-external

Bon, jusqu’ici, ça reste du classique au niveau de la configuration de la VIP avec Cilium.
On indique quelle IP on veut, et quels sont les labels permettant de l’attacher au bon service.

Eh bien, c’est là que ça se corse un peu (mais vraiment pas beaucoup).

En faisant mes recherches, je suis en premier lieu tombé sur la façon de faire avec MetalLB pour permettre du partage d’adresse IP via une annotation sur les services : metallb.io/allow-shared-ip

Et là, je me suis dit « attends… MetalLB sait le gérer, à quel moment Cilium n’en serait pas capable ? ».

Du coup, c’est parti, je fouille la doc… Et là… C’est la révélation, et c’est affligeant de simplicité.

Je vous mets le lien vers la doc de Cilium qui traite du sujet ici.

En fait, de façon triviale, Cilium traite la chose exactement de la même façon que ce que fait MetalLB. Soit via une simple annotation sur le service (lbipam.cilium.io/sharing-key) !

Alors quand même, il faut en parler un minimum. Parce qu’il y a plusieurs choses à prendre en comptes :

  • Par défaut, une même adresse ne peut pas être partagée entre plusieurs namespaces
  • Il est impératif que la valeur fournie dans l’annotation soit commune à l’ensemble des services qui partageront la même VIP

Bref, j’ai trouvé mon bonheur, c’est le moment de tester ça.

Je crée donc deux services pour pouvoir exposer mon fameux port 53 sur une seule et même adresse IP :

apiVersion: v1
kind: Service
metadata:
annotations:
lbipam.cilium.io/sharing-key: "cdnsext1"
labels:
app.kubernetes.io/instance: coredns
app.kubernetes.io/name: coredns
cilium.lbpool/name: "coredns-external"
name: cdns-lb-udp
namespace: coredns-external
spec:
type: LoadBalancer
ports:
- name: udp-53
port: 53
protocol: UDP
targetPort: 53
selector:
app.kubernetes.io/instance: coredns
app.kubernetes.io/name: coredns

Et une fois appliqué ça donne ça :

Terminal window
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
...
cdns-lb-tcp LoadBalancer 10.101.9.103 192.168.0.153 53:31251/TCP 7h37m
cdns-lb-udp LoadBalancer 10.104.45.49 192.168.0.153 53:31716/UDP 7h37m
...

On voit bien qu’on a nos deux services qui écoutent sur la même External IP.

Bon, ce n’est pas tout ça, mais si on s’assurait que ça fonctionne bien quand même ?

On teste en mode TCP :

Terminal window
dig -t A +tcp blog.fricouv.eu @192.168.0.153

Et on obtient la réponse suivante :

; <<>> DiG 9.20.18-1~deb13u1-Debian <<>> -t A +tcp blog.fricouv.eu @192.168.0.153
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51368
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 083e5e3c94d7ae61 (echoed)
;; QUESTION SECTION:
;blog.fricouv.eu. IN A
;; ANSWER SECTION:
blog.fricouv.eu. 30 IN CNAME obelix.fricouv.eu.
obelix.fricouv.eu. 30 IN A 38.242.215.124
;; Query time: 52 msec
;; SERVER: 192.168.0.153#53(192.168.0.153) (TCP)
;; WHEN: Wed Apr 01 16:56:20 UTC 2026
;; MSG SIZE rcvd: 135

En mode TCP, c’est OK !

En UDP maintenant :

Terminal window
dig -t A +udp blog.fricouv.eu @192.168.0.153

Et on obtient la réponse suivante :

; <<>> DiG 9.20.18-1~deb13u1-Debian <<>> -t A +udp blog.fricouv.eu @192.168.0.153
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27637
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: d3884161ae8965e8 (echoed)
;; QUESTION SECTION:
;blog.fricouv.eu. IN A
;; ANSWER SECTION:
blog.fricouv.eu. 30 IN CNAME obelix.fricouv.eu.
obelix.fricouv.eu. 30 IN A 38.242.215.124
;; Query time: 0 msec
;; SERVER: 192.168.0.153#53(192.168.0.153) (UDP)
;; WHEN: Wed Apr 01 17:00:26 UTC 2026
;; MSG SIZE rcvd: 135

Et voilà le travail ! J’ai enfin trouvé comment exposer mes services sur une même IP externe et sur le même port, quel que soit le protocole que j’utilise.