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:
- Im ersten Build-Abschnitt kopieren wir die Sources und die Maven pom.xml Datei und führen den Build mit maven aus. Dazu benötigen wir ein Base-Image mit Java JDK und Maven aus dem maven Repository: maven:3.9.4-eclipse-temurin-17
- Im zweiten Abschnitt definieren wir die Laufzeitumgebung für unsere jar Datei und verwenden dazu die Java 17 JRE (eclipse-temurin:17-jre). Durch die Verwendung der JRE verkleinern wir die Größe des gebauten Images.
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
- metadata.name: Ist hier frei wählbar
- spec.selector.matchLabels: Wird für die interne Auswahllogik des Deployment-Controllers verwendet.
- spec.template.metadata.labels: Wird verwendet, um die vom Deployment erstellten Pods zu kennzeichnen.
- template.spec.containers: Hier tragen wir den Namen des Docker Images (unter "image") sowie einen frei wählbaren Namen des containers
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).