BlogWissenwertesDocker: Was genau ist ein Container? – Teil 2

Docker: Was genau ist ein Container? – Teil 2

Ein Beitrag von Benjamin Breitenstein

Virtualisierung

Grundsätzlich steht einem Prozess, der beispielsweise unter Windows oder Linux läuft, der gesamte Arbeitsspeicher zur Verfügung. Der Prozess kann also so damit umgehen, als wäre er der einzige Prozess, der aktuell läuft. Tatsächlich laufen im Betriebssystem natürlich mehrere Prozesse, es wird dem Prozess also ein Arbeitsspeicher vorgegaukelt, der so gar nicht existiert. Dies nennt man Virtualisierung.

Neben dem Arbeitsspeicher kann man noch weitere Dinge virtualisieren, wie beispielsweise:

  • CPU Ressourcen
  • I/O Devices
  • Dateisystem
  • Netzwerk

Viele Anwendungen benötigen nur die Sicht auf einen sehr kleinen Teil des Systems. Standardmäßig wird aber nur der Arbeitsspeicher virtualisiert, alles weitere bedarf eines erheblichen Konfigurationsaufwands. Wenn nun viele der Virtualisierungen verwendet und für seine Prozesse konfiguriert werden, laufen diese quasi in ihrer „eigenen“ Welt. Einen so konfigurierten Prozess nennt man „Container„.
Wichtig hierbei ist, dass es sich NICHT um eine Virtualisierung der CPU selbst handelt, es ist also keine virtuelle Maschine (VM), sondern lediglich ein ganz normaler Prozess mit verschiedenen zusätzlichen Einstellungen.

Da die Virtualisierung bestimmte Features im Kernel voraussetzt, ist damit normalerweise immer Linux verbunden. „Windows-Container“ hingegen funktionieren ähnlich. Windows hat jedoch keine 1:1 Entsprechungen für viele der Einstellungen. Stattdessen wird unter BSD von „Jails“ gesprochen.

Container machen unter anderem Gebrauch von den folgenden Technologien (kein Anspruch auf Vollständigkeit):

  • AppArmor (Linux kernel security module, https://www.apparmor.net/)
  • SELinux (Linux kernel security module, https://www.redhat.com/en/topics/linux/what-is-selinux)
  • namespaces
  • libseccomp (Virtualisierung der Linux SystemCalls mittels eBPF)
  • rootfs (Virtualisierung des Dateisystems)
  • cgroups/cgroupsv2 (Virtualisierung der CPU Resourcen und I/O, https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)
  • iptables (Virtualisierung der Netzwerk-Infrastruktur)

Was ist Docker?

Docker ist ursprünglich ein Produkt der Firma Docker Inc., das die Erstellung und Verwaltung von Containern übernimmt und damit die Virtualisierung für Prozesse maßgeblich vereinfacht.
Dadurch wurden Container erstmals einfach und ohne viel Aufwand nutzbar, was die Verbreitung der Technologie erheblich vorantrieb. Alternativ zu Docker gibt es auch weitere ähnliche Produkte, beispielsweise von RedHat (podman, crun, …). Im weiteren Verlauf geht es hier aber um den Software-Stack von Docker, welcher auch die Referenz-Implementierung der OCI darstellt (Open Container Initiative – https://opencontainers.org/).

Komponenten von Docker

runc ist ein Kommandozeilen-Tool, das Applikationen starten kann, die im OCI Format vorliegen, d.h. es ist eine Implementierung der OCI Runtime Spezifikation (https://github.com/opencontainers/runtime-spec).

Container Images werden als Bundle konfiguriert. In einem Bundle befindet sich eine „config.json“-Datei, die das Bundle näher spezifiziert, sowie das Root-Dateisystem, welches den initialen Inhalt des späteren Containers darstellt.

runc kapselt im Prinzip alle low-level Funktionalitäten des Betriebssystems um den Container zu erstellen und zu starten. Es ist sehr komplex in der Bedienung und nicht unbedingt für die direkte Ausführung von Usern gedacht.

containerd ist die Container Runtime, also das Programm, das Container auf dem System ausführt. Es kann runc verwenden, um auf dessen Funktionalitäten für den Start des Containers zurückzugreifen, hat aber darüber hinaus noch weitere Aufgaben:

  • Ausführen von Containern mittels runc und den dafür notwendigen Parametern
  • Push und Pull von Docker Images über Repositories 
  • Management des Speichers für den Container
  • Management der Netzwerk-Interfaces und Einstellungen des Containers
  • Management der Netzwerk-Namespaces für die Container, sodass beispielsweise zwei Container miteinander kommunizieren können.

Zudem öffnet containerd einen Unix-Socket (/var/run/containerd/containerd.sock), über den beispielsweise die Shim mit containerd kommuniziert.

Die containerd-shim-runc-v2 (kurz: „shim“), ist ein leichtgewichtiger Wrapper für container, welcher als Parent Prozess über dem Container Prozess sitzt. Die Shim hält die Handles (File Descriptors) für Input und Output offen, für den Fall, dass containerd oder dockerd abstürzen sollten. Das ermöglicht, dass der Container auch im Fehlerfall unabhängig von den anderen Docker-Komponenten weiterlaufen kann. Weiterhin kann sie den Exit-Code des Prozesses zwischenspeichern, sodass dieser an die anderen Komponenten kommuniziert werden kann, ohne dass der Container-Prozess dafür am Laufen gehalten werden muss. Dazu kommuniziert sie mit containerd über den containerd socket.

Genaueres über die Kommunikation zwischen der Shim und containerd findet man unter https://github.com/containerd/containerd/blob/main/core/runtime/v2/README.md

dockerd ist der Docker Daemon, der als Service im Betriebssystem läuft. Er managt den kompletten Lifecycle der Container auf einer Maschine mit Hilfe von containerd. Beim Start erstellt der daemon einen Unix-Socket (/var/run/docker.sock), über den andere Programme mit ihm kommunizieren können. Der Socket stellt die offizielle Docker API dar, über die mit dem gesamten Docker Host kommuniziert wird.

docker ist das Kommandozeilen-Tool (docker-cli), welches über den Socket des Docker Daemons mit diesem kommuniziert. Wichtig hierbei ist, dass das docker-cli eigentlich nichts selbst macht, sondern lediglich die API des Docker Deamons anspricht, der dann die eigentliche Aufgabe ausführt.

Hier ein Beispiel für den Prozess-Baum eines laufenden Containers – in diesem Falle ein nginx Server:

Docker Teil 2 Bild 1

Wie das geschulte Auge sieht, ist der Docker Daemon (dockerd) unabhängig vom laufenden Container. Der Container enthält in diesem Fall den nginx-Server inklusive dessen Worker-Prozesse.

runc ist hier nicht mehr sichtbar, weil es nur zum Starten des Containers notwendig war. Danach wird runc komplett beendet und benötigt auch keine System-Ressourcen mehr. Stattdessen wird der Container durch die Shim (hier zu sehen als Parent Process von nginx) überwacht, die dann dafür sorgt, dass der Daemon den Status des Containers und den Exit-Code entsprechend abfragen kann.
Ein häufiges Argument für die RedHat Implementierung crun (https://github.com/containers/crun) ist, dass crun schneller sei als runc und dass crun einen viel niedrigeren „memory footprint“ habe. Dies stimmt zwar, ist jedoch in der Praxis nicht so relevant, da runc nur einmal beim Container-Start aufgerufen wird und dann direkt wieder beendet wird.

Dieses Konstrukt hat natürlich aber auch Nachteile. Beispielsweise ist es notwendig, den Docker Daemon zu starten, wenn man über „docker build“ ein Docker-Image erstellen oder pullen/pushen möchte.
Wenn nun in einer CI/CD-Pipeline ein Docker-Image innerhalb eines Containers eingebaut werden möchte, wie das z.B. bei GitLab-Pipelines oder GitHub-Actions meist der Fall ist, dann reicht es nicht, wenn nur das docker-cli vorhanden ist.

Es gibt dann folgende Möglichkeiten:

  • Der Socket des dockerd vom Host-System wird in den Container eingebunden .Dies ist normalerweise das empfohlene Vorgehen.
  • docker-in-docker: innerhalb des Containers läuft ein weiterer, unabhängiger dockerd (https://hub.docker.com/_/docker)
  • In der Pipeline wird ein alternatives Tool, wie beispielsweise Kaniko (https://github.com/GoogleContainerTools/kaniko) für das Bauen der Docker Images genutzt oder Crane (https://github.com/google/go-containerregistry/blob/main/cmd/crane) zum pushen/pullen.

Praktische Tipps

Priviledged Mode für Container

Entgegen vieler Artikel im Internet sind Priviledged Container grundsätzlich nicht notwendig.
Es müssen lediglich die notwendigen Linux-Capabilities beim Starten des Containers mit angegeben werden.

Ein Beispiel: Für das Lesen einiger virtueller Files des /proc Verzeichnisses wird die SYS_PTRACE Capability benötigt. Viele Monitoring-Tools kommen also mit wenigen Capabilities aus, während priviledged Container sehr weitreichende Berechtigungen haben.

Anstatt: DOCKER RUN –RM -IT –PRIVILEGED CADVISOR
Besser: DOCKER RUN –RM -IT –CAP-ADD SYS_PTRACE CADVISOR

Docker Compose und Docker Swarm

Docker Compose ist eine etwas ältere Technologie, um Container als Services auf einem Host laufen zu lassen. Der Docker Daemon kann mehrere Container damit verwalten und als Services anbieten.
Seit Compose V2 können auch mehrere Container gleichzeitig damit verwaltet werden, beispielsweise wenn ein Applikations-Container und ein Datenbank-Container während der Entwicklung verwendet werden.
Die verschiedenen Container/Services werden dann in einer compose-Datei konfiguriert.

Docker Swarm ist verglichen mit Docker Compose eigentlich in allen Belangen einfacher zu verstehen und leichter zu bedienen.
Seit Compose V2 ist die Konfiguration auch sehr ähnlich – das zeigt meines Erachtens nach, dass Docker Swarm ein besseres Design hatte und bei Docker Compose später daraus gelernt wurde.

Daher empfiehlt es sich, gleich Docker Swarm zu verwenden, wenn dies auch der Produktions-Umgebung entspricht, da ein Swarm auch mit einer einzigen Maschine ohne Einschränkungen funktioniert – im Gegensatz zu Kubernetes, bei dem lokal Tools wie https://minikube.sigs.k8s.io eingesetzt werden.

Es gibt eigentlich keinen Grund, neben Docker Swarm dann noch ein weiteres Tool (Compose) einzuführen – dies erhöht lediglich die Komplexität bei der Entwicklung.

Fazit

Container sind keine Magie, sondern normale Linux-Prozesse, die durch eine restriktive Konfiguration für mehr Sicherheit und eine höhere Systemstabilität sorgen sollen.

Mittels Docker sind diese auch mit wenig Aufwand einsetzbar und es entstehen – im Gegensatz zu einer VM – nur minimale Performance-Einbußen für den Prozess.

Weiterführende Quellen und interessante Artikel

https://alexander.holbreich.org/docker-components-explained/

https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci



Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert