🔀 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 :
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.
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
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.
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
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 :
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
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.
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 :
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
(...)
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.
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
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
(...)
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
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
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.
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.
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 :