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 containerid
et 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:
- Permet de démarrer des conteneurs sans démons.
- STDIO FD containerd docker.
- 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.json
dans 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.