Dieser Artikel beschreibt die grundsätzlichen Möglichkeiten eine Anwendung / einen Service welches im Cluster läuft nach aussen verfügbar zu machen. „Aussen“ heisst dabei, dass ein Client / Webbrowser den Service aufrufen kann. Dabei betrachten wir die Möglichkeiten die Kubernetes und OpenShift bieten.
Ein Pod im Cluster erhält eine interne IP, welche nur im Cluster gültig ist. In dem Beispiel Bild kann Pod A mit Pod B kommunizieren und umgekehrt, aber von aussen kommt man so ohne weiteres an die Pods nicht dran.
Selbst wenn die Kommunikation mit Pods innerhalb des Clusters über ihre interne IP möglich ist, ist es eine schlechte Idee dies zu tun. Denn Pods sind volatil, sie können zu jeder Zeit von einem Node auf einen anderen umgezogen werden, welches dazu führt, dass sie eine neue, private IP erhalten.
Um eine stabile Kommunikation mit dem Pod ermöglichen zu können brauchen wir einen „Service“. Ein Service ist ein Kubernetes API Object welches explizit erstellt werden kann. Dieser bekommt eine statische IP, die solange gültig ist, wie dieses API Object besteht und nicht gelöscht wird. Ein Service ist mit ein oder mehreren Pods verbunden und fungiert als Loadbalancer und Kommunikations-Entrypoint zu den dazugehörigen Pods. Welche Pods zu einem Service gehören, wird über den Selector des Services bestimmt. Nachfolgend ein Auszug aus einem Deployment Manifest wo der Pod mit dem Label app: gvcv-backend gekennzeichnet wird.
Hier nun der Auszug aus dem Service Manifest:
Der selector gibt dabei an, welche Pods als downstream hinter dem Service hängen. Ein Service routet also traffic zu den dahinter liegenden Pods (ein oder mehrere) im loadbalancing Verfahren. Es ist anzumerken, dass es sich hierbei um ein Layer 4 Loadbalancer handelt, d.h. auf Ip und Port Ebene. Der Vorteil von einem Service als „Frontmann“ zu Pods, ist eben die statische IP und die Loadbalancing Funktionalität. Ferner sind Services über ihren Namen im internen DNS auflösbar. Sprich in dem Beispiel unten kann Pod B mit Pod A eben über „ServiceA“ als Hostnamen kommunizieren.
Wir können aber immer noch nicht von aussen mit dem Service kommunizieren, denn auch die IP des Service ist eine interne Cluster IP.
Anzumerken ist, dass die IP des Service keine reguläre HostIp darstellt, sondern eben nur eine „virtuelle“ IP ist. D.h. dass man sie z.B. nicht pingen kann. Spricht ein Pod mit einem Service wird die IP dynamisch auf die IP der dahinter liegenden Pods umgemapped.
Ein Service hat verschiedene „Types“. Defaultmäßig – wenn nicht sonst angegeben – hat der Service den Type : ClusterIP. Damit der Service von aussen erreichbar ist gibt es nun verschiedene Wege. Zum einen kann man bei einem Service den Type : NodePort definieren. Hierbei wird ein Port zwischen 30000 und 35000 auf jedem Node im Cluster geöffnet, der dann den Traffic weiterleitet zu dem zugehörigen Pod oder Pods. Das nachfolgende Bild verdeutlicht einen Nodeport Service.
Jetzt kann man aus dem Internet jeden beliebigen Node des Clusters auf dem vergebenen Nodeport (welches zufällig zwischen 30000 und 35000 liegt, aber auch fest vorgegeben werden kann in der yaml Datei) ansprechen. Ganz gleich, ob der zugehörige Pod auf dem Node liegt oder nicht, der Request wird auf jeden Fall durch die richtigen IPTable rules (welche von Kubernetes auf dem Host automatisch korrekt eingerichtet werden) weitergeleitet zum Pod.
Immerhin kommt man jetzt von aussen an die Pods dran. Aber optimal ist es natürlich nicht. Zum einen muss der Client wissen gegen welchen Node er gehen will und zum anderen was passiert, wenn der Node ausfällt. Einen Ausfallmechanismus hat man so noch nicht. Man könnte auch einen Loadbalancer ausserhalb des Clusters einsetzen, um alle Nodes zu bespielen im Round Robin Verfahren. Aber was wenn Nodes wegfallen oder dazukommen? Dann muss ein Admin immer die Nodes im Loadbalancer pflegen.
Kommen wir zur nächsten Stufe: Ein Service vom Type: Loadbalancer. Dieser Type funktioniert allerdings nur in einer Cloud Umgebung wie GKE, AWS, Azure oder Digital Ocean. Wenn man ein Service vom Type : Loadbalancer definiert sorgt ein Cloud spezifischer Controller dafür, dass ein externer Loadbalancer provisioniert und dieser leitet Requests an alle Nodes im Cluster. Wenn Nodes hinzukommen oder wegfallen wird der Loadbalancer automatisch aktualisiert.
Hier ein Beispiel für ein Service Manifest:
Wenn man mit kubectl get service sich den Service auf der Kommandozeile anschaut, dann stellt man fest, dass die Spalte External-IP einen Wert enthält. Es kann einige Minuten dauern bis der Wert erscheint. Vorher steht dort „Pending“. Es dauert also ne Weile bis der Cloud Provider den Loadbalancer provisioniert hat:
Zumindest haben wir jetzt unseren Service nach aussen bereitgestellt. Bis hierhin funktionieren Kubernetes und OpenShift gleich. Wenn man sich den extern provisionierten Loadbalancer anschaut, dann sieht man, dass dieser Traffic auf Port 80 und 443 annimmt und weiter leitet an Ports zwischen 30000 und 35000. Daraus schliessen wir das der „Loadbalancer“ Type unter der Haube ebenfalls NodePorts nutzt.
Wenn wir davon ausgehen, dass im Enterprise Umfeld viele Services nach aussen „bekannt“ gemacht werden müssen oder eventuell der Cluster on-premise läuft und nicht in der Cloud, dann ist der ServiceType Loadbalancer nicht die richtige Lösung. Denn das setzt voraus, dass wir für jeden Service einen eigenen Loadbalancer provisionieren müssen. Hier kommen Ingress Controller und Ingress Objekte ins Spiel, bzw. Routes in OpenShift. Diese behandeln wir in dem zweiten Teil des Artikels.