kind-Cluster fuer die Jenkins-Test-Stufe
Zielgruppe: DevOps. Phase 11. Ergaenzt jenkins-pipeline.md - macht die Stufe "Deploy nach Test" zu einem ECHTEN Deployment statt eines Dry-Runs. Fuer einen produktionsnahen, dauerhaften Cluster (CRC/OKD, Cloud) bleibt kubernetes-guide.md zustaendig - beide Dokus bewusst getrennt, siehe dort fuer die Begruendung.
Warum kind und nicht CRC/OKD
kubernetes-guide.md
dokumentiert einen konkreten, mehrstuendigen CRC-Versuch, der an abgelaufenen
Bundle-Zertifikaten, einer zu alten CRC-Version und RAM-Konflikten mit der laufenden
Compose-Infrastruktur scheiterte. kind ("Kubernetes IN Docker" - der Name; hier mit Podman
als Provider betrieben, KIND_EXPERIMENTAL_PROVIDER=podman) umgeht diese Probleme
strukturell: statt einer eigenen VM (Hyper-V) ist ein kind-"Node" schlicht ein normaler
Podman-Container, der einen kompletten Kubernetes-Node simuliert. Kein separates
VM-Bootstrapping, kein Zertifikatsproblem, deutlich geringerer Ressourcenbedarf - laeuft in der
bereits vorhandenen Podman-Installation. Der Preis: kind ist bewusst ein Wegwerf-Cluster
fuer Test-/CI-Zwecke, kein produktionsnahes Setup (siehe "Bewusst ausserhalb des Scopes" unten).
Installation
scoop install kind # Windows, Paketmanager bereits fuer die JDK dieses Projekts genutzt
# Alternativen: choco install kind / go install sigs.k8s.io/kind@latest / siehe kind.sigs.k8s.io
kind version # z. B. kind v0.32.0
# kind muss den Podman-Provider nutzen (dieses Projekt hat kein Docker). Die Env-Var bei JEDEM
# "kind ..."-Aufruf setzen - am besten dauerhaft ins Profil:
$env:KIND_EXPERIMENTAL_PROVIDER = "podman" # PowerShell
# export KIND_EXPERIMENTAL_PROVIDER=podman # bash
Voraussetzung: die Podman-Machine laeuft bereits (podman machine start, dieselbe, die auch
infra/docker-compose.yml betreibt) - kind braucht keine zusaetzliche Virtualisierungsschicht.
Rootless-Podman-Hinweis: fuer kind wird ggf. ein systemd-cgroup-v2-Delegate benoetigt - siehe
kind.sigs.k8s.io/docs/user/rootless.
Cluster erzeugen
kind create cluster --config infra/kind/kind-config.yaml --name library-test
kubectl cluster-info --context kind-library-test
infra/kind/kind-config.yaml ist bewusst ein
Einzel-Knoten-Cluster (Lernprojekt/Test-Stufe, keine Multi-Node-Redundanz noetig) ohne
extraPortMappings - Zugriff erfolgt ueber kubectl/kubectl logs, nicht ueber den Browser
(siehe "Bewusst ausserhalb des Scopes" unten). kind create cluster schreibt automatisch einen
neuen Kontext (kind-library-test) in die lokale kubeconfig und aktiviert ihn - kubectl zeigt
danach ohne weiteres Zutun auf den neuen Cluster.
Wie kind-Pods die bestehende Compose-Infrastruktur erreichen
Bewusste Entscheidung gegen einen Ausbau des Helm-Charts um Bitnami-Postgres/-Kafka/-Redis/
-Keycloak-Subcharts (das waere kubernetes-guide.mds "Vertiefungsaufgabe" auf einen Schlag
umgesetzt - deutlich groesserer Scope als hier angefragt). Stattdessen bleibt die komplette
Infrastruktur unveraendert in Podman Compose, und die acht App-Services laufen in kind als
einzige neue Schicht. Erreichbarkeit ueber host.containers.internal (Podmans DNS-Adresse fuer
"der Host, auf dem Podman laeuft"; Podman legt zusaetzlich host.docker.internal als Alias an).
Dazu die bereits von docker-compose.yml exponierten Host-Ports:
Stolperstein Podman + kind: anders als Docker Desktop injiziert Podman
host.containers.internalnur in den kind-Node-Container, nicht automatisch in die Pods darin. Loest ein Pod den Namen nicht auf, hilft eine der ueblichen Abhilfen:--add-host=host.containers.internal:host-gatewayist beim kind-Node nicht direkt setzbar - stattdessen entweder die feste Gateway-IP des Podman-Netzwerks (podman network inspect kind->gateway) inenv/kind-values.yamleintragen, oderextraPortMappings+ einenNodePortnutzen. Fuer dieses Lernprojekt bleibt der kind-Pfad bewusst "nur wennKUBECONFIG_TESTwirklich auf einen kind-Cluster zeigt" (siehe unten) - die genaue Netz-Verdrahtung ist dann Teil des Setups auf der jeweiligen Maschine.
| Infrastruktur | Erreichbar unter | Bereits vorhandener Host-Port? |
|---|---|---|
| PostgreSQL | host.containers.internal:5432 |
Ja, unveraendert |
| Redis | host.containers.internal:6379 |
Ja, unveraendert |
| RabbitMQ | host.containers.internal:5672 |
Ja, unveraendert |
| Keycloak (JWKS-Abruf) | host.containers.internal:8180 |
Ja, unveraendert |
| Kafka | host.containers.internal:9094 |
Neu - dritter Listener, siehe unten |
Gesetzt wird das ueber ein gemeinsames Overlay,
infra/helm/values/env/kind-values.yaml, das
als vierte -f-Datei hinter der Service- und der Test-Overlay-Datei uebergeben wird (siehe
"Deploy-Kette" unten).
Stolperstein: Kafka braucht einen dritten Listener
Der bestehende PLAINTEXT_HOST-Listener in docker-compose.yml advertised sich als
localhost:9092. Ein Kafka-Client verbindet sich zwar initial erfolgreich zu einer beliebigen
erreichbaren Adresse, bekommt dann aber per Metadata-Response localhost:9092 als tatsaechliche
Broker-Adresse genannt und versucht, dorthin ERNEUT zu verbinden (Kafkas
Broker-Discovery-Protokoll). Aus einem kind-Pod heraus zeigt "localhost" auf den Pod selbst,
nicht auf den Podman-Host - die zweite Verbindung schlaegt fehl, obwohl die erste erfolgreich
war (ein tueckischer Fehler: er sieht wie ein zeitweiliges Netzwerkproblem aus, ist aber
strukturell).
Loesung: ein DRITTER Kafka-Listener (KIND_HOST, Port 9094), zusaetzlich zu den bestehenden
zwei, NICHT als Ersatz - der bestehende PLAINTEXT_HOST-Listener fuer lokale IDE-Prozesse bleibt
unveraendert, um diesen bereits funktionierenden Weg nicht zu gefaehrden:
# infra/docker-compose.yml, kafka-Service (Auszug)
KAFKA_LISTENERS: PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092,KIND_HOST://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,KIND_HOST://host.containers.internal:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,KIND_HOST:PLAINTEXT
env/kind-values.yaml setzt entsprechend SPRING_KAFKA_BOOTSTRAP_SERVERS: host.containers.internal:9094 fuer alle Services, die Kafka nutzen.
Stolperstein: Helm-Values-Layering braucht Maps statt Listen
Ein gemeinsames kind-Overlay fuer env (wie bei test-values.yaml/qs-values.yaml) war mit
der urspruenglichen Chart-Struktur nicht moeglich: env war eine LISTE
([{name: X, value: Y}, ...]), und Helm merged Listen zwischen mehreren -f-Dateien NICHT
Element-fuer-Element - eine spaeter uebergebene Datei ERSETZT die komplette Liste. Ein
env-Overlay haette also den kompletten, bereits vorhandenen env-Block jedes der 8 Services
erneut vollstaendig duplizieren muessen.
Loesung: env von Liste auf Map umgestellt ({SPRING_X: "Y"} statt
[{name: SPRING_X, value: "Y"}]). Maps werden zwischen -f-Dateien SCHLUESSELWEISE (und sogar
verschachtelt, siehe unten) gemerged - eine spaetere Datei ueberschreibt nur passende Schluessel,
der Rest bleibt erhalten.
# infra/helm/library-service/templates/deployment.yaml - vorher/nachher
# vorher (Liste):
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
# nachher (Map):
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
probes und resources waren in diesem Chart schon immer verschachtelte Maps (nie Listen) -
Helms Merge-Verhalten gilt genauso fuer sie, deshalb kann kind-values.yaml weiter unten auch
einzelne Unter-Schluessel wie probes.liveness.initialDelaySeconds ueberschreiben, ohne
Geschwister-Schluessel wie probes.liveness.periodSeconds anzutasten (siehe
pattern-katalog.md fuer den Katalogeintrag zu diesem
Muster).
SPRING_DATASOURCE_URL bleibt trotzdem je Service in einer eigenen, winzigen Datei
(env/kind/<service>-values.yaml) - der Datenbankname unterscheidet sich pro Service in
derselben JDBC-URL-Zeichenkette, ein einzelner Env-Var-Schluessel kann nicht TEILWEISE
ueberschrieben werden.
Deploy-Kette
helm upgrade --install catalog-service infra/helm/library-service \
-f infra/helm/values/catalog-service-values.yaml \
-f infra/helm/values/env/test-values.yaml \
-f infra/helm/values/env/kind-values.yaml \
-f infra/helm/values/env/kind/catalog-service-values.yaml \
-n library-test --create-namespace --set image.tag=<git-sha>
Reihenfolge ist bewusst: service-spezifisch → umgebungsweit (Test) → kind-weit → kind-spezifisch
je Service - jede spaetere Datei ueberschreibt nur die Schluessel, die sie tatsaechlich setzt.
api-gateway und config-server haben kein env/kind/<service>-values.yaml (kein
SPRING_DATASOURCE_URL, siehe infra/helm/values/README.md) - das
Jenkinsfile prueft das per fileExists(...), bevor es die vierte -f
anhaengt.
Vorher noetig: die DB-Zugangsdaten als Kubernetes-Secret (wie in kubernetes-guide.md beschrieben, gilt identisch fuer den kind-Namespace):
kubectl create secret generic catalog-service-db-credentials -n library-test \
--from-literal=SPRING_DATASOURCE_USERNAME=library \
--from-literal=SPRING_DATASOURCE_PASSWORD=library
Image im Cluster verfuegbar machen - kind hat keine eigene Registry, kind load docker-image (das Subkommando heisst weiterhin so) kopiert mit
KIND_EXPERIMENTAL_PROVIDER=podman ein lokal gebautes Image aus Podmans Storage direkt in den
Node-Container:
podman build -f library-platform/Dockerfile --build-arg MODULE=catalog-service \
--build-arg PORT=8080 -t library/catalog-service:<tag> library-platform
kind load docker-image library/catalog-service:<tag> --name library-test
Im Jenkinsfile uebernimmt das loadImageIntoKindIfClusterAvailable(...)
automatisch vor jedem helm upgrade der Test-Stufe - nur wirksam, wenn das
KUBECONFIG_TEST-Credential tatsaechlich auf einen kind-Cluster zeigt (siehe
"Jenkins-Anbindung" unten), sonst greift ohnehin der Dry-Run-Fallback aus
jenkins-pipeline.md.
Stolperstein: CPU-Drosselung macht Spring-Boot-Kaltstarts extrem langsam
Die urspruenglichen Test-Overlay-Werte (env/test-values.yaml: 300m CPU-Limit) waren fuer einen
dedizierten Test-Cluster kalkuliert. Unter kind reicht das auf der Referenzmaschine nicht:
kind selbst ist bereits eine Container-in-Container-Schicht (Podman-Machine/WSL2 → kind-Node-
Container → Pod) - der zusaetzliche Virtualisierungs-Overhead macht das Standard-Limit spuerbar
zu knapp. Gemessen am reinen Spring-Data-Repository-Scanning (ein einzelner, isoliert messbarer
Startphasen-Schritt):
| CPU-Limit | Dauer fuer Repository-Scanning |
|---|---|
| 300m (Standard-Test-Overlay) | 16,9 Sekunden |
| 1500m (kind-Overlay) | 0,4-0,6 Sekunden |
Ueber 25x langsamer unter dem Standard-Limit - und das ist nur EIN Startphasen-Schritt von vielen (DataSource, Flyway, JPA/Hibernate, Kafka-Client, JWKS-Abruf kommen alle noch dazu). In der Praxis bedeutete das: der Pod wurde von der Liveness-Probe (Standard-Delay 30s) staendig neu gestartet, BEVOR die Anwendung ueberhaupt fertig hochgefahren war - ein CrashLoopBackOff, der wie ein Anwendungsfehler aussieht, aber reine Ressourcen-Ursache hat.
Loesung: env/kind-values.yaml erhoeht CPU (500m/1500m statt 50m/300m) und Speicher (512Mi
statt 384Mi - mehr CPU-Zuteilung bedeutet mehr parallele GC-/JIT-Compiler-Threads, was
zusaetzlichen Speicher jenseits des Heaps braucht; ohne diese Erhoehung endete ein Pod mit
Exit-Code 137) sowie grosszuegigere Probe-Delays (Liveness 240s, Readiness 90s). Diese Zahlen
sind bewusst am beobachteten WORST CASE bemessen, nicht am Durchschnitt: auf derselben
Referenzmaschine schwankte die Gesamt-Boot-Zeit zwischen ca. 22 Sekunden (Host im Leerlauf) und
ueber 200 Sekunden (Host durch parallele Container-Builds/Image-Loads ausgelastet) - keine feste
Zahl ist hier "richtig", nur eine mit ausreichend Sicherheitsmarge.
Stolperstein: ein zweiter, unabhaengiger Bug wurde dabei aufgedeckt
Der allererste echte Deploy-Versuch deckte einen bis dahin unbemerkten Schema-Bug in
catalog-service auf: BookEntity.reservationCount (@Column(nullable = false)) hat keine
entsprechende Spalte in der Flyway-Migration
V1__init_catalog_schema.sql -
Hibernates Schema-Validierung (ddl-auto=validate) lehnte den Start folgerichtig mit
missing column [reservation_count] in table [books] ab.
Derselbe Bugtyp wie ein bereits dokumentierter Fund weiter oben in genau dieser
Migrationsdatei (author_order, siehe Kommentar dort): eine Spalte, die eine JPA-Entity
verlangt, aber die Migration nie angelegt hat - unbemerkt, weil die Testcontainers-
Integrationstests, die das sonst gefangen haetten, auf dieser Maschine damals wegen der
dokumentierten Docker-Desktop-npipe-Flakiness nie zuverlaessig durchliefen (inzwischen ueber
Podman geloest, siehe docker-compose-guide.md).
Erst der erste ECHTE kind-Deploy - die
erste Gelegenheit, ddl-auto=validate gegen eine wirklich frische Postgres-Instanz laufen zu
lassen - deckte es auf. Gefixt nach demselben, bereits etablierten Muster: Spalte direkt in
V1 ergaenzt (nicht als separate V2 - V1 wurde nie gegen eine echte, dauerhafte Datenbank
angewendet, siehe Kommentar in der Migrationsdatei selbst).
Die Lehre ist dieselbe wie beim ersten Fund, nur diesmal durch kind statt durch einen
zufaelligen Testlauf bestaetigt: ein Cluster, der wirklich deployt, findet Bugs, die ein
Dry-Run strukturell nicht finden kann - genau das Argument fuer diese ganze Phase 11.
Jenkins-Anbindung
kind create cluster erzeugt automatisch eine kubeconfig fuer den neuen Kontext. Um sie als
Jenkins-Credential zu hinterlegen:
kind get kubeconfig --name library-test > kubeconfig-test.yaml
Unter "Manage Jenkins → Credentials" ein Credential vom Typ "Secret file" mit der ID
KUBECONFIG_TEST anlegen (Datei: kubeconfig-test.yaml) - genau die ID, die
loadImageIntoKindIfClusterAvailable(...) und deployOrDryRun(...) im
Jenkinsfile erwarten. Bewusst NICHT in casc.yaml vorbelegt (ein
Kubeconfig ist ein Geheimnis, siehe PR-Checkliste, Security - dieselbe Begruendung wie beim
GitHub-Token in jenkins-pipeline.md).
Wichtiger Unterschied zu einem "echten" Cluster: eine kind-Kubeconfig zeigt per Default auf
127.0.0.1:<zufaelliger-Port> - erreichbar von der Maschine, auf der kind create cluster lief,
aber NICHT unbedingt vom Jenkins-Container aus (der selbst als Container laeuft, siehe
shared/enterprise-infrastructure/docker-compose.yml, jenkins-Service). Da der Jenkins-Container ueber den
Podman-Socket-Mount ohnehin vollen Host-Podman-Zugriff hat (siehe "Vertiefungsaufgaben" in
jenkins-pipeline.md), funktioniert kubectl/helm aus dem Container heraus trotzdem - die
kind-Node-Container sind fuer Host-Podman sichtbar, das der Jenkins-Container mitbenutzt.
Manueller Test-Deploy (ohne Jenkins, zur Kontrolle)
# 1. Cluster erzeugen (falls noch nicht geschehen)
kind create cluster --config infra/kind/kind-config.yaml --name library-test
kubectl cluster-info --context kind-library-test
# 2. Infrastruktur muss laufen - kind ersetzt sie NICHT, siehe oben
cd infra && podman compose up -d postgres redis kafka rabbitmq keycloak
# 3. DB-Secret anlegen (einmalig pro Service/Namespace)
kubectl create secret generic catalog-service-db-credentials -n library-test \
--from-literal=SPRING_DATASOURCE_USERNAME=library \
--from-literal=SPRING_DATASOURCE_PASSWORD=library
# 4. Image bauen und laden (KIND_EXPERIMENTAL_PROVIDER=podman muss gesetzt sein)
podman build -f library-platform/Dockerfile --build-arg MODULE=catalog-service \
--build-arg PORT=8080 -t library/catalog-service:kindtest library-platform
kind load docker-image library/catalog-service:kindtest --name library-test
# 5. Deployen (bewusst ohne --wait - siehe Stolperstein oben zur variablen Boot-Zeit;
# --wait mit einem zu knappen --timeout schlaegt sonst faelschlich fehl, waehrend der Pod
# im Hintergrund normal weiter hochfaehrt)
helm install catalog-service infra/helm/library-service \
-f infra/helm/values/catalog-service-values.yaml \
-f infra/helm/values/env/test-values.yaml \
-f infra/helm/values/env/kind-values.yaml \
-f infra/helm/values/env/kind/catalog-service-values.yaml \
-n library-test --create-namespace --set image.tag=kindtest
# 6. Status/Logs pruefen (READY 1/1 nach realistisch 20-200s je nach Host-Auslastung)
kubectl get pods -n library-test -w
kubectl logs -n library-test -l app.kubernetes.io/instance=catalog-service -f
Ein erfolgreicher Start zeigt in den Logs u. a.:
o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "public", now at version v1
j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
- echte Postgres-Verbindung ueber
host.containers.internal:5432, echte Flyway-Migration, echte Hibernate-Schema-Validierung - nicht bloss gerendertes YAML wie beim bisherigen Dry-Run.
Aufraeumen
kind delete cluster --name library-test
Der Cluster selbst ist kein Repository-Artefakt und jederzeit aus
infra/kind/kind-config.yaml neu erzeugbar - anders als die Compose-Infrastruktur (die
persistente Volumes wie pgdata haelt) gibt es hier nichts, was ueber das Loeschen hinaus
aufgeraeumt werden muesste.
Bewusst ausserhalb des Scopes (Vertiefungsaufgaben)
- Kein Browser-/HTTP-Zugriff von aussen:
kind-config.yamlhat bewusst keineextraPortMappings. Das wuerde zusaetzlich einen Ingress-Controller (ingress-nginxo. Ae.) im Cluster voraussetzen - Verifikation erfolgt hier ueberkubectl get pods/kubectl logs, nicht ueber einen Browser-Aufruf wie bei Compose. - QS/Prod bleiben Dry-Run: in der Realitaet waeren das separate, dauerhafte Cluster (Cloud-
Managed oder ein eigener CRC/OKD-Cluster), nicht das Laptop einer einzelnen Person, das nach
jedem
kind delete clusterwieder bei null anfaengt - siehe jenkins-pipeline.md. - Nur catalog-service exemplarisch verifiziert (deckt alle vier Infrastruktur-Verbindungen
gleichzeitig ab: Postgres, Redis, Kafka, Keycloak) - die uebrigen sieben Services folgen
demselben Muster (unterschiedliche
env/kind/<service>-values.yaml, falls vorhanden), wurden aber nicht einzeln nachgestellt. - kind-Infrastruktur-Subcharts (Postgres/Kafka/Redis/Keycloak per Bitnami-Chart IM Cluster
statt via
host.containers.internal) - siehe die bereits inkubernetes-guide.mddokumentierte Vertiefungsaufgabe, die hier bewusst nicht mit erledigt wurde (deutlich groesserer Scope).