Le(s) réseau(x) de Docker Swarm, deep dive
Petite investigation sur le réseau Swarm.
Introduction
Il y a pas mal de littérature sur le réseau avec Docker et Swarm, notamment:
- L'utilisation des réseaux Overlay avec Docker dans la doc officielle
- Des articles avec des schémas pour creuser un peu plus comme celui-ci de Nigel Poulton
Mais concrètement, comment observer tout ça sur mes serveurs, dans mon terminal ? J'ai trouvé un très bon article ici mais je voulais mieux comprendre:
- Le load balancing par IPVS
- Où est effectué le port forwarding
Je vais donc explorer ça ici !
Setup
Prenons un cluster de 3 nodes swarm (parce que 1 ça n'est pas redondant, 2 ça n'est pas robuste au split, et plus de 3 c'est overkill ).
Sur lequel on a démarré créé un service web tout simple dont voici le composefile:
--- version: '3.8' services: web: image: httpd ports: - 8091:80 deploy: replicas: 2 networks: - testnet networks: testnet: attachable: true
Pour démarrer cette stack:
docker stack deploy -c docker-compose.yml webtest
Check
On vérifie que c'est bien déployé, et que ça répond:
[herve@node0 test]$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS ox7io3soug7o webtest_web replicated 2/2 httpd:latest *:8091->80/tcp [herve@node0 test]$ docker service ps webtest_web ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS mjyuczobcrb4 webtest_web.1 httpd:latest node0 Running Running 13 minutes ago klj0q37g2k22 webtest_web.2 httpd:latest node1 Running Running 13 minutes ago [herve@node0 test]$ curl 127.0.0.1:8091 <html><body><h1>It works!</h1></body></html> [herve@node0 test]$
Exploration
Les réseaux utilisés par un container:
# pour avoir l'outil "ip" herve@node0:~$ docker exec -ti webtest_web.2.klj0q37g2k22a1ar9f6sp7wai \ bash -c "apt-get update && apt-get install -y iproute2" [snip] herve@node0:~$ docker exec -ti webtest_web.2.klj0q37g2k22a1ar9f6sp7wai 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 25274: eth0@if25275: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default link/ether 02:42:0a:00:00:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.0.0.197/24 brd 10.0.0.255 scope global eth0 valid_lft forever preferred_lft forever 25276: eth2@if25277: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:12:00:0c brd ff:ff:ff:ff:ff:ff link-netnsid 2 inet 172.18.0.12/16 brd 172.18.255.255 scope global eth2 valid_lft forever preferred_lft forever 25278: eth1@if25279: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default link/ether 02:42:0a:00:05:04 brd ff:ff:ff:ff:ff:ff link-netnsid 1 inet 10.0.5.4/24 brd 10.0.5.255 scope global eth1 valid_lft forever preferred_lft forever herve@node0:~$
On a donc 3 IPs, sur 3 réseaux:
- 10.0.0.197/24 sur eth0
- 10.0.5.4/24 sur eth1
- 172.18.0.12/16 sur eth2
Sur l'autre container:
- 10.0.0.196/24 sur eth0
- 10.0.5.3/24 sur eth1
- 172.18.0.6/16 sur eth2
Comparons ça aux réseaux docker:
herve@node0:~$ docker network ls NETWORK ID NAME DRIVER SCOPE a815e8c6b042 bridge bridge local 040fa5ce7313 docker_gwbridge bridge local 918497e0a927 host host local yewz22nd9rf7 ingress overlay swarm c53d64081620 none null local i0ylvjdz6sqg webtest_testnet overlay swarm herve@node0:~$ docker network inspect --format '{{ json .Containers }}' docker_gwbridge | jq '.' { "1b2ffd90e4f770b1f6db42bd48eef81d0c9c3edfe3d6298df1e4213a54b9e43c": { "Name": "gateway_161c8a8cd5e7", "EndpointID": "8cdadbc8543d4dc343ca6b3882e26d7a12251cb16c3b651bfe7ac636b673eef6", "MacAddress": "02:42:ac:12:00:06", "IPv4Address": "172.18.0.6/16", "IPv6Address": "" }, "7d05626b82efc45c7d7834eafd55f025f8098b3e1dabe4ffd6e55754a8522e45": { "Name": "gateway_b5b17964f1aa", "EndpointID": "b5ab1d4a5f4b05a564653e3cf7132313487d74c1b6b9fb31535baf263dc8b1c5", "MacAddress": "02:42:ac:12:00:0c", "IPv4Address": "172.18.0.12/16", "IPv6Address": "" }, "ingress-sbox": { "Name": "gateway_ingress-sbox", "EndpointID": "6a6974e33a38e0c97eb35f63688dda840731a4f74da6e7fc32f2202a2281bc17", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" } } herve@node0:~$ docker network inspect --format '{{ json .Containers }}' ingress | jq '.' { "7d05626b82efc45c7d7834eafd55f025f8098b3e1dabe4ffd6e55754a8522e45": { "Name": "webtest_web.2.klj0q37g2k22a1ar9f6sp7wai", "EndpointID": "65009589cd3e3083228c27c907c8dc956dc4e58e36b168eeb6b3955ee9174e4c", "MacAddress": "02:42:0a:00:00:c5", "IPv4Address": "10.0.0.197/24", "IPv6Address": "" }, "ingress-sbox": { "Name": "ingress-endpoint", "EndpointID": "898b76a4aeb2144a60a7bd3066113e647974b0f639bb76934ac4c4ee43da68f5", "MacAddress": "02:42:0a:00:00:04", "IPv4Address": "10.0.0.4/24", "IPv6Address": "" } } herve@node0:~$ docker network inspect --format '{{ json .Containers }}' webtest_testnet | jq '.' { "7d05626b82efc45c7d7834eafd55f025f8098b3e1dabe4ffd6e55754a8522e45": { "Name": "webtest_web.2.klj0q37g2k22a1ar9f6sp7wai", "EndpointID": "619345cf52266f5b1c810ff1ccd3d8299952c226689234a13625ee62432e8eb9", "MacAddress": "02:42:0a:00:05:04", "IPv4Address": "10.0.5.4/24", "IPv6Address": "" }, "lb-webtest_testnet": { "Name": "webtest_testnet-endpoint", "EndpointID": "3b3736e50d02ef8fb8abedaf721ef57b7f58f16a17cb8fa16fe0dd98cfb5aba2", "MacAddress": "02:42:0a:00:05:05", "IPv4Address": "10.0.5.5/24", "IPv6Address": "" } } herve@node0:~$
Décryptage:
Le réseau webtest_testnet (10.0.5.0/24) est un réseau overlay spécifique à mon service. Et je n'y vois que les containers présents sur le node.
Le réseau ingress (10.0.0.0/24) est un réseau overlay commun à tous mes services (D'autres containers de services Swarn sur le même host y apparaîtraient). Je n'y vois que les containers présents sur le node, référencés par leur nom.
Le réseau docker_gwbridge (172.18.0.0/16) est un réseau bridge (= qui fait le "pont" avec le host) commun à tous mes services. Et j'y voir des containers hébergés sur les hosts voisins. Par contre les noms "gateway_xxxx" ne sont pas associables directement à un container.
Un peu de sondage avec curl
herve@node0:~$ curl 172.18.0.1:8091 <html><body><h1>It works!</h1></body></html> herve@node0:~$ curl 172.18.0.2:8091 <html><body><h1>It works!</h1></body></html> herve@node0:~$ curl 172.18.0.6:8091 curl: (7) Failed to connect to 172.18.0.6 port 8091: Connection refused herve@node0:~$ curl 172.18.0.12:8091 curl: (7) Failed to connect to 172.18.0.12 port 8091: Connection refused herve@node0:~$ curl 172.18.0.6:80 curl: (7) Failed to connect to 172.18.0.6 port 80: Connection refused herve@node0:~$ curl 172.18.0.12:80 <html><body><h1>It works!</h1></body></html> herve@node0:~$
- 172.18.0.1 est une IP portée par le host (interface docker_gwbridge). Elle permet d'accéder à mon service.
- Via 172.18.0.2, l'IP de la gateway_ingress-sbox du réseau docker docker_gwbridge, ça marche aussi.
- Par contre avec 172.18.0.6 et 172.18.0.12, les IPs des containers, ça ne marche plus sur le port exposé sur le host (8091)
- Mais sur 172.18.0.12, l'ip du container qui tourne sur le host en cours, je peux interroger le port exposé dans le container (80).
Il semble se passer des choses intéressantes dans ce réseau
iptables et namespaces réseau
Commencons par le début, ce qui arrive sur notre host
herve@node0:~$ sudo iptables -nv -L PREROUTING -t nat Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 222M 20G DOCKER-INGRESS all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL 173M 17G DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL herve@node0:~$
PREROUTING (et OUTPUT) passent les paquets dans les tables DOCKER-INGRESS et DOCKER. DOCKER pour les containers standard Docker, DOCKER-INGRESS pour Swarm.
Intéressons-nous à DOCKER-INGRESS
herve@node0:~$ sudo iptables -nv -L DOCKER-INGRESS -t nat | grep 8091 4 240 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8091 to:172.18.0.2:8091 herve@node0:~$
Le port 8091 va être redirigé vers 172.18.0.2.
Tout traffic entrant sur mon host sur le port 8091 est redirigé vers le réseau docker_gwbridge.
Nickel.
La suite se passe dans un autre namespace réseau, nommé ingress_sbox
Commençons par regarder les namespaces réseau:
herve@node0:~$ sudo ls -l /run/docker/netns total 0 -r--r--r-- 1 root root 0 Sep 11 08:01 1-i0ylvjdz6s -r--r--r-- 1 root root 0 Jul 16 13:15 1-yewz22nd9r -r--r--r-- 1 root root 0 Sep 11 08:01 b5b17964f1aa -r--r--r-- 1 root root 0 Jul 16 13:15 ingress_sbox -r--r--r-- 1 root root 0 Sep 11 08:01 lb_i0ylvjdz6 herve@node0:~$
Inventaire:
- 1-xxx correspond à un réseau overlay
- 1-i0ylvjdz6s pour le réseau docker i0ylvjdz6sqg - webtest_testnet
- 1-yewz22nd9r pour le réseau docker yewz22nd9rf7 - ingress
- lb_i0ylvjdz6 pour le load balancer du réseau i0ylvjdz6sqg - webtest_testnet
- ingress_sbox - au moins lui il a un nom parlant !
- b5b17964f1aa pour finir: spécifique à notre container. (il n'y en a qu'un, car l'autre container est sur un autre host).
On peuc retrouver l'ID du netns de notre container en l'inspectant:
herve@node0:~$ docker inspect \ --format '{{ json .NetworkSettings.SandboxKey }}' webtest_web.2.klj0q37g2k22a1ar9f6sp7wai \ | jq '.' "/var/run/docker/netns/b5b17964f1aa" herve@node0:~$
Exploration du namespace ingress_sbox
herve@node0:~$ sudo nsenter --net=/run/docker/netns/ingress_sbox \ iptables -t mangle -L PREROUTING -vn | head -n 2 Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination herve@node0:~$ sudo nsenter --net=/run/docker/netns/ingress_sbox \ iptables -t mangle -L PREROUTING -vn | grep 8091 42 2792 MARK tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8091 MARK set 0x27f2 herve@node0:~$ sudo nsenter --net=/run/docker/netns/ingress_sbox \ iptables -t mangle -L INPUT -vn | grep 27f2 0 0 MARK all -- * * 0.0.0.0/0 10.0.0.195 MARK set 0x27f2 herve@node0:~$ # 0x27f2 = 10226 en base 10 herve@node0:~$ sudo nsenter --net=/run/docker/netns/ingress_sbox \ ipvsadm | grep -A 3 10226 FWM 10226 rr -> 10.0.0.196:0 Masq 1 0 0 -> 10.0.0.197:0 Masq 1 0 0 herve@node0:~$
On voit ici que les paquets à destination de TCP:8091 sont taggés "0x27f2", ainsi que tous ceux à destination de 10.0.0.195.
Ensuite, IPVS a des règles de routage pour les paquets taggés "0x27f2", indiquant qu'il faut faire du round robin entre 10.0.0.196 et 10.0.0.197.
Les paquets sont donc transmis à chaque container du service à tour de rôle, tout en gardant le même port pour le moment (8091)
Port-forwarding
Pour terminer, le port forwarding se passe dans le namespace spécifique à chaque container:
herve@node0:~$ sudo nsenter --net=/var/run/docker/netns/b5b17964f1aa iptables -L PREROUTING -t nat -nv Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 86 5160 REDIRECT tcp -- * * 0.0.0.0/0 10.0.0.197 tcp dpt:8091 redir ports 80 herve@node0:~$
Conclusion
On a vu:
- Le routage des paquets extérieurs, du réseau host vers le docker_gwbridge
- Leur tagging vers pour utilisation sur un load-balancer IPVS
- la redirection du load-balancer vers une IP privée sur le réseau ingress
- la redirection du port exposé par Docker vers le port exposé par le service dans le container
On n'a pas vu:
- les vxlans, ou comment abstraire le fait que les containers sont sur différents hosts
- le lien entre réseaux docker et namespaces réseau
- les communications d'un container à un autre
- ...et plein d'autres choses passionnantes...
Personnellement, c'est un truc que je trouve génial avec Docker: ça repose énormément sur des mécanismes système, présents bien avant Docker.
C'est donc possible d'explorer tout ça sans lire le code source, juste en observant les serveurs.
À vous de continuer à explorer et vivement qu'on se croise à un meetup pour discuter de tout ça
Versions utilisées
- Version de Docker: 20.10.7
- Hosts: Debian 10
Commentaires en attente de modération
Ce post a 1 réaction en attente de modération...