Docker Engine : Les secrets de la relation entre Docker Client et Docker Daemon.

Une fois l’installation de Docker réussi, vous aurez accès à de nombreuses commandes afin de conteneuriser vos application et les déployer plus facilement auprès d’autres utilisateurs.

Comment Docker rend-t-il cela possible ? Que se passe-t-il lorsque on utilise une commande Docker ?

Après l’installation de Docker , un message peut vous aider à comprendre son fonctionnement si vous utilisez docker run hello-world.

Ce message nous indique 4 étapes de la manière suivante :

Afin de pouvoir aisément distinguer les différents composants intervenant dans ce processus, je vous invite à regarder ce schéma.

Cette article a pour vocation de vous expliquer en détail le fonctionnement du Docker Client et ses interactions avec le Docker Daemon,

Le Docker Client : Qu’est ce que c’est ?

Si vous êtes familier avec la structure que peut avoir un projet réseau, vous ne serez pas dépaysé. Lors d’une interaction avec Docker, il y a 3 «acteurs» principaux: l’utilisateur (vous), Docker Client (Client) et Docker Daemon (Serveur).

En tant qu’utilisateur, vous solliciterez le Docker Client à chaque commande Docker écrite sur votre terminal. Il s’agit d’un outil implémenté pour pouvoir interagir avec l’hôte sur lequel écoute le Docker Daemon. Il utilise pour cela une technologie appelé Docker Engine API, une API REST a laquelle je m’intéresserai dans la suite de cet article.

Les commandes du Docker Client ont un comportement par défaut défini dans un fichier .json qui se trouve dans le répertoire .docker (dont l’emplacement peut être défini par l’utilisateur grâce des variables d’environnement).

Il est, par ailleurs, possible de modifier ce fichier .json afin de contrôler la façon dont se comporte certaines commandes. Ces modifications peuvent être apportées en utilisant un panel de variables d’environnement reconnues par Docker ou par les options disponibles pour les commandes que l’on souhaite utiliser.

Les modifications ont un «régime de priorité» : les changements amenés par des options sont prioritaires sur les variables d’environnement qui, elles-même, sont prioritaires sur les configurations choisies dans le fichier .json.

Voici un exemple de configuration du Docker Client :

{
"HttpHeaders":{ "MyHeader":"MyValue" },
"psFormat":"table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}",
"imagesFormat":"table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
"pluginsFormat":"table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
"statsFormat":"table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
"servicesFormat":"table {{.ID}}\t{{.Name}}\t{{.Mode}}",
"secretFormat":"table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
"configFormat":"table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
"serviceInspectFormat":"pretty",
"nodesFormat":"table {{.ID}}\t{{.Hostname}}\t{{.Availability}}",
"detachKeys":"ctrl-e,e",

"stackOrchestrator":"kubernetes",
"plugins":{
"plugin1":{
"option":"value",
"plugin2":{
    "anotheroption":"anothervalue",
    "athirdoption":"athirdvalue"
},
"proxies":{
    "default":{
        "httpProxy":"http://user:pass@example.com:3128",
        "httpsProxy":"http://user:pass@example.com:3128",
        "noProxy":"http://user:pass@example.com:3128",
        "ftpProxy":"http://user:pass@example.com:3128"
    },
    "https://manager1.mycorp.example.com:2377":{
        "httpProxy":"http://user:pass@example.com:3128",
        "httpsProxy":"http://user:pass@example.com:3128"
    },
}
}

Chaque propriété se terminant par «Format» permet de définir des règles de formatage pour une commande précise. Par exemple, psFormat définit le formatage de la commande docker ps.

Ces règles de formatage utilisent des templates en Go en Python qui seront interprété dans le Docker Daemon. Ces modifications peuvent être écrasées par l’option –format de la commande docker ps.

Exemple de format :

$ docker ps --format"{{.ID}}: {{.Command}}"
a87ecb4f327c: /bin/sh -c#(nop) MA
01946d9d34d8: /bin/sh -c#(nop) MA
c1d3b0166030: /bin/sh -c yum -y up
41d50ecd2f57: /bin/sh -c#(nop) MA

$ docker ps --format"table {{.ID}}\t{{.Labels}}"
CONTAINER ID        LABELS
a87ecb4f327c        com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
01946d9d34d8
c1d3b0166030        com.docker.swarm.node=debian,com.docker.swarm.cpu=6
41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd

La propriété stackOrchestrator permet de définir l’orchestrateur par défaut lorsque vous utilisez la commande docker stack. Vous pouvez choisir entre « swarm » , « kubernetes » ou « all » ( les deux ). Cette propriété peut être écrasée par l’option –orchestrator ou par une variable d’environnement DOCKER_STACK_ORCHESTRATOR.

La propriété proxies définit les variables proxy qui seront passées aux containers lors de leur construction avec la commande docker build. Il est possible de configurer un set de proxies par « défaut » qui sera utilisé par tout les docker daemon auquel le docker client (celui qui aura été reconfiguré) se connectera afin de réagir (par exemple) aux requêtes wget ou curl provenant des adresses définies.

La propriété plugins contient des paramètres spécifiques au CLI. La clé est le nom d’un plugin et cette clé peut contenir plusieurs options, qui sont propres à ce plugin. Cela fonctionne de manière similaire à un objet de type HashMap<Nom, Options>.

La propriété HttpHeaders permet de spécifier un set de headers à ajouter à tous les messages qui seront envoyé du docker client au docker daemon. Le client n’interprète pas ces headers, il se contente de les ajouter aux messages.

Maintenant que vous êtes capable de configurer votre Docker Client à votre guise, nous allons maintenant nous intéresser à la communication de ce dernier avec le Docker Daemon.

Comment le client «contacte» le serveur lors de la première étape ?

Nous allons désormais expliquer les interactions qui se produisent entre le Docker Client et le Docker Daemon en nous attachant à ce qui se passe lors d’une commande comme docker run Hello-World. Comme je vous l’avais montré précédemment, la première étape nous indiquait l’évènement suivant :

1. The Docker client contacted the Docker daemon.

Le Docker Daemon constitue notre serveur, chargé du stockage et du déploiement de l’ensemble de nos containers .

Il peut écouter les requêtes du Docker Client grâce à des sockets unix, tcp ou fd. Une socket unix est créée par défaut sur sous le nom /var/run/docker.sock. Cette création nécessite cependant les permissions de l’utilisateur root. C’est en partie la raison pour laquelle certaines commandes docker doivent être précédées de « sudo ».

Cette socket ne dispose d’aucun système de protection (pas de cryptage, ni d’authentification). Ce sera donc à vous, de la sécuriser, en utilisant une socket ou un proxy web sécurisé que vous pourrez spécifier auprès du Docker Daemon avec la variable d’environnement DOCKER_HOST ou l’option -H (que vous pouvez utiliser plusieurs fois si vous voulez faire écouter votre Docker Daemon sur plusieurs sockets en même temps).

Voici le format accepté pour l’option -H ou DOCKER_HOST :

tcp://[host]:[port][path] or unix://path

Par exemple :

tcp:// -> connexion TCP à l’adresse locale 127.0.0.1 soit sur le port 2376 si le chiffrement TLS est actif, ou sinon sur le port 2375 pour les communication en texte brut.

tcp://host:2375 ->connexion TCP sur host:2375

tcp://host:2375/path -> connexion TCP sur host:2375 avec préfix de chemin pour toutes les requêtes.

unix://path/to/socket -> Pour un socket du domaine Unix de chemin path/to/socket

En résumé :

# Download an ubuntu image, use default Unix socket
$ docker pull ubuntu
# OR use the TCP port
$ docker -H tcp://127.0.0.1:2375 pull ubuntu

Tout comme le docker Client, de nombreux paramètres du Docker Daemon peuvent modifiés à travers des options de commande ou des variables d’environnement. Comme je vous l’a montré, vous pouvez donc changer port d’écoute mais aussi le mode de stockage, le DNS, les proxies etc. Mais cela pourra faire l’objet d’un nouvel article prochainement. Ici, nous allons nous concentrer sur le Docker Engine API.

Nous avons donc vu le point de contact entre le client et le serveur. Maintenant, il est temps d’étudier le contenu de ces communications. Ces dernières s’effectuent grâce à Docker Engine API , une technologie fournie par Docker qui permet à notre Docker Client d’interagir avec Docker Daemon.

Il s’agit d’une API REST accessible par un client HTTP grâce à l‘utilisation de wget ou curl. Il existe également des SDK en Go ou Python qui possèdent une librairie capable d’interagir avec un Docker Daemon. Il en existe, par ailleurs, dans de nombreux autres langages (Java, Scala, C, C++, Rust, etc.) mais il ne s’agit pas de bibliothèques «officielles».

Que se passe-t-il donc concrètement à la deuxième étape du « docker run hello-world » ?

Une fois le point de contact établi, voici à quoi ressemble la requête (curl) auprès du Docker Daemon.

Ceci correspond à ce qui se se produit lorsque on utilise la commande « docker run alpine echo hello world ».

$ curl --unix-socket /var/run/docker.sock -H"Content-Type: application/json"\
-d'{"Image": "alpine", "Cmd": ["echo", "hello world"]}'\
-X POST http:/v1.24/containers/create
{"Id":"1c6594faf5","Warnings":null}

$ curl --unix-socket /var/run/docker.sock -X POST http:/v1.24/containers/1c6594faf5/start

$ curl --unix-socket /var/run/docker.sock -X POST http:/v1.24/containers/1c6594faf5/wait
{"StatusCode":0}

$ curl --unix-socket /var/run/docker.sock "http:/v1.24/containers/1c6594faf5/logs?stdout=1"
hello world

Si j’essaie la première ligne de commande, j’obtiens ceci :

Cela se produit si vous n’avez jamais récupéré (pull) l’image que vous souhaitez faire tourner. (J’ai volontairement changé l’exemple de départ pour que cela vous produise une erreur, au cas ou vous tomberiez sur le même genre de problème).

Docker Daemon a besoin d’une image avant de pouvoir créer et démarrer un nouveau container chargé d’exécuter vos commandes.

Il vous faudra donc d’abord récupérer (pull) l’image « alpine » comme ceci :

Vous pourrez ensuite utiliser les 2 commandes curl suivantes en y insérant les bon identifiants.

Pour en revenir à l’exemple de départ (docker run hello-world), voici la variante curl :

Quelques explication sur Curl

Avant d’aller plus loin, je vais vous donner quelques explications au sujet des commandes curl que nous venons de voir.

Curl est une interface en ligne de commande qui permet de transférer des données sur le réseau. Ces données sont représentées par des URLs. Ce transfert peut être effectué via de nombreux protocoles (FTP, SMTP, HTTP, etc.). Ici, avec Docker, curl utilisera principalement le protocole HTTP.

L’option –unix-socket précise la socket à laquelle on souhaite se connecter, celle sur laquelle le Docker Daemon écoute. Ici , il s’agit de la socket unix par défaut.

-H permet d’indiquer au client le type de donnée que contiendra la réponse du serveur avec laquelle il devra travailler. Ici, les réponses du serveur sont générées en JSON, nous avons donc « Content–Type: application/json »

-d est une option prenant en argument un tableau json dans lequel on peut préciser des données spécifiques. Dans les exemples précédents, on pouvait préciser une image, celle que l’on voulait télécharger, et des commandes que l’on voulait exécuter une fois le container lancé.

-X permet de préciser le type de requête qu’on souhaite faire avec curl. Par défaut, quand cette option n’est pas explicitement indiquée, la requête est GET.

Le docker Daemon comprend ensuite précisément l’action qu’il doit accomplir à partir de l’URL qui lui est indiqué.

A quoi correspondent donc ces URL ?

Ces URLs, une fois associées à un type de requête, permettent de donner des instructions qui seront comprises par le Docker Daemon. Lors de « docker run hello-world », on commence d’abord par récupérer (pull) l’image hello-world (une image de test).

Pour cela, le docker daemon attend une requête POST et une URL de la forme suivante :

/images/create

Il faut ensuite ajouter un argument de la forme suivante :

fromImage : string

Nom de l’image à récupérer pull. Ce nom peut inclure un tag ou un digest. Ce parametre peut etre utilisé uniquement lorsqu’on pull une image. Le pull est annulé si la connection HTTP est interrompue.

FromSrc : string

Source à importer. La valeur, ici, peut être une URL à partir de laquelle l’image peut etre recuperée.Ce parametre peut etre utilisé uniquement lorsqu’on pull une image.

 Repo:string

Le nom d’un repertoire donnant l’accès a une image une fois importé. Ce parametre peut etre utilisé uniquement lorsqu’on importe une image.

Tag : string

Tag ou digest. Si il est vide lors d’un pull , cela provoque le pull de tous les tags d’une image donnée.

Message : string

Le message de commit d’une image importée.

Les réponses attendu par le Docker Client sont les suivantes :

200 no error
404 repository does not exist or no read access  : {  "message": "Something went wrong." } 
500 server error : {  "message": "Something went wrong." }

Je vous invite à revoir l’exemple précédent sur le pull de l’image alpine réalisé en curl, vous verrez précisément à quoi ressemble une URL de cette forme.

Une fois l’image obtenue, Docker Daemon lance un container à partir de cette image . Il réalise le troisième étape de notre « docker run hello-world ».

Pour cela, il attends une requête POST avec une URL de cette forme :

/containers/{id}/start

id :string

Corresponds à l’identifiant d’une image.

Réponses attendues par le client :

204 no error
304 container already started
404 no such container : {"message": "No such container: c2ada9df5af8"}
500 server error :{"message": "Something went wrong."}

Il effectue ensuite un « wait » qui se fait de la même manière que le « start », il vous suffit de remplacer « start » par « wait » a la fin de l’URL. Cette commande permet d’attendre la fin de l’exécution d’un container pour renvoyer son code de retour (exit code).

Réponses attendues par le client :

 200 The container has exit :  {"StatusCode": 0, "Error": {"Message":"string" }}
 404 no such container : { "message": "No such container: c2ada9df5af8"}
 500 server error : { "message": "Something went wrong."}

Une fois que le lancement du container est assuré. Il ne reste plus qu’a réaliser la quatrième étape, c’est à dire rediriger le résultat vers la sortie standard de l’utilisateur. Pour cela, on effectue un requête GET avec une URL de la forme :

/containers/{id}/logs?stdout=1

Et voila ! Vous obtenez ainsi le message « Hello from Docker » que je vous ai montré sur la toute première page de cet article.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.