Aller au contenu

🔀 Le routing mesh

Le routing mesh

Avant de se jeter corps et âme dans le déploiement de service, il est important d'aborder une notion importante qui est celle du routing mesh (maillage de routage). Il permet d'exposer des services (pour ceux dont un ou plusieurs ports ont été publiés) à l’exterieur de telle sorte que les ports soient accessibles depuis tous les noeuds du Swarm. Cela expliquant pourquoi je pouvais précédemment requêter apache2 sur un noeud où la tâche n'était pas exécutée.

Le routing mesh permet donc à chaque nœud du cluster Swarm d'accepter des connexions sur les ports publiés pour tout service exécuté dans le cluster, même si aucune tâche (conteneur) n'est en cours d'exécution sur le nœud en question. Le routing mesh achemine toutes les demandes entrantes vers les ports publiés sur les nœuds disponibles vers un conteneur actif.

Différents mécanismes entrent en jeu pour le routing mesh à travers plusieurs namespaces.

Les namespaces

Les namespaces (espaces de noms) réseau sont une fonctionnalité de virtualisation offerte par le noyau Linux. Ils permettent de créer des environnements réseau isolés sur une même machine. Chaque namespace réseau a ses propres interfaces réseau, ses routes, ses tables de routage et ses règles de pare-feu, indépendamment des autres namespaces ou de l'espace de noms réseau global (par défaut).

Nous allons illustrer cette explication avec notre stack apache.

Adressage IP

Les adressages IP qui seront cités sont propres à mon cluster. Il se peut que vos adressages IP soient différents.

Les namespaces réseau de base

Namespace réseau root

C'est l'espace de l'hôte (un des noeuds du cluster). Sans lancer de services, voici à quoi il ressemble :

Configuration réseau de l'hôte
root@ds01:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:d4:74:aa brd ff:ff:ff:ff:ff:ff
    inet 10.1.4.2/24 brd 10.1.4.255 scope global eth0
       valid_lft forever preferred_lft forever
3: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:f3:d5:a3:e6 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:44:1c:48:65 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
28: veth397271b@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether 0e:62:4f:cc:25:dc brd ff:ff:ff:ff:ff:ff link-netnsid 2

docker_gwbridge est un bridge virtuel. Les requêtes externes à destination des services tournant sur notre cluster Swarm n'arrivent pas directement sur lui mais sur eth0 qui, par le biais de règles de routage, va les rediriger vers ce bridge virtuel.

veth397271b@if27 est en quelque sorte un câble réseau virtuel permettant de relier le conteneur ingress_sbox au brige virtuel docker_gwbridge.

Namespace réseau ingress_sbox

Le conteneur ingress_sbox est un conteneur non visible avec les commandes docker. C'est un des piliers qui va permettre aux paquets d'arriver vers la tâche voulue. Il tourne sur chaque noeud du cluster.

Configuration réseau du namespace ingress_sbox
root@ds01:~# nsenter --net=/var/run/docker/netns/ingress_sbox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
25: eth0@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
       valid_lft forever preferred_lft forever
27: eth1@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever
Nous retrouvons notre câble virtuel précédemment cité eth1@if28 mais dans l'autre sens. @if28 correspond au numéro de l'interface 28 affiché lors de la sortie de la commande "ip -a" sur notre hôte. eth1 est donc relié au bridge virtuel docker_gwbridge. Retenez l'IP 172.18.0.2.

eth0@if26 est notre câble virtuel reliant le conteneur ingress_sbox à un réseau virtuel appelé le réseau ingress.

Namespace du réseau ingress

Le réseau ingress est un réseau de type overlay. Un réseau de type overlay permet d’étendre la connectivité entre des containers qui tournent sur des machines différentes. Il se base sur le protocole VxLAN. Un network de type overlay ne peut être créé que dans un contexte de cluster d’hôtes Docker.

Configuration réseau du namespace ingress
root@ds01:~# NAMESPACE_INGRESS="1-$(docker network ls -f name=ingress -q | cut -c 1-10)"
root@ds01:~# nsenter --net=/var/run/docker/netns/$NAMESPACE_INGRESS ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 06:02:48:68:f4:4d brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 brd 10.0.0.255 scope global br0
       valid_lft forever preferred_lft forever
24: vxlan0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default 
    link/ether 06:02:48:68:f4:4d brd ff:ff:ff:ff:ff:ff link-netnsid 0
26: veth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether a2:76:5c:d9:25:7e brd ff:ff:ff:ff:ff:ff link-netnsid 1

br0 est un bridge virtuel permettant de rattacher les conteneurs à ce réseau.

veth0@if25 est le câble virtuel permettant de relier le conteneur ingress_sbox à br0.

vxlan0 permet de faire transiter les paquets vers un autre conteneur si par exemple le conteneur visé ne tourne pas sur le noeud réceptionnant les paquets lui étant destinés. C'est la technologie VxLAN qui permet d'étendre le réseau ingress sur l'ensemble des noeuds du cluster.

Nous allons maintenant rentrer un peu plus dans le détail avec notre stack apache.

Explication avec le service apache

Lançons le service apache qui pour rappel est accessible sur le port 8080.

Observons maintenant les changements au niveau des namespaces vus plus haut.

Namespace réseau root

Configuration réseau de l'hôte
root@ds01:~# ip a
(...)
3: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:f3:d5:a3:e6 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
28: veth397271b@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether 0e:62:4f:cc:25:dc brd ff:ff:ff:ff:ff:ff link-netnsid 2
193: vethffd74a9@if192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether d6:8d:80:73:85:52 brd ff:ff:ff:ff:ff:ff link-netnsid 4

Nous constatons alors l'ajout d'un nouveau lien réseau numéroté 193 (vethffd74a9@if192) qui est en fait le câble réseau virtuel entre le conteneur tournant sur notre noeud et le bridge virtuel docker_gwbridge. Ce lien permet au conteneur d'initier des connexions réseaux vers les réseaux externes (Internet par exemple).

Cependant toute requête initiée depuis l'extérieur vers notre service (ici le port 8080) passera d'abord par le conteneur ingress_sbox comme nous le montre la sortie de la commande iptables exécutée sur notre hôte :

Règle iptables sur lenoeud hôte
root@ds01:~# iptables -t nat -nvL
(...)
Chain DOCKER-INGRESS (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.18.0.2:8080
 608K   50M RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0
L'ip 172.18.0.2 correspond à l'interface eth1 du conteneur ingress_sbox. Donc tous les requêtes initiée depuis l'extérieur à destination de l'IP de notre noeud sur le port 8080 seront redirigées par le biais du bridge virtuel docker_gwbridge vers l'interface eth1 sur le port 8080 du conteneur ingress_sbox.

C'est le même principe pour les autres noeuds, qui hébergent chacun un conteneur ingress_sbox.

Namespace réseau ingress_sbox

Observons maintenant le réseau du conteneur ingress_sbox.

Configuration réseau du namespace ingress_sbox
root@ds01:~# nsenter --net=/var/run/docker/netns/ingress_sbox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
25: eth0@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.0.0.86/32 scope global eth0
       valid_lft forever preferred_lft forever
27: eth1@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever

Une nouvelle adresse ip est apparue sur l'interface eth0 : 10.0.0.86/32. C'est l'ip virtuelle de notre service apache sur le réseau ingress. On la retrouve sur chaque ingress_box tournant sur les noeuds du cluster.

Une fois arrivés sur l'interface eth1 du conteneur ingress_sbox, les paquets à destination du port 8080 et à destination de l'ip 10.0.0.86 sont marqués avec avec une valeur particulière :

Marquages des paquets pour IPVS
root@ds01:~# nsenter --net=/var/run/docker/netns/ingress_sbox iptables -t mangle -nvL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 MARK set 0x14f

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       0    --  *      *       0.0.0.0/0            10.0.0.86            MARK set 0x14f
(...)
Ici la valeur 0x14f exprimée en hexadecimal équivaut à la valeur décimale 335. Cette valeur sera par la suite utilisée par IPVS.

Les paquets sont ensuite redirigés vers IPVS.

IPVS

IPVS (IP Virtual Server) est une fonctionnalité du noyau Linux qui implémente un équilibrage de charge au niveau IP dans le cadre du projet LVS (Linux Virtual Server). IPVS permet de distribuer le trafic réseau entrant à travers plusieurs serveurs backend, améliorant ainsi la disponibilité et la capacité de traitement des services réseau.

Table IPVS
root@ds01:~# nsenter --net=/var/run/docker/netns/ingress_sbox ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
FWM  335 rr
  -> 10.0.0.87:0                  Masq    1      0          0         
  -> 10.0.0.88:0                  Masq    1      0          0         
  -> 10.0.0.89:0                  Masq    1      0          0
Nous retrouvons ici la valeur 335. IPVS "loadbalancera" alors en mode round robin les paquets marqués 335 vers les ip des conteneurs faisant tourner apache 10.0.0.87-88-89.

Règle SNAT sur le conteneur ingress_box
root@ds01:~# nsenter --net=/var/run/docker/netns/ingress_sbox iptables -t nat -nvL
(...)
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER_POSTROUTING  0    --  *      *       0.0.0.0/0            127.0.0.11          
90481 5429K SNAT       0    --  *      *       0.0.0.0/0            10.0.0.0/24          ipvs to:10.0.0.2
(...)
Nous constatons enfin que la chaine POSTROUTING fait un SNAT des paquets à destination du range 10.0.0.0/24. Cette règle sert à modifier l'adresse IP source des paquets sortants pour les paquets gérés par IPVS qui sont destinés au sous-réseau 10.0.0.0/24.

Example

Avant SNAT : Un paquet provenant de l'adresse IP source 192.168.1.100 est acheminé par IPVS et destiné à 10.0.0.87.

Après SNAT : La règle iptables modifie l'adresse source du paquet par 10.0.0.2 avant que le paquet ne quitte l'interface réseau de l'IPVS. Ainsi, la conteneur configuré avec l'ip 10.0.0.89 voit le paquet comme venant de 10.0.0.2.

Namespace du réseau ingress

Configuration réseau du namespace ingress
root@ds01:~# NAMESPACE_INGRESS="1-$(docker network ls -f name=ingress -q | cut -c 1-10)"
root@ds01:~# nsenter --net=/var/run/docker/netns/$NAMESPACE_INGRESS ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 06:02:48:68:f4:4d brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 brd 10.0.0.255 scope global br0
       valid_lft forever preferred_lft forever
24: vxlan0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default 
    link/ether 06:02:48:68:f4:4d brd ff:ff:ff:ff:ff:ff link-netnsid 0
26: veth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether a2:76:5c:d9:25:7e brd ff:ff:ff:ff:ff:ff link-netnsid 1
195: veth17@if194: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether c2:f5:4f:04:55:41 brd ff:ff:ff:ff:ff:ff link-netnsid 2
veth0@if25 est le câble virtuel permettant de relier le conteneur ingress_sbox à br0.

veth17@if194 est le câble virtuel permettant de relier le conteneur "apache" tournant sur le noeud en question à br0. Ce dernier fait office de commutateur (switch) entre les conteneurs rattachés au réseau ingress.

Namespace réseau du conteneur apache

Voici la configuration d'un des conteneurs apache tournant sur le noeud ds01.

Configuration réseau du namespace du conteneur apache
root@ds01:~# docker ps
CONTAINER ID   IMAGE          COMMAND              CREATED       STATUS       PORTS     NAMES
5a204d827e9f   httpd:latest   "httpd-foreground"   9 hours ago   Up 9 hours   80/tcp    apache_apache.3.x5nfrvnukx7pa1pb5q49nm8e5

root@ds01:~# CONTAINER_NAME=apache_apache.3.x5nfrvnukx7pa1pb5q49nm8e5
root@ds01:~# NAMESPACE_CTN=$(docker inspect -f '{{ .NetworkSettings.SandboxKey }}' $CONTAINER_NAME)


root@ds01:~# nsenter --net=$NAMESPACE_CTN ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
190: eth0@if191: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:14:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.20.5/24 brd 10.0.20.255 scope global eth0
       valid_lft forever preferred_lft forever
192: eth2@if193: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth2
       valid_lft forever preferred_lft forever
194: eth1@if195: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:00:59 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 10.0.0.89/24 brd 10.0.0.255 scope global eth1
       valid_lft forever preferred_lft forever

eth1@if195 est le câble virtuel reliant le conteneur au réseau ingress dont l'ip est 10.0.0.89.

Les paquets arriveront donc sur cette interface sur le socket 10.0.0.89:8080 pour être ensuite redirigés vers le port 80.

Redirection du port 8080 vers le port 80 avec iptables dans le conteneur apache
root@ds01:~# nsenter --net=$NAMESPACE_CTN iptables -t nat -vnL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 REDIRECT   6    --  *      *       0.0.0.0/0            10.0.0.89            tcp dpt:8080 redir ports 80
(...)

Conclusion

Un paquet entrant sur le Swarm va naviguer entre différents network namespaces avant d'arriver à destination et être traité par le container du service constituant l'application que nous pouvons résumer avec ce schéma :

meshrouting