Docker is vreemd

Containers zijn niet echt

In tegenstelling tot VM's zijn containers gewoon processen met speciale beperkingen. Je kernel voert dat proces nog steeds uit! En als je weet waar je moet kijken, kun je dat proces zelfs terugvinden in /proc.

Containers zijn écht niet echt

Er is bovendien niet echt een Linux-functie die simpelweg "containers" heet. In plaats daarvan zijn containers eigenlijk vijf verschillende Linux-kernel-functies in een trenchcoat.

Namespaces verbergen wat de host en de container kunnen zien, cgroups beperken het resourcegebruik van de container, en er is een hele reeks netwerkmagie die ik niet begrijp waarmee het netwerk van een container kan worden geïsoleerd.

Images zijn gewoon mappen

Images zijn simpelweg mappen die Docker ergens plaatst en vervolgens chroot (of pivot_root) aanroept om die map de nieuwe root-directory van je containerproces te maken. Dus als je docker run -it --rm ubuntu /bin/bash uitvoert, doet Docker het volgende:

  1. Download de Ubuntu-image van Dockerhub.
  2. Plaats die image ergens.
  3. Voer /some/weird/docker/path/bin/bash uit.

Go is een vrij verschrikkelijke taal voor een container-runtime

De container-runtime van Docker, runc, is geschreven in Go, en Go heeft enkele ontwerpkeuzes die het minder geschikt maken voor een container-runtime. De runtime van Go is van nature multi-threaded. Dat is een erg mooie functie, maar het leidt tot enkele uitdagingen in runc.

Veel van de Linux-syscalls die een container-runtime moet maken, moeten worden uitgevoerd in een single-threaded omgeving. Daarom heeft runc een krankzinnige C-module die wordt uitgevoerd voordat de runtime van Go wordt geïnitialiseerd, zodat deze syscalls kunnen worden uitgevoerd.

Go heeft bovendien geen fork! Fork splitst alleen de hoofdthread, wat een risico is in een runtime zoals die van Go. Een container-runtime wil echter echt forken, een reeks beperkingen instellen en vervolgens het proces uitvoeren dat je daadwerkelijk in docker run hebt gezet. Om dit te omzeilen, voert runc /proc/self/exe uit met een speciaal opdrachtregelargument dat het identificeert als een kindproces om een fork te benaderen.

Docker vereist root!

Dit is vrij gemakkelijk over het hoofd te zien als je, net als ik, Docker op MacOS draait. Veel van de Linux-kernel-API's die container-runtimes gebruiken, vereisen (of vereisten in ieder geval) dat ze als root worden uitgevoerd, waardoor de Docker-daemon meestal als root draait. Ik kan me voorstellen dat containerbeveiliging een zeer zware taak is.

Dit lijkt tegenwoordig beter te zijn. De grote uitzondering lijkt netwerking te zijn. Als je een rootless container-runtime gebruikt, lijkt het erop dat de hele netwerkstack in userspace opnieuw moet worden geïmplementeerd om je daadwerkelijk dezelfde functies te kunnen bieden!

Leren is leuk!

Ik ben net begonnen met mijn eigen container-runtime en ik heb het gevoel dat ik al zoveel over Docker heb geleerd. Het schrijven van je eigen versie van een tool die je gebruikt, is een geweldige manier om er meer over te leren!