Partage IP entre services avec Cilium Load Balancer
Le Contexte du pourquoi ?
Section titled “Le Contexte du pourquoi ?”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…
Objectif et le but
Section titled “Objectif et le but”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.
Comment ça se passe alors ?
Section titled “Comment ça se passe alors ?”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.
La VIP pour CoreDNS
Section titled “La VIP pour CoreDNS”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: CiliumLoadBalancerIPPoolmetadata: name: "cdns-vip" namespace: coredns-externalspec: blocks: - cidr: "192.168.0.153/32" serviceSelector: matchLabels: "io.kubernetes.service.namespace": "coredns-external" "cilium.lbpool/name": "coredns-externalapiVersion: "cilium.io/v2alpha1"kind: CiliumL2AnnouncementPolicymetadata: name: cdns-vip namespace: coredns-externalspec: serviceSelector: matchLabels: "io.kubernetes.service.namespace": "coredns-external" "cilium.lbpool/name": "coredns-external" externalIPs: false loadBalancerIPs: trueBon, 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.
Et le partage du coup ?
Section titled “Et le partage du coup ?”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: v1kind: Servicemetadata: 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-externalspec: type: LoadBalancer ports: - name: udp-53 port: 53 protocol: UDP targetPort: 53 selector: app.kubernetes.io/instance: coredns app.kubernetes.io/name: corednsapiVersion: v1kind: Servicemetadata: 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-tcp namespace: coredns-externalspec: type: LoadBalancer ports: - name: tcp-53 port: 53 protocol: TCP targetPort: 53 selector: app.kubernetes.io/instance: coredns app.kubernetes.io/name: corednsEt une fois appliqué ça donne ça :
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE...cdns-lb-tcp LoadBalancer 10.101.9.103 192.168.0.153 53:31251/TCP 7h37mcdns-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 :
dig -t A +tcp blog.fricouv.eu @192.168.0.153Et 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: 135En mode TCP, c’est OK !
En UDP maintenant :
dig -t A +udp blog.fricouv.eu @192.168.0.153Et 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: 135Et 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.