3.5. Communication réseau
Les containers tournant sur un hôte donné étant isolés au niveau des ressources systèmes, leurs processus ne peuvent a priori pas communiquer, sauf à passer par des communications réseau. Docker fournit pour cela plusieurs possibilités de réseaux [1] :
- Les réseaux bridge, détaillés plus bas, dont le réseau par défaut
- Le réseau “host”, qui permet d’utiliser directement les fonctionnalités réseaux de l’hôte en supprimant l’isolation réseau. Cette fonctionnalité est utilisée dans des cas exceptionnels, comme par exemple lorsque l’hôte n’héberge et ne sert que pour un seul container applicatif.
- Le réseau “none”, qui sert à complètement désactiver le réseau sur un container
- Les réseaux “macvlan” et “ipvlan” que nous n’aborderons pas
- Les réseaux “overlay”, qui sont permettent d’interconnecter des containers tournant sur des hôtes différents.
Pour manipuler les réseaux, la CLI Docker offre une série de commandes “sans surprise” :
docker network ls
docker network create <name>
docker network inspect <name>
docker network remove <name>
docker network connect <network> <container>
docker network disconnect <network> <container>
Par défaut, Docker crée trois réseaux de types différents, comme le montre le screenshot ci-dessous : <figcaption>
On voit :
- un réseau de type “bridge” appelé “bridge”, qui est le réseau dans lequel sont placés les containers pour lesquels l’utilisateur n’a pas indiqué de réseau associé,
- le réseau “host”,
- et le réseau “none”, présentés plus haut.
Nous nous contenterons ici d’explorer les réseaux de type “bridge”, qui sont un moyen souple et simple de mettre en réseau des containers, en commençant par le bridge par défaut .
Le réseau “Bridge” par défaut
Lorsqu’un container est lancé sans configuration réseau spécifique, Docker va le connecter par défaut sur un réseau spécial connectant tous les containers dans ce cas de figure. Ce réseau s’appelle le “réseau Bridge”, et possède généralement le subnet 172.17.0.0/16, de gateway 172.17.0.1.
<figcaption>
Dans la figure ci-dessus, l’utilisateur a lancé une application conteneurisée contenant un serveur web et une DB. Comme il n’a rien spécifié, les deux containers se retrouvent sur le bridge par défaut. Néanmoins, ils ne sont pas nécessairement “seuls” sur ce réseau : On voit ici la présence d’un autre container lancé précédemment et peut-être oublié par l’utilisateur. Ce container peut contacter les deux autres, dont la DB, puisqu’il est sur le même subnet, et est de plus accessible de l’extérieur via une publication d’un port. Ce container constitue un risque de sécurité pour le serveur web et la DB!
Réseaux bridges personnalisés
Si l’on ne souhaite pas connecter un container à ce réseau bridge par défaut, on peut utiliser les réseaux bridge définis par l’utilisateur. Ils fonctionneront de manière similaire au réseau bridge, avec même des fonctionnalités supplémentaires, et surtout, un contrôle plus précis des containers qui s’y connecteront. De plus, cela permet de segmenter le réseau, puisque les containers situés sur un réseau bridge ne peuvent pas contacter les containers situés sur un autre réseau bridge. Notez cependant que l’hôte, lui, a toujours un accès réseau aux containers de tous les réseaux bridge, car il possède une interface dans chacun de ces derniers (vous pouvez le vérifier avec un ifconfig).
En pratique, chaque réseau Bridge Docker créé sera un nouveau Linux Bridge, auxquels les containers viendront se connecter via une paire veth (voir plus bas).
Le screencast ci-dessous illustre la création et l’utilisation d’un réseau docker de type bridge. Après avoir créé le réseau appelé “my-net”, deux containers sont créés et ajoutés à ce réseau grâce à l’option --network my-net**
. On aurait également pu créer les containers sur le bridge par défaut, puis les connecter au réseau “my-net” avec un docker network connect
.
Pour récupérer les adresses IP des containers, on peut utiliser docker inspect <container name>
, ou docker network inspect <network-name>
. Ensuite, en se connectant au container appelé c1 et en effectuant un ping, on constate que les deux containers peuvent bien se joindre.
<figcaption>
Illustration
Un exemple typique d’utilisation d’un réseau docker est la communication entre un serveur web et sa base de données : cette communication doit être absolument isolée de l’extérieur puisqu’elle sert à l’échange de données importantes. Une possibilité pour implémenter cette contrainte est d’utiliser un segment réseau dédié entre le serveur web et la DB. Ce cas de figure correspond à notre exemple illustratif, dont le schéma est repris ci-dessous. Un réseau dédié a été créé entre le serveur web et la DB. Contrairement au réseau par défaut, aucun autre container ne peut accéder à ce réseau dédié sans y avoir été explicitement connecté.
<figcaption>
Voici une série de commandes Docker permettant d’implémenter ce cas de figure. Notez que pour rendre l’exemple entièrement fonctionnel, en plus de ces commandes Docker, il faudra configurer les applicatifs web et SGBD pour qu’ils se contactent l’un l’autre. Cette connexion pourra se faire par dessus le réseau “net”, soit en utilisant les adresses IP, soit en utilisant les noms des containers, qui seront automatiquement associés aux adresses IP correspondante via la résolution de noms fournie par Docker.
docker volume create db-vol
docker network create net
docker run -d \
--name web \
-p 3000:80 \
--mount type=bind,source="$(pwd)"/src,target=/app \
--network=net
nginx
docker run -d \
--name db \
--mount source=db-vol,target=/var/lib/mysql \
--network=net \
mysql
Différences entre le réseau bridge par défaut et les réseaux créés par les utilisateurs
Même si le réseau par défaut s’avère pratique dans des cas de figure simple, il n’offre pas les mêmes fonctionnalités que les autres réseaux bridge.
Une première différence se situe au niveau de la résolution DNS : Dans le réseau par défaut, les containers ne peuvent se contacter que via leur adresse IP, à moins de manipuler leurs fichiers /etc/hosts.txt, ou d’utiliser la fonctionnalité ‘link’ qui est dépréciée.
Par contre, dans les autres réseaux bridge, les containers peuvent se contacter en utilisant leur nom, qui seront automatiquement traduit en adresse IP par Docker. Cela simplifie la gestion et permet plus d’indépendance par rapport à la configuration de l’hôte.
Les réseaux créés par l’utilisateur sont également plus sûrs, puisque seuls les containers qui y sont explicitement attachés y ont accès.
D’après la documentation Docker, les réseaux créés par l’utilisateur seraient également plus pratique, car on peut attacher/détacher un container d’un réseau bridge n’importe quand, alors que sur le réseau par défaut, cela ne pourrait s’effectuer qu’au démarrage. Notez cependant qu’en pratique, il semble possible de détacher un container du réseau par défaut par ces mêmes commandes connect/disconnect…
Implémentation Docker du réseau bridge par défaut [facultatif] [2,3,4]
L’implémentation des réseaux de type bridge repose (une fois de plus) sur des mécanismes pré-existants dans le noyau Linux, à savoir les Linux Bridges et les devices Ethernets virtuels veth. Les Linux Bridge sont des commutateurs software (virtuels) permettant de faire communiquer des interfaces réseaux. Les veth peuvent être vus comme des câbles Ethernet connectant des interfaces virtuelles.
Lorsque le Docker Engine démarre, il va créer sur la machine Linux hôte un bridge appelé Docker0, qui possède l’interface 172.17.0.1. On voit sur le screenshot ci-dessous que sur la machine hôte, il existe une interface docker0 associée à l’IP 172.17.0.1, ainsi qu’un Linux Bridge appelé justement docker0, correspondant à cette interface. On voit également qu’il existe une route pour le subnet des containers (172.17.0.0/16) avec comme gateway l’IP 172.17.0.1 du bridge docker0;
<figcaption>
Il s’agit donc bien de l’implémentation du “réseau Bridge” par défaut de Docker. Lorsqu’un container est créé, docker lui ajoute une interface réseau virtuelle, et associe un lien “veth” entre cette interface virtuelle et le Linux Bridge Docker0. De cette manière, le container et l’hôte peuvent communiquer.
De plus, via un mécanisme de NAT, le container peut également communiquer avec l’extérieur de l’hôte (l’inverse n’étant bien entendu pas possible par défaut). On voit sur l’image ci-dessous le bridge docker0, ainsi que les veth entre ce bridge et les containers. On voit aussi la couche “Linux Networking”, entre le bridge docker0 et l’interface de sortie, qui se chargera de la traduction de port/adresse nécessaire pour que les containers puissent communiquer vers l’extérieur.
<figcaption>
Dans le screenshot ci-dessous, on a démarré un container sur l’hôte Linux. Suite à cela, une nouvelle interface (identifiée par l’index 7) apparaît, commençant par “veth” : il s’agit de l’interface virtuelle du bridge docker0 qui sera connectée par un “câble virtuel” au container.
<figcaption>
Après avoir examiné les interfaces sur l’hôte, voici un aperçu des interfaces sur le container. On constate que ce dernier possède une interface “eth0@if7” : il s’agit de l’interface connectée au port “veth” du bridge0 repéré juste avant sur l’hôte (cette interface était d’ailleurs identifiée par le numéro 7). <figcaption>
Comme déjà mentionné plus haut, le réseau bridge par défaut, bien que pratique, a de nombreuses limitations et présente potentiellement des risques. A ce titre, il est considéré comme “legacy” (déprécié) par Docker, et ne doit pas être utilisé dans des environnements de production.
Bibliographie
[1] Docker, documentation des réseaux Docker, consulté en février 2023
[2] Hank Preston, Cisco, Exploring Default Docker Network, part 1, août 2022, consulté le 16/02/2023
[3] Hank Preston, Cisco, Exploring Default Docker Network, part 2, août 2022, consulté le 16/02/2023
[4] Hangbin Liu, site Red Hat Developer, An introduction to Linux bridging commands and features, 6 avril 2022, consulté le 17/2/2023