Fonctionnement du processus de création d'un conteneur Docker (de Docker Run à Runc)

La traduction de l'article a été préparée en prévision du début du cours "Plateforme d'infrastructure basée sur Kubernetes" .








Au cours des derniers mois, j'ai passé beaucoup de temps à apprendre comment fonctionnent les conteneurs Linux. En particulier, que fait-il exactement docker run. Dans cet article, je vais résumer ce que j'ai découvert et essayer de montrer comment les éléments individuels forment une grande image. Nous commencerons notre voyage en créant un conteneur alpin à l'aide du docker run:



$ docker run -i -t --name alpine alpine ash


Ce conteneur sera utilisé dans la sortie ci-dessous. Lorsque la commande d'exécution du docker est appelée, elle analyse les paramÚtres qui lui sont passés sur la ligne de commande et crée un objet JSON pour représenter l'objet que le docker doit créer. Cet objet est ensuite envoyé au démon docker via le socket de domaine UNIX /var/run/docker.sock. Pour surveiller les appels d'API, nous pouvons utiliser l'utilitaire strace :



$ strace -s 8192 -e trace=read,write -f docker run -d alpine


[pid 13446] write(3, "GET /_ping HTTP/1.1\r\nHost: docker\r\nUser-Agent: Docker-Client/1.13.1 (linux)\r\n\r\n", 79) = 79
[pid 13442] read(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Mon, 19 Feb 2018 16:12:32 GMT\r\nContent-Length: 2\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nOK", 4096) = 196
[pid 13442] write(3, "POST /v1.26/containers/create HTTP/1.1\r\nHost: docker\r\nUser-Agent: Docker-Client/1.13.1 (linux)\r\nContent-Length: 1404\r\nContent-Type: application/json\r\n\r\n{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":null,\"Image\":\"alpine\",\"Volumes\":{},\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{},\"HostConfig\":{\"Binds\":null,\"ContainerIDFile\":\"\",\"LogConfig\":{\"Type\":\"\",\"Config\":{}},\"NetworkMode\":\"default\",\"PortBindings\":{},\"RestartPolicy\":{\"Name\":\"no\",\"MaximumRetryCount\":0},\"AutoRemove\":false,\"VolumeDriver\":\"\",\"VolumesFrom\":null,\"CapAdd\":null,\"CapDrop\":null,\"Dns\":[],\"DnsOptions\":[],\"DnsSearch\":[],\"ExtraHosts\":null,\"GroupAdd\":null,\"IpcMode\":\"\",\"Cgroup\":\"\",\"Links\":null,\"OomScoreAdj\":0,\"PidMode\":\"\",\"Privileged\":false,\"PublishAllPorts\":false,\"ReadonlyRootfs\":false,\"SecurityOpt\":null,\"UTSMode\":\"\",\"UsernsMode\":\"\",\"ShmSize\":0,\"ConsoleSize\":[0,0],\"Isolation\":\"\",\"CpuShares\":0,\"Memory\":0,\"NanoCpus\":0,\"CgroupParent\":\"\",\"BlkioWeight\":0,\"BlkioWeightDevice\":null,\"BlkioDeviceReadBps\":null,\"BlkioDeviceWriteBps\":null,\"BlkioDeviceReadIOps\":null,\"BlkioDeviceWriteIOps\":null,\"CpuPeriod\":0,\"CpuQuota\":0,\"CpuRealtimePeriod\":0,\"CpuRealtimeRuntime\":0,\"CpusetCpus\":\"\",\"CpusetMems\":\"\",\"Devices\":[],\"DiskQuota\":0,\"KernelMemory\":0,\"MemoryReservation\":0,\"MemorySwap\":0,\"MemorySwappiness\":-1,\"OomKillDisable\":false,\"PidsLimit\":0,\"Ulimits\":null,\"CpuCount\":0,\"CpuPercent\":0,\"IOMaximumIOps\":0,\"IOMaximumBandwidth\":0},\"NetworkingConfig\":{\"EndpointsConfig\":{}}}\n", 1556) = 1556
[pid 13442] read(3, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Mon, 19 Feb 2018 16:12:32 GMT\r\nContent-Length: 90\r\n\r\n{\"Id\":\"b70b57c5ae3e25585edba898ac860e388582391907be4070f91eb49f4db5c433\",\"Warnings\":null}\n", 4096) = 281


C'est lĂ  que commence le vrai plaisir. DĂšs que le dĂ©mon docker reçoit la requĂȘte, il analyse la sortie et communique avec containerd via l' API gRPC pour configurer le runtime (ou runtime) du conteneur en utilisant les paramĂštres transmis sur la ligne de commande. Pour observer cette interaction, nous pouvons utiliser l'utilitaire ctr:



$ ctr --address "unix:///run/containerd.sock" events


TIME                           TYPE                           ID                             PID                            STATUS
time="2018-02-19T12:10:07.658081859-05:00" level=debug msg="Calling POST /v1.26/containers/create" 
time="2018-02-19T12:10:07.676706130-05:00" level=debug msg="container mounted via layerStore: /var/lib/docker/overlay2/2beda8ac904f4a2531d72e1e3910babf145c6e68dfd02008c58786adb254f9dc/merged" 
time="2018-02-19T12:10:07.682430843-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/attach?stderr=1&stdin=1&stdout=1&stream=1" 
time="2018-02-19T12:10:07.683638676-05:00" level=debug msg="Calling GET /v1.26/events?filters=%7B%22container%22%3A%7B%22d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f%22%3Atrue%7D%2C%22type%22%3A%7B%22container%22%3Atrue%7D%7D" 
time="2018-02-19T12:10:07.684447919-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/start" 
time="2018-02-19T12:10:07.687230717-05:00" level=debug msg="container mounted via layerStore: /var/lib/docker/overlay2/2beda8ac904f4a2531d72e1e3910babf145c6e68dfd02008c58786adb254f9dc/merged" 
time="2018-02-19T12:10:07.885362059-05:00" level=debug msg="sandbox set key processing took 11.824662ms for container d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f" 
time="2018-02-19T12:10:07.927897701-05:00" level=debug msg="libcontainerd: received containerd event: &types.Event{Type:\"start-container\", Id:\"d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f\", Status:0x0, Pid:\"\", Timestamp:(*timestamp.Timestamp)(0xc420bacdd0)}" 
2018-02-19T17:10:07.927795344Z start-container                d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f                                0
time="2018-02-19T12:10:07.930283397-05:00" level=debug msg="libcontainerd: event unhandled: type:\"start-container\" id:\"d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f\" timestamp:<seconds:1519060207 nanos:927795344 > " 
time="2018-02-19T12:10:07.930874606-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/resize?h=35&w=115" 


La configuration de l'exĂ©cution d'un conteneur est une tĂąche assez importante. Les espaces de noms doivent ĂȘtre configurĂ©s, l'image doit ĂȘtre montĂ©e, les contrĂŽles de sĂ©curitĂ© doivent ĂȘtre activĂ©s (profils de protection d'application, profils seccomp, capacitĂ©s), etc., etc., etc. Vous pouvez vous faire une assez bonne idĂ©e tout ce qui est nĂ©cessaire pour configurer le runtime en regardant la sortie docker inspect containeridet le fichier de spĂ©cification du runtime config.json(plus Ă  ce sujet dans un instant ).



À proprement parler, containerd ne crĂ©e pas de runtime de conteneur. Il configure l'environnement et appelle ensuite containerd-shimpour exĂ©cuter le runtime du conteneur via le runtime OCI configurĂ© (contrĂŽlĂ© par le paramĂštre containerd "–runtime"). La plupart des systĂšmes modernes exĂ©cutent le runtime du conteneur basĂ© sur runc . Nous pouvons observer cela avec l'utilitaire pstree :



$ pstree -l -p -s -T
systemd,1 --switched-root --system --deserialize 24
  ├─docker-containe,19606 --listen unix:///run/containerd.sock --shim /usr/libexec/docker/docker-containerd-shim-current --start-timeout 2m --debug
  │   ├─docker-containe,19834 93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /var/run/docker/libcontainerd/93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /usr/libexec/docker/docker-runc-current


Puisque pstree supprime le nom du processus, nous pouvons vérifier le PID avec ps :



$ ps auxwww | grep [1]9606


root     19606  0.0  0.2 685636 10632 ?        Ssl  13:01   0:00 /usr/libexec/docker/docker-containerd-current --listen unix:///run/containerd.sock --shim /usr/libexec/docker/docker-containerd-shim-current --start-timeout 2m --debug


$ ps auxwww | grep [1]9834


root     19834  0.0  0.0 527748  3020 ?        Sl   13:01   0:00 /usr/libexec/docker/docker-containerd-shim-current 93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /var/run/docker/libcontainerd/93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /usr/libexec/docker/docker-runc-current


Quand j'ai commencé à rechercher les interactions entre dockerd, containerd et shim , je ne comprenais pas complÚtement à quoi servait shim . Heureusement, Google a conduit à une excellente rédaction de Michael Crosby . Shim sert plusieurs objectifs:



  1. Permet de démarrer des conteneurs sans démons.
  2. STDIO FD containerd docker.
  3. containerd .


Les premier et deuxiĂšme points clĂ©s sont trĂšs importants. Ces fonctionnalitĂ©s vous permettent de dĂ©coupler le conteneur du dĂ©mon docker , permettant Ă  dockerd d' ĂȘtre mis Ă  jour ou redĂ©marrĂ© sans affecter les conteneurs en cours d'exĂ©cution. TrĂšs efficace! J'ai mentionnĂ© que shim est responsable de l'exĂ©cution de runc pour dĂ©marrer rĂ©ellement le conteneur. Runc a besoin de deux choses pour faire son travail : le fichier de spĂ©cification et le chemin vers l'image du systĂšme de fichiers racine (une combinaison de ces Ă©lĂ©ments est appelĂ©e un bundle ). Pour voir comment cela fonctionne, nous pouvons crĂ©er des rootfs en exportant l'image du docker alpin :



$ mkdir -p alpine/rootfs


$ cd alpine


$ docker export d1a6d87886e2 | tar -C rootfs -xvf -


time="2018-02-19T12:54:13.082321231-05:00" level=debug msg="Calling GET /v1.26/containers/d1a6d87886e2/export" 
.dockerenv
bin/
bin/ash
bin/base64
bin/bbconfig
.....


L'option d'exportation accepte un conteneur, que vous pouvez trouver dans la sortie docker ps -a. Vous pouvez utiliser la commande runc spec pour créer le fichier spec :



$ runc spec


Cela crĂ©era un fichier de spĂ©cification nommĂ© config.jsondans votre rĂ©pertoire actuel. Ce fichier peut ĂȘtre personnalisĂ© en fonction de vos besoins et exigences. Une fois que vous ĂȘtes satisfait du fichier, vous pouvez exĂ©cuter runc avec le rĂ©pertoire rootfs comme seul argument (la configuration du conteneur sera lue Ă  partir du fichier config.json):



$ runc run rootfs


Cet exemple simple créera une enveloppe de cendres alpines:



$ runc run rootfs


/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.7.0
PRETTY_NAME="Alpine Linux v3.7"
HOME_URL="http://alpinelinux.org"
BUG_REPORT_URL="http://bugs.alpinelinux.org"


La possibilité de créer des conteneurs et de jouer avec la spécification d'exécution d'exécution est incroyablement puissante. Vous pouvez évaluer différents profils d'application, tester les capacités Linux et expérimenter tous les aspects de l'exécution du conteneur sans avoir à installer docker. J'ai seulement égratigné un peu la surface et je recommande vivement de lire la documentation runc et containerd . Des outils trÚs sympas!






En savoir plus sur le cours.







All Articles