Nachlese
Unterschied VMs <=> Container
Bei virtuellen Maschinen haben wir einen umfangreichen Hypervisor, der Platz belegt, und jede VM hat eine umfangreiches Betriebssystem welches platz belegt. Ein System Call aus der Applikation wird durch einen Hardwaretreiber geleitet, dort wird er in Hardwarenahe Befehle umgewandelt, danach wird er durch die Virtuelle Hardware und den Hypervisor wieder in System Calls umgewandelt, danach landet er im Hardwaretreiber und wird wieder in (dieses mal echte) Hardwarebefehle umgewandelt. Viel Arbeit, dafür das das HostOS doch schon weiß, wie es mit dem System Call umgehen müsste.
Natürlich haben die meisten Hypervisor mittlerweile Pseudotreiber, welche dafür sorgen, das die Befehle nicht vollständig umgewandelt werden müssen. Die Daten werden dann per Shared Memory kopiert, oder ähnliches. In diesen Bereichen werden Virtuelle Maschinen schon Containern ähnlich.
Docker geht einen etwas anderen Weg. Der Hypervisor wird ersetzt durch den Docker Daemon, welcher nun die Prozesse in den Containern in namespaces isoliert, jedoch weiterhin den gleichen Kernel benutzt.
Der Prozess läuft also auf dem Hostsystem (er ist dort auch in der Prozessliste sichtbar) er ist jedoch so isoliert, das der Prozess im Container nicht auf Prozesse und daten außerhalb des Containers zugreifen kann.
In folgenden Bereichen können Container durch namespaces isoliert werden:
- Mount (mnt): Eingehängte Verzeichnisse können nur für den Container oder nur für den Host sichtbar sein
- Process ID (pid): Innerhalb des Containers werden neue bei 1 beginnende Process IDs vergeben. Die Prozesse sind außerhalb des Containers unter einer anderen PID sichtbar.
- Network (net): Der Container bekommt einen neuen Netzwerkstack. Beim erstellen hat ein Netzwerk Namespace nur ein Loopback Interface.
- Interprocess Communication (ipc): Die Container werden für SysV Kommunikation isoliert. Ein benannter shm (Shared Memory) Bereich innerhalb des Containers kann auf einen anderen Speicherbereich verweisen, wie ein gleich benannter auerhalb des Containers
- UTS (Unix Time-Sharing) erlaubt den Containerprozessen einen anderen Hostnamen zu haben, als die Prozesse ausserhalb des Containers
- User ID (user): Die User ID's innerhalb des Containers können andere User ID's repräsentieren als außerhalb des Containers. Bei Podman bedeutet das sogar der ein User einen Container starten kann, in dem die Prozesse die User ID 0 (root) haben. (Dies wird jedoch sehr ... interessant ... wenn man Dateisysteme in den Container einhängt.
- cgroup (Control Group): Die verwendbaren Ressourcen können für den Container eingeschränkt werden (mur 1 CPU, max. 100MB RAM, ...)
- Time: Der Container kann in einer anderen Zeitzone als der Host sein.
Diese Namespaces werden seit 2008 von shared Hostern z.B. in Openvirtuzzo / OpenVZ zur Virtualisierung eingesetzt. Damit wird z.B. Sichergestellt das die PHP Umgebung von Kunde 1 auf die Daten von Kunde 2 zugreifen kann. Sie wurde also seit 2008 verfeinert, und auf Fehler geprüft (in einem recht unfreundlichen Umfeld)
Hello World in Containern
Nehmen wir als Beispiel ein Hello World Programm:
#include<stdio.h>
int main() {
printf("Hello World aus Docker\n");
return 0;
}
Im klassischen Deployment kompiliere ich dies mit
gcc -o helloworld helloworld.c
und bekomme eine ausführbare Datei, die ich auf das Zielsystem übertragen und ausführen kann. Dabei habe ich aber schon die erste Abhängigkeit in mein Deployment gebracht. Das Zielsystem muss eine Kompatible Version der Standard C Library (in diesem Fall die glibc installiert haben) Auf einem Embedded System welches meistens die Busybox als Basis hat kann ich diese Datei vermutlich nicht ausführen, weil hier zumeist nur die uClibc enthalten ist.
Erst wenn ich stattdessen
gcc -o helloworld -static helloworld.c
verwende, kompiliert gcc die Bibliothek glibc mit in mein Programm. Damit ist mein Hello World Programm aber auch schon stolze 911 kB groß. Ich kenne Zeiten da hatte ich in 911kB ein Komplettes Betriebssystem.
Ohne Statische Kompilierung ist also der Betrieb meiner Applikation von der Umgebung in der sie betrieben wird abhängig. In vielen Fällen ist sie das auch bei statischer Kompilierung. Ich kann die Umgebung falsch Konfigurieren, sie durch einen Fehler zerstören, oder ähnliches. Da die Umgebung persistent ist, bleiben eventuelle Fehler bestehen, und eventuell muss ich das System wieder komplett neu aufsetzen um meine Applikation wieder lauffähig zu machen. Vor allem auch weil mir oft nicht bekannt ist, von welchen Bibliotheken / Komponenten / ... meine Applikation abhängig ist.
Wie sieht das also bei Containern aus:
Ich erstelle eine Datei "Dockerfile" neben meiner Applikation:
FROM scratch
COPY helloworld /
CMD ["/helloworld"]
- FROM scratch : Fange mit einem leeren Container an
- COPY helloworld / : Kopiere die datei helloworld in das Hauptverzeichnis des Containers
- CMD ["/helloworld"] : Trage /helloworld als init Befehl des Containers ein
Ich habe also einen Container erstellt, welcher nur das helloworld Programm enthält. Wenn ich aus diesem nun mit
docker build -t helloworld .
ein Image erstelle, dann hat dieses Image nur das Programm selber.
-t helloworld : lege dieses Image unter dem Namen helloworld ab.
Wenn ich dieses dann mit
docker run --rm helloworld
ausführen kann, dann habe ich automatisch sichergestellt, das dieses Image auch auf allen anderen Systemen läuft. Da ich es von allen anderen Komponenten in meiner Betriebsumgebung isoliert habe, habe ich so sichergestellt das das Programm keine externen Abhängigkeiten hat.
--rm : entferne den Container sofort nach dessen Beendigung
Wenn ich helloworld ohne den -static parameter kompiliert hätte, dann wäre dieser Container nicht lauffähig.
Dokumentationslinks: docker build, docker run
Wenn ich ein applikation habe, welche von glibc und anderen standard Komponenten abhängig ist, dann kann ich z.B. das folgende Dockerfile verwenden:
FROM ubuntu
COPY ./helloworld /helloworld
CMD ["/helloworld"]
Wir fangen also nicht mit einem leeren Container an, sondern sagen, das dieser eine kleine Linux Laufzeitumgebung benötigt.
Dies bedeutet: Selbst wenn wir Abhängigkeiten haben, dann müssen wir diese Docker mitteilen, damit die entsprechende Basis verwendet wird. Der Container selbst ist dann wieder auf allen Umgebungen Lauffähig, weil es von dieser Isoliert ist, und alle Abhängigkeiten mitbringt.
Eine Andere Möglichkeit wäre:
FROM busybox
CMD ["echo", "Hello World aus Docker"]
Wir erhalten einen container, der das gleiche Ergebnis liefert, jedoch wird die Komplexität jetzt an die echo Routine die mir der Container selber stellt (FROM busybox) ausgelagert.
Das können wir auch mit komplexeren Dingen tun wie z.B. einem webserver
Hello Web
Wenn wir eine Seite im Web darstellen wollen, dann können wir natürlich relativ schnell einen minimalen Webserver in C schreiben, und wie o.g. einbinden. Das wäre aber nicht sonderlich sinnvoll. Stattdessen verwenden wir einen webserver wie den nginx.
Natürlich können wir nun einen Container mit der Basis ubuntu, oder debian erstellen, und in desem einen Webserver installieren (im Dockerfile kann ich auch mit RUN Befehle ausführen: )
FROM debian
RUN apt-get install nginx -y
......
......
COPY html/index.html
.....
In vielen Fällen lohnt sich aber ein Blick auf den Docker Hub, wo bereits ein (offizielles) nginx image existiert.
Dort ist auch das Dockerfile aus dem dieses Image erstellt wurde hinterlegt:
hier wird z.B. in Zeile 43 das offizielle Debian Repository von nginx eingebunden und in Zeile 87 installiert.
Ich kann für mein Image wiederum diese Image als Basis verwenden:
FROM nginx
COPY html/index.html /usr/share/nginx/html/
Den Pfad wo ich die HTML Datei ablegen muss finde ich wiederrum auf der Seite vom Image (s.o.)
Dieses kann ich wie bisher mit
docker build -t helloworld .
in ein image umwandeln. Durch die o.g. Isolationen muss ich Docker jedoch mitteilen, das ich einen Port welcher innerhalb des Containers geöffnet wird mit -p aussen:innen auf dem Host verfügbar machen will:
docker run -p 80:80 --rm helloworld
Wenn ich den Port nur intern verfügbar machen will, dann kann ich diesen mit -p 127.0.0.1:80:80 auf mein Localhost Interface Einschränken
Folgende Posts
Die Folgenden Themen werde ich noch in weiteren Posts belechten:
- Multi-Container Builds (Dockerfiles die ihre Basis selbst kompilieren)
- Docker-Compose (Applikationen welche aus mehreren Conatinern bestehen)
- Portainer eine grafisch (Web-)oberfläche für die Verwaltung von Containern.