Networking avec Docker (1/3)
Lundi 22 Août 2016 à 03:41

Bonjour,

Pour ceux qui utilisent Docker régulièrement, je doute que la présente vous apprenne beaucoup de choses. Et puis, ce qu’il y a de bien avec Docker c’est qu’ils ont déjà une super documentation, même si tout le monde ne la lit pas. Je l’ai appris à mes dépends avec un autre stagiaire (“J’ai fait un EXPOSE 8888 mais il a pas ouvert le port sur mon localhost je comprends paaaaaaaaaaas” -> RTFM).

Au programme: les bases des links/expose/publish, pour que tout le monde parte sur de bonnes bases pour la suite.

Préambule

Docker vient avec trois réseaux prédéfinis (bridge, none, host) mais pour une utilisation classique, le bridge suffit amplement, celui qui est utilisé par défaut. -> Tous les conteneurs lancés seront sur bridge, à moins que vous n’ayez des besoins spécifiques auquel cas il faudra spécifier clairement ce que vous voulez faire.

  • Un conteneur sur none n’aura pas accès au réseau, seulement à localhost
  • Un conteneur lancé sur host aura accès à toutes les interfaces et connexions de l’hôte. C’est plus efficace car il y a une couche de virtualisation en moins, mais plus dangereux si votre conteneur est ouvert aux autres.

Niveau interfaces réseau premièrement, vous avez une jolie interface docker0 qui est créée (à l’installation si je ne me plante pas) et qui est là peu importe que le démon soit lancé ou pas. Lorsqu’un conteneur est créé, il va se retrouver avec une interface eth0, tandis que l’hôte va voir poper une nouvelle interface vethxxxxxxx. Et vous avez un conteneur qui tourne en 172.17.0.2, dont vous êtes la gateway (172.17.0.1). Pas de quoi casser trois pattes à un canard, mais ce sont les bases.

Avec un certain nombre de conteneurs sur sa machine, on a donc plein d’interfaces veth (logique).

Une petite commande pour supprimer tous les conteneurs qui tournent, des fois qu’il y en ai beaucoup:

docker rm -f $(docker ps -a -q)

Les relations c’est important

Maintenant, si vous avez un minimum de choses à faire avec, vous allez vouloir nommer le conteneur, par exemple si vous avez besoin de connecter des conteneurs entre eux (link). Vous pouvez toujours lier des conteneurs via leur id (les quatre premiers caractères suffisent), mais ce n’est pas super pratique.

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
f3819bd7dafb        mariadb             "docker-entrypoint.sh"   6 minutes ago       Up 6 minutes        3306/tcp            maria

J’ai donc un conteneur mariadb, que que j’ai nommé maria. Il tourne actuellement en 172.17.0.2, on peut le voir avec docker network inspect bridge. Si je veux lui parler depuis un autre conteneur, je utiliser l’IP directement dans mon autre conteneur. (Sur une échelle de saleté, c’est équivalent à $goto = "label"; exec("goto $goto);). Sinon, la solution reste le lien:

docker run --name web --link maria:maria -d web

la syntaxe est --link <conteneur source>:<nom de destination>

Et vous pourrez vous connecter à l’uri mysql://user:pass/maria:3306/ma-db

Si vous vouler changer le nom pour vous connecter à mysql://user:pass/unicorn:3306/ma-db, c’est possible aussi avec docker run --name web --link maria:unicorn -d web Rien de magique là dedans, docker rajoute une ligne dans /etc/hosts à la création du conteneur web:

172.17.0.2	f381 f3819bd7dafb maria

Avec l’id maintenant:

docker run --name f381 --link maria:maria -d web

Avec cette méthode, le lien est toujours uni-directionnel: web peut parler à maria, mais maria ne peut pas parler à web (pas avec juste son nom).

Pour faire un lien bi-directionnel, ce n’est pas possible sur le réseau par défaut (bridge) pour des raisons de compatibilités (selon la doc). Cependant, pour des réseaux créés par l’utilisateur, il y a une vraie résolution DNS, et pas une ligne ajoutée dans /etc/hosts. Le daemon se débrouille pour maintenir à jour un DNS pour les conteneurs (plus d’infos en partie 3).

Donc, Les instructions suivantes marchent:

docker network create --driver bridge my-network
docker run -ti --name maria -e MYSQL_ROOT_PASSWORD=secret --link web:web --net my-network -d mariadb
docker run -ti --name web --link maria:maria --net my-network -d web

Et voilà, un lien bi-directionnel !


Note: Sur création d’un réseau défini par l’utilisateur, docker créé une règle iptables MASQUERADE correspondant au subnet englobé par ce réseau.


Exposition de ports

Reste une question épineuse: les ports. Et c’est la qu’il faut trouver la bonne documentation. Pour exposer un port, il faut rajouter une directive dans le dockerfile pour rajouter le bon port avec le bon protocole (TCP par défaut):

EXPOSE 3306/tcp
EXPOSE 3306/udp

Il est aussi tout à fait possible d’exposer des ports sur lesquels le conteneur n’écoute pas, ce qui est relativement logique.

Side-effect des liens: Docker créé des variables d’environnement pour les ports exposés, pour maria, depuis web: $MARIA_PORT, $MARIA_PORT_3306_TCP, $MARIA_PORT_3306_TCP_ADDR, $MARIA_PORT_3306_TCP_PROTO (elles sont deprecated btw)


Tip: On récupère aussi les variables d’environnements déclarées pour le conteneur: Mariadb a été run en spécifient la variable MYSQL_ROOT_PASSWORD (qui est mandatory), et on y a accès depuis web avec $MARIA_ENV_MYSQL_ROOT_PASSWORD


Plus qu’à publier

Maintenant que les ports sont exportés, on peut tout à fait les publier -> l’hôte ouvre un port qui redirige vers le conteneur. Mettons que notre conteneur web exporte un port 80:

docker run --name web --link maria:maria -p 80:80 -d web

Et on peut aller voir tout ça sur http://localhost. Autres possibilités, changer le port de l’hôte, spécifier le protocole, l’adresse d’écoute.

docker run --name web --link maria:maria -p 5000:80 -d web // http://localhost:5000)
docker run --name web --link dns:dns -p 53:53/udp -d web
docker run --name maria -p 127.0.0.1:3306 -d mariadb

Plus d’exemples sur la documentation. A noter que la syntaxe est différente de link: Si aucun port hôte n’est spécifié, docker en ouvre un au hasard dans la moitié supérieure. Si aucune adresse de binding n’est spécifiée, docker écoute sur le monde (!!)

On peut aussi publier des ports qui ne sont pas exposés.


Bref résumé:

  • Possibilité d’exposer un port via un EXPOSE dans le Dockerfile
  • Possibilité de publier le port d’un conteneur sur l’hôte via le flag -p (et -P, cf doc)
  • Possibilité de publier un port NON EXPOSÉ sur l’hôte via le flag -p (et -P, cf doc)

Légère subtilité

Et là on commence à s’amuser: retour à la documentation d’EXPOSE.

The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.

Sachant que l’on peut publier un port qui n’est pas exposé, on pourrait penser que tout port qui n’est pas spécifiquement exposé est inaccessible, même depuis les autres conteneurs, sinon pourquoi faire une directive qui créé juste des variables d’environnement ?

Dit autrement: - Je peux publier le port NON EXPOSÉ d’un conteneur sur l’hôte. - Si je peux accéder au port NON EXPOSÉ d’un conteneur depuis un autre, à quoi donc sert la directive ‘EXPOSE’ ?

Vérification rapide avec netcat, sur nos deux conteneurs maria et web:

docker exec -ti maria /bin/bash
apt-get update && apt-get install -y netcat
nc -l -p 1234

docker exec -ti web /bin/bash
apt-get update && apt-get install -y netcat
nc maria 1234

… Et ça marche.

Et là, ça parait assez contre-intuitif quand même comme résultat, d’autant que la documentation sur EXPOSE est assez succinte.

Résolution

La réponse est un petit peu cachée: Modifier le fichier d’environnement du daemon pour rajouter les options --icc=false et --iptables=true au démarrage. Sans cela, docker ne fait pas de vérification d’accès.

Fichier /etc/sysconfig/docker avant:

OPTIONS='--selinux-enabled --log-driver=journald'

Fichier /etc/sysconfig/docker après:

OPTIONS='--selinux-enabled --log-driver=journald --icc=false --iptables=true'

Pour être sur que c’est le bon fichier que l’on modifie: systemctl show docker | grep EnvironmentFile, et vérification du contenu de docker.service: systemctl show --property FragmentPath docker

Concrètement, que’est ce que ça a changé ?

Au lieu de dire “Tout le monde peut se connecter chez tout le monde, je vérifie rien du tout”, on voit de jolies règles iptables ACCEPT, et docker drop tout le reste.

ACCEPT     tcp  --  172.17.0.3           172.17.0.2           tcp dpt:3306
ACCEPT     tcp  --  172.17.0.2           172.17.0.3           tcp spt:3306

Plus possible pour les conteneurs de se parler sans avoir une autorisation préalable (EXPOSE). A noter que l’hôte peut toujours parler à ses conteneurs, de même qu’il a toujours la liberté de publier des ports qui ne sont pas exposés, mais ils ne seront pas non plus accessibles aux autres conteneurs.

~

Cette première partie est maintenant terminée. Rien de très difficile ni technique, ce sont juste les bases pour travailler avec quelques conteneurs dockers. Rendez-vous dans la deuxième partie pour voir comment marche docker-compose et la création de nos premires networks.


Back to posts