Guillaume Fenollar DevOps et SysAdmin Freelance

Guillaume FENOLLAR

Ingénieur Linux/DevOps Indépendant

− Montpellier −

Kube-proxy ou la magie de iptables au service de Kubernetes

Kube-proxy est un service indispensable à tout cluster Kubernetes et qui s'occupe de donner vie aux Services Kubernetes inscrits dans ETCd et les rend disponibles depuis tout le cluster, ce qui fait qu'un pod où qu'il soit peut accéder à tous services (sauf filtrage ou NetworkPolicy).

L’article qui va suivre considère que le proxy-mode utilisé par Kube-proxy est iptables, utilisé par défaut à partir de la version 1.2 de Kubernetes. Partant d’un nom de service Kubernetes qu’un pod essaie d’atteindre, le cheminement est le suivant :

  • Le service Kube-DNS (désormais pris en charge par CoreDNS à l’heure où j’écris ces lignes) retourne l’adresse IP du service
  • Le pod tente une connexion sur l’adresse IP, avec le protocole et port voulus
  • Iptables va alors gérer le cheminement jusqu’aux pods exposés par le service grâce eux Endpoints qui ont été créés par le endpoint controller

Note sur les Endpoints

Quand vous exposez un ou des pods depuis un service Kubernetes en utilisant un service, un autre type d’objet que “Service” est créé dans l’API, les “Endpoints”. Cet objet, créé et maintenu par le endpoints controller répertorie tous les couples IP de pod / port d’application correspondant à un service, quand celui-ci utilise un selector. L’exemple suivant montre l’unique endpoint créé quand un service expose un seul port, et qu’un seul pod correspond au selector

guillaume@GuiHome:~$ kubectl get services -nkube-system kubernetes-dashboard
NAME                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
kubernetes-dashboard   ClusterIP   192.168.18.202   <none>        443/TCP   111d
guillaume@GuiHome:~$ kubectl get endpoints -nkube-system kubernetes-dashboard
NAME                   ENDPOINTS            AGE
kubernetes-dashboard   192.168.34.17:8443   111d
Pour référence, voici la partie spec du service kubernetes-dashboard en question:
spec:
  clusterIP: 192.168.18.202
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard

Ce résultat d’Endpoints va bien évidemment changer en temps réel si on monte le nombre de pods ayant le label k8s-app: kubernetes-dashboard

Iptables entre dans l’arène

Jusqu’ici, nous avons donc un service avec un IP stable, et on comprends maintenant le lien vers les pods finaux grâce à l’existence des Endpoints. Mais à aucun moment un pod qui veut contacter un service va être au courant de l’existence de ces endpoints, ceux-là sont utiles à kube-proxy, qui va alors créer des règles iptables (dans sa configuration proxy-mode: iptables) qui vont mener le flux à bon port.

Remontons ensemble la chaine de ces règles de filtrage, qui

En reprenant l’exemple kubernetes-dashboard, on essaie de joindre le service à l’IP 192.168.18.202 sur le port 443. La règle iptables qui va réagir aux flux est la suivante:

Chain KUBE-SERVICES (2 references)
KUBE-SVC-XGLOHA7QRQ3V22RZ  tcp  --  0.0.0.0/0  192.168.18.202  /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:443

La chaine KUBE-SERVICES match donc et fait référence à une autre chaine personnalisée, KUBE-SVC-XGLOHA7QRQ3V22RZ. Regardons-là de plus près :

Chain KUBE-SVC-XGLOHA7QRQ3V22RZ (1 references)
target     prot opt source               destination         
KUBE-SEP-NG56X4MU77FFZ5RK  all  --  0.0.0.0/0   0.0.0.0/0  /* kube-system/kubernetes-dashboard: */

Cette chaîne définit donc le point d’accès vers le service via un nom généré, facilement reconnaissable par son “SVC” dans le nom. Une nouvelle chaîne est alors référencée, KUBE-SEP-NG56X4MU77FFZ5RK. SEP ici fait alors référence à Service EndPoint (au singulier).

Chain KUBE-SEP-NG56X4MU77FFZ5RK (1 references)
target     prot opt source               destination         
DNAT       tcp  --  0.0.0.0/0  0.0.0.0/0  /* kube-system/kubernetes-dashboard: */ tcp to:192.168.34.17:8443

On retrouve alors ici notre IP de pod 192.168.34.17, avec le port originel de l’application, 8443 (targetPort défini dans le service), et la boucle est bouclée. La couche réseau prend alors le relai via le routage aux pods qui ne sera pas abordé dans ce article, mais qui dépend de votre Network Provider (Calico, Flannel…).

Une règle d’endpoint (KUBE-SEP-*) ne concerne qu’un seul pod à la fois. Si le service expose plusieurs pods d’un coup, nous allons avoir plusieurs chaînes SEP références à partir d’une chaîne SVC, example :

Chain KUBE-SVC-FAITROITGXHS3QVF (1 references)
target     prot opt source               destination         
KUBE-SEP-TJ2CXVZOWVM6T4YV  all  --  0.0.0.0/0  0.0.0.0/0 /* kube-system/coredns:dns-tcp */ 
     statistic mode random probability 0.50000000000
KUBE-SEP-HWX34FYUV4Y7PLZG  all  --  0.0.0.0/0  0.0.0.0/0  /* kube-system/coredns:dns-tcp */

Chacune de ces deux chaînes renvoyées vont s’occuper de l’accès à un pod. On a donc ici un load balancing random (avec une probabilité de 0.5 de matcher la première chaine), je pense que vous aurez vous-même compris comment ça se passe quand on augmente notre nombre de pods au delà de deux ;)