Dabei werden einfache Container-Konstrukte betrachtet, um die grundlegenden Prinzipien und Unterschiede verständlich darstellen zu können. Komplexere Anwendungsfälle, wie zum Beispiel die Zusammenarbeit eines containerisierten Webserver mit einem Datenbank-Container, werden nicht explizit betrachtet. Die hier vermittelten Grundlagen sind jedoch beim Verstehen und Erstellen eigener Container-Projekte hilfreich.
Im folgenden Artikel werden auch Code-Beispiele für Docker genutzt. Wir beziehen uns dabei auf Docker unter Linux in der Version 20.10.8. Die Herangehensweise für Docker-Desktop kann sich davon unterscheiden. Bei OpenShift (hier in Version 4.7) werden gegebenenfalls Beispiele ebenfalls per Code, unter Nutzung des Kommandozeilenbefehls „oc“, angegeben. Da der Artikel unserer Reihe „Docker vs. OpenShift“ auf grundlegenden Begriffen wie Pod, Container oder Images aufbaut, setzen wir an dieser Stelle voraus, dass die Begriffe und deren Kontext bekannt sind. Anderenfalls empfehlen wir, am Anfang der Reihe zu starten.
Background
Um ein das Thema Networking im Container-Umfeld einzusteigen, werfen wir einen kurzen Blick auf Virtual Machines (VM’s), um zu verstehen, welche grundsätzlichen Möglichkeiten es gibt, verschiedene Netzwerkkonfigurationen zu realisieren. Folgende Optionen sind möglich:
- Erstellung vom Host isolierter Netzwerke, die mittels Bridging und Network Address Translation (NAT) genutzt werden können
- Direkte Nutzung des Host-Netzwerks
- Erstellung isolierter Netzwerke, die mehrere VM’s auf einem Host oder über mehrere Hosts hinweg verbinden
Mit Containern sind diese Netzwerkkonfigurationen ebenso möglich. Jedoch haben VM’s jeweils ihren eigenen Netzwerk-Stack, den sie für sich selbst vollständig nutzen. Das ist bei Containern anders, denn diese teilen sich den Netzwerkstack mit ihrem Container-Host beziehungsweise mit dessen Ressourcen.
Das kann mitunter zu Problemen führen, die anhand eines einfachen Beispiels erläutert werden sollen: In den Grenzen der Ressourcen eines Container-Hosts können viele containerisierte Instanzen erstellt werden. Zum Beispiel könnten mehrfach httpd-Container erstellt werden. Dabei stößt man sofort auf das Problem, dass es an einem Host nur einen Port 80 oder nur einen Port 443 zu belegen gibt. Um all die Container-Instanzen bedienen zu können, ohne mehrfach den Port 80 zu belegen, bedarf es einer Lösung. Für Container wurden gleich zwei entwickelt.
Docker arbeitet zur Organisation von Netzwerkanbindungen mit dem sogenannten „Container Networking Model“ (CNM). OpenShift dagegen arbeitet mit dem Container Networking Interface (CNI). Um es vorwegzunehmen: CNI hat sich als De-facto-Standard durchgesetzt, da es von Anbietern wie Google – und somit von dem weitverbreiteten Kubernetes – eingesetzt wird. Dem entgegen steht allein Docker, welches CNM nutzt. Zwar hat Google versucht, CNM in die eigenen Produkte zu implementieren, dies aber aufgegeben und sich somit CNI gewidmet.[1]
Networking in Docker
Sehen wir uns an, wie Docker Netzwerkanbindungen realisiert.
Container bieten verschiedene Services im Netzwerk an. Um diese erreichen zu können, gibt es unter Docker mehrere Möglichkeiten. Im Überblick sind dies folgende Netzwerktypen:
- bridge
- host
- overlay
- macvlan
- ipvlan
- none
Abhängig vom Netzwerktyp kann auch definiert werden, wie Ports bereitgestellt werden:
- Expose
- Publish
Da sowohl Expose als auch Publish in den verschiedenen Netzwerktypen unterschiedlich genutzt werden, ist es wichtig auf beide näher einzugehen, um sie und ihre Verwendung besser zu verstehen.
Expose stellt den angegebenen Port innerhalb des Netzwerkes, in dem sich der Container befindet, zur Verfügung. Hier liegt das Augenmerk auf: Innerhalb des Netzwerkes des Containers. Warum das wichtig ist, wird sich bei der Betrachtung von Host- und Bridged-Network zeigen.
Ob ein Port exposed werden soll, kann bereits in der Image-Erstellung mittels Dockerfile angegeben werden. Falls dies nicht bereits geschehen ist, kann die Option –expose an den docker run-Befehl angehängt werden.
Publish stellt einen angegebenen Port über das Container-Netzwerk hinaus zur Verfügung, also am Port des Container-Hosts, wo dieser veröffentlicht wird. Man kann einen Port publishen, ohne ihn zuvor explizit exposed zu haben. Publishen kann man nur mittels des docker run commands. Sind ein oder mehrere Ports an einem Container exposed, kann man auch mittels der Option — publish-all alle exposed Ports an zufälligen Ports des Hosts publishen.
Unabhängig davon, ob man einen Port exposed oder published, ist in beiden Fällen die jeweilige Konfiguration von Container-Netzwerk und Host-Firewall zu beachten. Dies läuft über integrierte “Mechanismen” (per Default) von allein. Schlussendlich bedeutet dies, dass die Konfigurationen automatisch erfolgen, nicht aber entfallen(!).
Im Folgenden wird das Bridge-Netzwerk genauer beschrieben, ebenso wie die Funktionalität des Host-Netzwerks. Alle anderen Typen werden nur kurz in einer Übersicht dargestellt.
Bridge-Netzwerk
Bridge-Netzwerke sind der Standardtyp unter Docker und machen es uns am einfachsten, denn per default ist bereits ein Bridge-Netzwerk eingerichtet, verteilt automatisch IP-Adressen an die angeschlossenen Container und nutzt NAT um den Datenverkehr zwischen Container und Host zu realisieren. Wird hier ein Port exposed, führt es dazu, dass dieser sowohl in dem Container-Netzwerk verfügbar und von anderen Containern im selben Netzwerk nutzbar ist, als auch vom Container-Host selbst. Clients des Hosts können den Port nicht erreichen.
Beim Publishen wird der Port des Containers an einen Port des Container-Hosts veröffentlicht und steht somit jedem weiteren Teilnehmer im Netzwerk zur Verfügung.
Ein Bridge-Netzwerk kann gut dazu genutzt werden, um ein komplexeres Konstrukt von Containern aufzubauen, wie zum Beispiel eine Webanwendung, die einen httpd-, MySQL- und Redis-Container nutzt. Mittels des Bridge-Netzwerks können die Container untereinander kommunizieren, dabei aber nur den Container/Port nach außen publishen, welcher tatsächlich von außen erreichbar sein muss.
Möchte man ein eigenes Bridge-Netzwerk erstellen, um zum Beispiel dem Netzwerk einen projektspezifischen Namen zu vergeben oder einfach um es vom Default-Netz abzugrenzen, kann man docker network create nutzen. Auch beim Erstellen neuer Netzwerke wird per Default ein Bridge-Netzwerk etabliert. Daher muss hier nichts weiter angegeben werden. Wenn man seine Konfiguration zum Beispiel in einem Dockerfile hinterlegt, kann es hilfreich sein, den Netzwerktyp mit anzugeben, um so schneller zu erkennen, welcher Typ genutzt werden soll. Dies ist mittels der Kommandozeilenoption – -driver möglich. Für ein Bridge-Netzwerk lautet der Kommandozeilenbefehl wie folgt:
docker network create - - driver=bridge <Name>
Zur Vergabe von IP-Adressen wird automatisch das interne IP-Address-Management-Plugin (IPAM-Plugin) genutzt. Andere Plugins sind zwar möglich, aber leider nicht sehr verbreitet. Daher empfiehlt es sich immer, das interne zu nutzen.
Host-Netzwerk
Das Host-Netzwerk verbindet den jeweiligen Container direkt mit dem Host, ohne ein isoliertes (Bridge-)Netzwerk zu nutzen. Somit kann der Container direkt auf die IP-Adresse des Hosts beziehungsweise auf das angebundene Netzwerk zugreifen. Einen Port zu publishen ist hier nicht möglich, da er bereits direkt am Host zur Verfügung steht. Er muss lediglich exposed werden. In dieser Konstellation ist zwingend angeraten, die entsprechende Firewall-Konfiguration zu beachten. Denn auch wenn die jeweilige Anwendung in einem Container läuft, verhält sie sich so, als wäre sie direkt auf dem Host installiert. Auch hier ist zu beachten, welche Container unter Umständen auf einen Container im Host-Netzwerk zugreifen müssen, da die Container nur von außen erreichbar sind.
Bei der Installation von Docker wird ein Host-Netzwerk gleich mit erstellt. Ein weiteres Netzwerk von diesem Typ zu erstellen ist nicht möglich, da es eben nur einen Host gibt.
Weitere Netzwerktypen
- overlay
- dient der Container-zu-Container-Kommunikation
- erstreckt sich dabei über mehrere Container-Hosts
- Beispiel: Nutzung mehrerer Webserver mit vorgeschaltetem Loadbalancer um Anfragen auf mehrere Container unterschiedlicher Hosts zu verteilen
- macvlan
- stellt Containern eine MAC-Adresse zur Verfügung
- geeignet für Legacy-Anwendungen beziehungsweise Anwendungen auf OSI Layers 2
- ein externer Blog liefert dafür ein interessantes Beispiel[2]
- Ipvlan Netzwerke
- ermöglicht die Nutzung von VLANs an Netzen/Containern
- Nutzung „unabhängig“ vom VLAN des Hosts möglich
- none
- Es wird kein Netzwerk zugewiesen, somit läuft der Container vollständig lokal und isoliert
Alle Mitglieder der Gruppe „docker“ können eigene Netze anlegen.
Networking in OpenShift
Nach diesem Ausflug in das Networking von Docker soll es darum gehen, wie dieses Thema in OpenShift umgesetzt ist. Zu Anfang des Artikels wurde bereits CNI kurz erwähnt. Nun soll hier noch darauf eingegangen werden, welche Bedeutung es inzwischen erlangt hat. CNI wird von Google in Kubernetes eingesetzt. Kubernetes wiederum wird in OpenShift (RedHat), der HPE Container-Plattform oder auch dem Plattformangebot „Tanzu“ von VMware genutzt. Durch diese starke Verbreitung von Kubernetes[3] ist CNI zum De-facto-Standard avanciert. Da CNI stark auf eine durch Plugins erweiterbare Architektur setzt, hat dessen Verbreitung auch zur Folge, dass fortlaufend Arbeit in die (Weiter-)Entwicklung der Plugins investiert wird.
In OpenShift trifft man nicht direkt auf CNI, da es in Kubernetes integriert ist und OpenShift wiederum Kubernetes integriert. So ist es kein Wunder, dass das Thema Networking an sich nur stark abstrahiert in OpenShift sichtbar wird. Das hat den Vorteil, dass der ein oder andere Schritt in OpenShift entfallen kann, der noch mit Docker gegangen werden muss. Es soll nicht bedeuten, dass alles unter OpenShift einfacher ist. Der Abstraktionsgrad hat auch zur Folge, dass bestimmte Konstellationen aufwendiger sein können. Zum Beispiel muss man unter Docker kein Projekt erstellen, um einen Container (beziehungsweise Pod) starten zu können. Aber um ein vorhandenes Projekt oder dessen Applikation von außen erreichbar zu machen, reicht es, diese Kommandozeile zu nutzen:
oc expose service/<Name>
Das lässt sich auch schnell nachvollziehen, indem man folgende Schritte ausführt, um ein Beispielprojekt mit Hilfe der bereits mitgelieferten Sample-Projekte zu erstellen:
oc new-project <new project>
Dieser Befehl erstellt das Projekt.
oc new-app <httpd-sample>
Mit dem Befehl wird in dem Projekt eine Applikation mit Hilfe des httpd-Samples erstellt
Anschließend muss nur noch die obige Kommandozeile genutzt werden, um die Applikation von außen erreichbar zu machen. Dies ist ausreichend, damit das httpd-Beispiel funktioniert. Um mehr muss man sich nicht zwingend kümmern.
Denn mit OpenShift betrachten wir nicht einzelne Container, sondern den Pod selbst. Dieser Pod ist mittels einer IP für die internen OpenShift-Dienste erreichbar. Unter OpenShift wird diese Erreichbarkeit als “Service” bezeichnet. Services werden allgemein in OpenShift benutzt, um den Netzwerkverkehr innerhalb des Clusters zwischen den Pods zu regeln (Stichworte: Loadbalancing, Session-Affinität). Dazu werden dem Service zugewiesene Nachrichten an Pods zugestellt, unabhängig davon wie viele Pods der Anwendung gerade aktiv sind (horizontale Skalierung).
Um den Service auch von extern (also von außerhalb eines OpenShift-Clusters) erreichbar zu machen, wird eine Route benötigt. Sie beinhaltet Information für das Routing, um zu definieren, wie der Service erreichbar ist. Diese Informationen werden auch im Kontext der Komponente „Ingress“ genutzt. Ingress ist der Teil eines OpenShift-Clusters, der sich darum kümmert, den Service nach außen über einen Namen zu präsentieren. Dies beinhaltet auch, dass ein Reverse-Proxy genutzt wird und an dieser Stelle SSL-Verbindungen terminiert werden – also einen Endpunkt haben.
Der Prozess, einen Pod (beziehungsweise die dahinterliegende Applikation) von außen erreichbar zu machen, ist hier nur verkürzt dargestellt, da es sich um einen komplexen Prozess handelt, der in OpenShift abstrahiert, kompakt und einfach zu nutzen ist. Schon in einem reinen Kubernetes-Cluster sind mehr Schritte nötig, um eine Applikation extern erreichbar zu machen als in OpenShift. Wichtig ist jedoch, dass wir hier nur ein einfaches Beispiel behandelt haben, für das es in einem OpenShift-Cluster auch eine einfache Lösung gibt.
In dem Teil des Artikels, der sich mit Docker beschäftigt, haben wir aufgezeigt, welche Netzwerk-Typen es in Docker zur Nutzung gibt. Auch wenn OpenShift auf CNI anstelle von CNM setzt, gibt es dieselben Typen auch in einem OpenShift-Cluster, denn auch hier können wir ein macvlan-Netz anlegen. Allerdings ist dies aufgrund der Ausrichtung von OpenShift als Cluster-Anwendung eine Aktion, die nur ein Cluster-Administrator ausführen kann und deren Konfiguration angepasst an das OpenShift-Cluster ist.
Beim Thema Networking erkennen wir deutlich, dass sich die Anwendungen nicht in den grundlegenden Prinzipien unterscheiden, sondern viel mehr in der Art und Weise, wie diese umgesetzt sind und genutzt werden. Bei der Entwicklung von Applikationen spielt das eine nicht unerhebliche Rolle: Für einen ersten Proof of Concept kann man gern auf Single-Host-Lösungen zurückgreifen, dort eigene Netzwerke konfigurieren und nutzen. Docker zum Beispiel ist durch die overlay-Netzwerke dann auch in der Lage, eine Multi-Host-Umgebung zu realisieren. Allerdings ist es eben „nur“ eine Multi-Host-Umgebung und keine Cluster-Applikation. Eine Cluster-Applikation kann mittels eines Services verschiedenen Containern oder Pods entsprechende Ressourcen zuweisen, so dass dieser immer auf die gleichen Ressourcen Zugriff hat, egal wo er gerade läuft. Das ist keine komplexe Konfiguration, sondern lediglich mit einem Kommandozeilenbefehl realisierbar.
Quellen / weiterführende Links
[1] https://kubernetes.io/blog/2016/01/why-kubernetes-doesnt-use-libnetwork/
[2] https://blog.carroarmato0.be/2020/05/08/exposing-podman-container-on-the-network/
Weiterführende Artikel
https://kubernetes.io/docs/concepts/
https://docs.openshift.com/container-platform/4.8/networking/understanding-networking.html