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

Cette troisième partie va présenter quelques options un peu plus avancée des networks avec compose, puis on s’intéressera au fonctionnement interne de tout ce schmilblick.

Un mot sur les drivers

Au tout début, j’avais parlé des trois réseaux shippés par docker: bridge, none et host. Ils sont à différencier des drivers qui viennent avec docker et que l’on doit spécifier à la création d’un réseau (bridge, overlay).

Le sujet de cette série est d’apprendre à faire un réseau ad hoc sur une machine de dev. Or, le driver overlay est utilisé pour faire des réseaux multi-hôtes –> clusters. C’est pour cela que nous avons utilisé le driver par défaut précédemment (bridge, à ne pas confondre avec le réseau du même nom).

Il y a aussi les drivers macvlan et ipvlan qui vont bientot faire leur apparition (pour l’instant ils sont expérimentaux selon la branche master) et qui sont documentés ici pour les intéressés. Comme leur nom l’indique, ils vont permettre de faire des VLANs en couche 2 et 3.

Enfin, il est aussi possible d’utiliser des plugins pour aider à manager son réseau. Il y en a actuellement trois qui sont officiellement listés sur la documentation: Contiv, Kuryr et Weave.

Un peu de customisation

On a déjà vu que l’on pouvait dire à docker-compose de créer des networks à la volée. Maintenant ça peut être intéressant d’avoir une configuration un peu plus fine du réseau pour ce que l’on veut faire.

Pour cela, on va reprendre notre master précédent, et on va un peu lui customiser son Dockerfile pour illustrer ces fonctionnalités.

Première chose: la section ipam du réseau. Après tout, on utilise seulement trois services, mais compose leur attribue un subnet /16. Moi j’appelle ça de l’overkill.

Donc, notre fichier devient:

cat docker-compose-master.yml

version: '2'
services:
    service_entry:
        image: exper
        expose:
            - "443"
        hostname: foo
        networks:
            internal:
                aliases:
                    - entry
    service_2:
        image: exper
        networks:
            - internal
    service_3:
        image: exper
        networks:
            - internal

networks:
    internal:
        ipam:
            driver: default
            config:
                - subnet: 172.21.0.0/29
                  ip_range: 172.21.0.0/30
                  gateway: 172.21.0.4

Bon, je crois qu’il n’y a pas besoin de plus de détails, le nom des paramètres est auto-suffisant. Après avoir lancé nos conteneurs, on observe notre réseau, et on voit qu’il a pris les paramètres qu’on lui avait donné en configuration. Surprenant !

L’interface réseau de l’hôte:

ip route
...
172.21.0.0/29 dev br-e56e5f920a3d  proto kernel  scope link  src 172.21.0.4
...

Et la configuration de master_internal:

docker network inspect master_internal:

{ blabla,
	"Driver": "bridge",
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.21.0.0/29",
                    "IPRange": "172.21.0.0/30",
                    "Gateway": "172.21.0.4"
                }
            ]
        }
	},
blabla}

Warning: Si un réseau du même nom est déjà présent (comme master_internal qui avait déjà été créé), il faut le supprimer pour que docker-compose le recréé avec la bonne configuration, sinon il n’y aura pas de changement dans son comportement.


Parmi les autres fonctionnalités possibles, on peut aussi spécifier des adresses auxiliaires dans la configuration de l’IPAM. Cela dit, elle ne sont pas utilisées pour la résolution DNS, et je n’ai pas pu trouver d’exemples attestant leur utilité. Donc j’ai laissé de côté.

Enfin parmi les options qui peuvent être utiles lors de la définition d’un service: - Possibilité de set le hostname, domain name via hostname et domainname - Possibilité de set l’adresse mac du conteneur via mac_address - Possibilité de set un/des serveurs DNS à interroger via l’option dns. On verra comment docker s’y prend dans la partie suivante. - Possibilité de set l’adresse ipv4 et/ou ipv6 du conteneur via les directives ipv4_address et ipv6_address

Bref, il y a pas mal d’options qui permettent de gérer son réseau un peu comme on l’entend, mais uniquement à la création du conteneur (on reviendra sur cette déclaration).

A la recherche du DNS perdu

On arrive là à un point sur lequel je me suis beaucoup interrogé en rédigeant cette série: À quel endroit se situe ce foutu DNS ?

La documentation fait référence à un DNS pour le réseau bridge par défaut, pour les réseaux définis par l’utilisateur, dans la ligne de commande docker et dans docker-compose.

A chaque fois les informations se recoupent, se croisent, à tel point que l’on peut avoir l’impression de lire tout le temps la même chose. Autant je ne sais pas si God is in the details mais la fourberie s’y trouve assurément.

J’ai donc commencé à investiguer pour trouver le DNS de docker. Nmap, netstat, wireshark, tcpdump, bref les classiques. Aucun moyen de mettre la main sur ce DNS, qui semble du coup faire une résolution sortie de nul part.

Finalement, j’ai fini par grep -R le code source, et quelques rebonds plus tard voici sur quoi je suis tombé juste avant la définition de l’interface Resolver:

github.com/docker/libnetwork/resolver.go

// Resolver represents the embedded DNS server in Docker. It operates
// by listening on container's loopback interface for DNS queries.
type Resolver interface {
	// Start starts the name server for the container
	Start() error
	// Stop stops the name server for the container. Stopped resolver
	// can be reused after running the SetupFunc again.
	Stop()
	// SetupFunc() provides the setup function that should be run
	// in the container's network namespace.
	SetupFunc() func()
	// NameServer() returns the IP of the DNS resolver for the
	// containers.
	NameServer() string
	// SetExtServers configures the external nameservers the resolver
	// should use to forward queries
	SetExtServers([]string)
	// ResolverOptions returns resolv.conf options that should be set
	ResolverOptions() []string
}

Je l’admets, je ne m’attendais pas à trouver un DNS par conteneur. Cependant, en étudiant toutes les possibilités offertes par docker en terme de résolution de nom (nom, alias, link, …), ça semble assez logique.

Maintenant que le plus dur est fait, regardons un peu comment il est utilisé:

Les DNS supplémentaires indiqués par la directive dns de compose ou --dns de docker run se trouvent dans une structure qui implémente l’interface resolver:

github.com/docker/libnetwork/resolver.go

type extDNSEntry struct {
	ipStr string
}

// resolver implements the Resolver interface
type resolver struct {
	sb         *sandbox
	extDNSList [maxExtDNS]extDNSEntry
	server     *dns.Server
	conn       *net.UDPConn
	tcpServer  *dns.Server
	tcpListen  *net.TCPListener
	err        error
	count      int32
	tStamp     time.Time
	queryLock  sync.Mutex
}

Le champ extDNSList contient donc tous les DNS définis par l’utilisateur. Ce qui, soi disant passant, respecte la topologie en graphe du protocole DNS.

La résolution à proprement parler est effectuéé par la structure sandbox. Le code est légèrement long, mais en gros, docker fait appel à la librairie github.com/miekg/dns pour parser et forger les messages DNS. Puis, il effectue la résolution d’adresse manuellement. La résolution en elle-même se fait en deux étapes (schématiquement):

  • En premier lieu, on effectue une résolution d’alias pour trouver le conteneur auquel appartient l’alias.
  • Ensuite, on effectue la résolution d’adresse du conteneur.

Voilà donc la magie qui se cachait derrière les links et les alias. Je ne sais pas ce que ça donne au niveau des performances, mais c’est une manière très propre de régler les problèmes.

Comme le DNS est très récent, j’imagine que dans les précédentes versions le démon se débrouillait à partir du /etc/hosts de chaque conteneur.

Pour info, les fichiers /etc/hosts, /etc/hostname et /etc/resolv.conf sont stockés dans le dossier /var/lib/docker/<id conteneur>/ pour chaque conteneur, le démon les garde à portée de cette manière.

Un petit récapitulatif du DNS de docker:

  • Un DNS par conteneur, sur lequel le démon a la main (possibilité de rajouter des enregistrements même après que le conteneur soit créé).
  • Le DNS est inaccessible depuis l’extérieur, puisqu’il écoute sur loopback
  • Résolution manuelle des links/alias en nom de conteneur
  • Résolution des noms de conteneurs en ip
  • Le seul moment où l’utilisateur a la main sur le DNS, c’est à la création d’un conteneur.

Etude de cas, suite et fin

Maintenant que l’on a plus d’informations sur le fonctionnement interne de docker, on peut établir une nouvelle manière de résoudre le problème qui nous préoccupait.

Je ne l’avais pas précisé avait, mais je ne suis toujours pas satisfait de la solution que l’on avait trouvé. Je trouve le fait de faire un réseau qui relie tous les points d’entrée/sortie particulièrement moche, même si c’est la manière idiomatique de résoudre ce genre de problèmes avec docker.

Idéalement, chaque master/slave tourne sur son propre réseau interne, et la connexion s’effectue à travers la gateway. Comme ça, ils sont à la fois tous isolés, et la gateway garde la main sur ce qu’il se passe dans le réseau.

Le problème est que l’on a pas la main sur le DNS de docker. La solution sale serait de rajouter des mappings <host:ip> dans /etc/hosts via les options à dispositions. Seulement, ce n’est pas joli joli.

Mais du coup, il est possible de faire tourner un DNS sur l’hôte qui écoute sur 172.0.0.0/8, et de l’ajouter en tant que dns externe. On a la main dessus, donc on peut tout à fait gérer les réponses. Afin de pallier les soucis de communication entre deux sous-réseaux, on aura aussi besoin d’établir des règles iptables, et le tour est joué.

Donc:

  • Un DNS custom qui écoute sur 172.0.0.0/8. Ça peut même être un conteneur placé sur le réseau host.
  • Ajout du DNS comme dns externe pour tous les services concernés
  • Ajout des règles iptables ACCEPT pour autoriser les communications.

Et voilà, tout beau tout propre, un joli réseau de 3 + 5*n services, réalisé avec docker :smile:

Conclusion

Ainsi s’achève la partie networking avec Docker(-compose). Elle est bien évidemment très incomplète, en partie parce que docker offre beaucoup de libertés pour construire des réseaux, mais aussi parce que pour être exhaustif il faudrait bien plus de temps, et que la documentation déjà présente est bien plus que correcte.

Cela dit, si vous maîtrisez déjà les deux premières parties de cet article, vous êtes armés pour réaliser à peu près n’importe quelle infra de DEV. N’allez pas croire que pour une mise en prod docker il n’y ait pas à se soucier de la sécurité/permission/labels/users et j’en passe.

Et une bonne digestion !


Back to posts