Grundlagen von Kubernetes mit Spring Boot und Java

In diesem Tutorial werden wir uns mit den Grundlagen von Kubernetes beschäftigen und mit K3D eine einfache Spring Boot Anwendung in einem lokalen Kubernetes-Cluster implementieren. In einem kleinen Glossar haben wir auch einige grundlegende Konzepte im Zusammenhang mit Containerisierung und Kubernetes gesammelt, wie Pods, Services oder Deployments. Es werden grundlegende Kenntnisse in Docker und Java/Maven vorausgesetzt.

Kubernetes (K8S - K, 8 Buchstaben und ein S) ist eine Open-Source-Plattform zur Container-Orchestrierung. Es wurde ursprünglich von Google entworfen und wird heute von der Cloud Native Computing Foundation (CNCF) gepflegt. Kubernetes wurde entwickelt, um die Herausforderungen bei der Bereitstellung, Skalierung und Verwaltung von containerisierten Anwendungen zu bewältigen. Mit Kubernetes lässt sich auch der Infrastruktur als Code (IaC) Ansatz umsetzen: Die gesamte Anwendungsinfrastruktur und der Bereitstellungsprozess können mithilfe von deklarativen YAML-Dateien definiert werden und auch zB. im Git verwaltet werden.

Um die Beispiele in diesem Tutorial nachzuvollziehen, benötigen wir Docker (https://www.docker.com), k3d (https://k3d.io) und kubectl (https://kubernetes.io/docs/tasks/tools/). Wir implementieren zwar eine kleine Anwendung mit Spring Boot und Java, allerdings dient sie nur zu Demonstrationszwecken und wer sie nicht lokal laufen lassen möchte, kommt auch ohne installiertes JDK zurecht (sonst empfehle ich das OpenJDK von https://adoptium.net).

K3D ist ein Wrapper für K3S - einer minimalistischen Kubernetes Implementierung von Rancher - und eignet sich gut zum Erlernen von Kubernetes, da es einfach lokal installiert werden kann (die einzige Anforderung ist Docker). Es erleichtert aber auch das Testen und Entwickeln von Anwendungen, weil man eben ohne ein vollwertiges Kubernetes-Cluster auskommt.

kubectl ist ein Kubernetes-Befehlszeilentool mit dem wir mit unserem Cluster kommunizieren können.

Kleines Kubernetes Glossar

Wie bei jeder neuen Technologie gibt es eine Reihe von Begriffen, mit denen man sich anfangs vertraut machen muss. Hier eine kleine Zusammenfassung der wichtigsten Fachwörter für Kubernetes:

Cluster: Ein Cluster ist eine Gruppe physischer oder virtueller Maschinen (Nodes), auf denen von Kubernetes verwaltete containerisierte Anwendungen ausgeführt werden. Er besteht aus einer Steuerungsebene (für die Verwaltung des Cluster) und Arbeitsknoten (auf denen Container ausgeführt werden).

Nodes: Nodes sind die einzelnen Maschinen (physisch oder virtuell) innerhalb eines Clusters. Wir können sie weiter in Master-Nodes (Teil der Steuerungsebene) oder Worker-Nodes (Laufumgebung für Container) unterteilen.

Pods: Ein Pod ist die Grundeinheit der Bereitstellung in Kubernetes. Er stellt eine einzelne Instanz eines laufenden Prozesses innerhalb des Clusters dar. Pods können einen oder mehrere Container enthalten, die dieselben Netzwerk- und Speicherressourcen nutzen.

Services: Ein Service ist eine Abstraktion, die einen logischen Satz von Pods und den Zugriff auf diese definiert. Er bietet einen stabilen Endpunkt (IP-Adresse und Port), der für die Kommunikation mit den Pods verwendet werden kann, auch wenn die zugrunde liegenden Pods hoch- oder herunterskaliert oder ersetzt werden.

ReplicaSet: Definiert einen Controller, der sicherstellt, dass eine bestimmte Anzahl identischer Pod-Replikate zu einem bestimmten Zeitpunkt ausgeführt wird. ReplicaSet kümmert sich um ausgefallene oder beendete Pods und ersetzt diese auf der Grundlage definierter Regeln.

Deployment: Ein Deployment ist ein übergeordneter Controller, der die Erstellung und Aktualisierung von ReplicaSets verwaltet. Es bietet deklarative Aktualisierungen für Pods und ermöglicht bei Bedarf Rollbacks zu früheren Versionen.

Namespace: Ein Namespace ist ein virtueller Cluster innerhalb eines Kubernetes-Clusters. Er bietet eine Möglichkeit, Ressourcen wie Pods, Dienste und Bereitstellungen innerhalb eines Clusters zu isolieren und zu partitionieren. Namespaces helfen bei der Organisation und Verwaltung von Anwendungen und Ressourcen.

Container: Ein Container ist ein leichtgewichtiges, eigenständiges und ausführbares Softwarepaket, das alles enthält, was zur Ausführung einer Anwendung benötigt wird.

Beispielanwendung mit Java und Spring Boot

Nach dieser kurzen Einführung, können wir gleich mit der eigentlichen Implementierung anfangen. Zunächst legen wir ein ganz normales Spring Boot Projekt an, dazu verwenden wir am einfachsten https://start.spring.io: Ich verwende die Spring Boot Version 3.1.3 mit Java 17 und wähle JAR-Packaging, da wir möglichst einfach die Applikation standalone laufen lassen wollen. Damit wir in der Applikation auch was sehen können, legen wir einen "RestController" für GET-Requests an:

package com.jberries.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("")
public class DemoController {

	@GetMapping
	public String halloJBerries() {
		return "Hallo JBerries";
	}
}

Im nächsten Schritt "verdockern" wir die Applikation. Dazu erstellen wir ein Dockerfile und definieren ein Multi-stage Build für das Image:

FROM maven:3.9.4-eclipse-temurin-17 as build
WORKDIR /app
COPY pom.xml .
COPY src ./src

RUN mvn -B clean package

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar ./demo.jar

EXPOSE 8080
CMD ["java", "-jar", "demo.jar"]

Der Docker Build läuft dann wie üblich mit: docker build --pull --rm -f Dockerfile -t jbk3dspringdemo:latest . Und um es schnell auszuprobieren, können wir den Container mit dem Image starten über: docker run --rm -d -p 8080:8080/tcp jbk3dspringdemo:latest

Deployment mit K3D

Jetzt können wir die Applikation endlich in Kubernetes deployen. Zu allererst müssen wir den Cluster anlegen mit:

k3d cluster create jberries-demo-cluster

Bevor wir mit dem eigentlichen Deployment anfangen können, müssen wir noch das Docker Image mit unserer Applikation in den Cluster importieren. Sonst würde Kubernetes annehmen, dass wir ein öffentlich verfügbares Image aus der Docker-Registry verwenden möchten und dann mit einem Fehler (ErrImagePull) bei der Erstellung der Pods scheitern.

docker tag jbk3dspringdemo:latest jbk3dspringdemo:1
k3d image import jbk3dspringdemo:1 -c jberries-demo-cluster

Vor dem Importieren haben wir das Image mit der Versionsnummer "1" getaggt: Würden wir das Image mit dem Tag "latest" importieren, so würde K3D beim Hochfahren des Deployments trotzdem nach einer aktuellen Version suchen und dann auch mit dem Fehler ErrImagePull scheitern, weil das Image ja in keiner Registry zu finden ist. Um die verfügbaren Images im Cluster aufzulisten, können wir folgenden Befehl verwenden:

docker exec -it k3d-jberries-demo-cluster-server-0 crictl images

Es ist auch möglich mit k3d eine lokale Registry für den Cluster zu starten, jedoch würde das den Rahmen dieses Tutorials sprengen (https://k3d.io/v5.4.6/usage/registries/#preface-referencing-local-registries).

In einem ersten Ansatz machen wir das Deployment Schritt-für-Schritt über die Befehlszeile mit kubectl. Als erstes definieren wir das "Deployment" mit dem Namen "jberries-demo-deployment" mit dem zuvor importierten Docker-Image "jbk3dspringdemo:1" und öffnen den Port 8080:

kubectl create deployment jberries-demo-deployment --image jbk3dspringdemo:1 --port 8080

Um den Status des Deployments zu überprüfen, führen wir kubectl get deployments aus oder kontrollieren am besten die angelegten Pods mit kubectl get pods .

Anschließend müssen wir die Applikation bzw. das Deployment "jberries-demo-deployment" als Service mit dem Typ LoadBalancer verfügbar machen, sonst wäre sie nur innerhalb des Clusters erreichbar:

kubectl expose deployment jberries-demo-deployment --port=8080 --target-port=8080 --type=LoadBalancer

Um die Applikation auf dem localhost sehen zu können, müssen wir noch eine Portweiterleitung einrichten:

kubectl port-forward service/jberries-demo-deployment 8080:8080

Zum Löschen des Deployments, der Pods und des Services führen wir aus:

kubectl delete deployment jberries-demo-deployment
kubectl delete service jberries-demo-deployment

Wir können natürlich über k3d cluster delete jberries-demo-cluster auch den kompletten Cluster löschen.

Konfiguration über YAML Dateien

In unserem Fall und vor allem wegen der Einfachheit der Anwendung könnten wir ohne Probleme für alles die Kommandozeile benutzen. Für komplizierte Setups ist es nicht gedacht und man hält die Konfiguration lieber in YAML Dateien fest. Der große Vorteil ist, dass man sie dann neben dem Applikations-Code in zB. Git verwalten kann.

Wir legen für die Konfiguration des "Deployments" eine deployment.yaml Datei mit folgendem Inhalt an:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jberries-demo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jberries-demo-deployment
  template:
    metadata:
      labels:
        app:  jberries-demo-deployment
    spec:
      containers:
        - name: jberries-demo-container
          image: jbk3dspringdemo:1
          ports:
            - containerPort: 8080

Und wie schon im ersten Beispiel benötigen wir auch noch einen LoadBalancer-"Service", den wir in der Datei "service.yaml" festlegen:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: jberries-demo-deployment
  name: jberries-demo-deployment
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: jberries-demo-deployment
  type: LoadBalancer

Mit kubectl apply können wir die Resourcen in unseren Kubernetes Cluster einfügen:

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

Wir können uns die Arbeit noch ein wenig vereinfachen und die komplette Konfiguration unserer Anwendung in nur eine Datei schreiben: Kubernetes unterstützt YAML-Dateien mit mehreren Dokumenten, so dass man mehrere Ressourcen in einer einzigen YAML-Datei definieren kann. Jede Ressource wird durch "---" (3 Mal das Minus-Zeichen) getrennt. Damit müssen wir nur noch ein Mal in der Kommandozeile: kubectl apply -f demo.yaml aufrufen (in der Datei demo.yaml befindet sich die Konfiguration für das Deployment und den Service).

Kubernetes ist einfachEinmal habe ich versucht, jemandem Kubernetes zu erklären... Dann haben wir es beide nicht (mehr) verstanden.