Kosteneffiziente Kubernetes-Cluster bei Bedarf, bereitgestellt auf CI-Worker-Nodes mit KIND
Michael Vittrup Larsen
Michael is a Consultant at our office in Aarhus. He has an MScEE from Aalborg University where he specialized in digital signal processing. Before joining us Michael was working on building and optimizing cloud infrastructure for the telecoms sector. In his spare time, he likes to explore the wilderness on his mountain bike.
So testet ihr Kubernetes-Artefakte wie Helm Charts und YAML-Manifeste in euren CI-Pipelines mit einem schlanken, bedarfsgerecht bereitgestellten Kubernetes-Cluster mit KIND – Kubernetes in Docker.Container sind für die Paketierung von Anwendungen sehr beliebt geworden, weil sie das Problem der Abhängigkeitsverwaltung lösen. Eine in einem Container paketierte Anwendung enthält alle notwendigen Laufzeitabhängigkeiten und ist dadurch über verschiedene Ausführungsplattformen hinweg portabel. Mit anderen Worten: Wenn sie auf meinem Rechner funktioniert, wird sie sehr wahrscheinlich auch auf eurem funktionieren.
Automatisierte Tests sind in DevOps allgegenwärtig. Wir sollten unsere Tests aus genau den gleichen Gründen containerisieren wie unsere Anwendungen: Wenn ein bestimmter Test auf meinem Rechner zuverlässig validiert, sollte er auf eurem genauso gut funktionieren – unabhängig davon, welche Bibliotheken und Tools ihr nativ installiert habt.
Testen mit Containern
Die folgende Abbildung zeigt eine Pipeline – oder vielleicht zwei, je nachdem, wie ihr eure Pipelines organisiert. Im oberen Teil wird die Anwendung in einem Container erstellt und paketiert, im unteren Teil geschieht dasselbe mit den Tests, die zur Validierung der Anwendung verwendet werden. Die Anwendung wird nur freigegeben, wenn die containerbasierten Tests erfolgreich sind.
Wenn wir davon ausgehen, dass die Anwendung ein über das Netzwerk erreichbarer Service ist, der per Black-Box-Test über eine Netzwerkverbindung getestet werden kann, lässt sich ein Setup wie das oben beschriebene einfach umsetzen:
Anwendungs- und Testcontainer erstellen, zum Beispiel mit „docker build …“
Eine Instanz des Anwendungscontainers starten, die mit einem Netzwerk verbunden ist, zum Beispiel mit „docker run …“
Eine Instanz des Testcontainers starten, die mit demselben Netzwerk wie die Anwendung verbunden ist, zum Beispiel mit „docker run …“
Der Exit-Code des Testcontainers bestimmt das Testergebnis der Anwendung
Dies wird in der folgenden Abbildung dargestellt.
Die oben beschriebenen Schritte 2 bis 4 können auch in einer docker-compose-Definition mit zwei Services beschrieben werden, zum Beispiel wie folgt (der Testcontainer wird über eine Umgebungsvariable mit dem Netzwerkstandort der Anwendung konfiguriert):
version: '3.7' services: test: image: test-container:latest environment: APPLICATION_URL: http://application:8080 depends_on: - application application: image: application:latest ports: - 8080:8080Ein Test mit den beiden Containern kann nun wie folgt ausgeführt werden:
docker-compose up --exit-code-from test
Kubernetes-Artefakte in der CI-Pipeline testen
Der oben beschriebene Prozess funktioniert gut für Tests auf „Container-Ebene“. Aber was ist, wenn die Ergebnisartefakte der CI-Pipelines Kubernetes-Artefakte wie YAML-Manifeste oder Helm Charts umfassen oder zur Validierung in einem Kubernetes-Cluster bereitgestellt werden müssen? Wie testen wir in solchen Situationen?
Eine Möglichkeit besteht darin, einen Kubernetes-Cluster bereitzustellen, in dem die CI-Pipelines Deployments ausführen können. Dabei gibt es jedoch einige Punkte zu beachten:
Ein gemeinsam genutzter Cluster, in dem alle CI-Pipelines Deployments ausführen können, wird im Grunde zu einem Mandanten-Cluster, der möglicherweise sorgfältige Überlegungen zu Isolation, Sicherheit und Robustheit erfordert.
Wie dimensionieren wir den CI-Kubernetes-Cluster? Wahrscheinlich wird die Cluster-Kapazität von der Kapazität der CI-Worker entkoppelt sein, das heißt, sie können keine Computing-Ressourcen gemeinsam nutzen. Das führt zu einer geringen Auslastung. Außerdem können wir den CI-Cluster nicht zu klein dimensionieren, da Tests nicht fehlschlagen sollen, weil andere Pipelines vorübergehend Ressourcen verbrauchen.
Möglicherweise möchten wir unsere Kubernetes-Artefakte mit vielen Kubernetes-Versionen und -Konfigurationen testen. Das heißt, wir benötigen im Grunde N verfügbare CI-Cluster.
Wir könnten auch für jeden CI-Job bedarfsgerecht einen Kubernetes-Cluster erstellen. Dafür benötigen wir:
Zugriff auf eine Cloud-ähnliche Plattform, auf der wir Kubernetes-Cluster dynamisch bereitstellen können.
Unsere CI-Pipelines benötigen die erforderlichen Berechtigungen, um Infrastruktur zu erstellen, was aus Sicherheitsgründen möglicherweise unerwünscht ist.
Für einige Testszenarien benötigen wir einen produktionsähnlichen Cluster und müssen eine der oben genannten Lösungen in Betracht ziehen, zum Beispiel für Charakteristiktests oder Skalierbarkeitstests. In vielen Fällen lassen sich die Tests, die unsere CI-Pipelines durchführen sollen, jedoch innerhalb der Kapazität eines einzelnen CI-Worker-Knotens ausführen. Im folgenden Abschnitt wird beschrieben, wie ihr bedarfsgerechte Cluster auf einem Container-fähigen CI-Worker-Knoten erstellt.
Bedarfsgerechter privater Kubernetes-Cluster mit KIND
Kubernetes-in-Docker (KIND) ist eine Implementierung eines Kubernetes-Clusters mit der Docker-in-Docker-Technologie (DIND). Docker-in-Docker bedeutet, dass wir Container innerhalb von Containern ausführen können und diese inneren Container nur innerhalb des äußeren Containers sichtbar sind. KIND nutzt dies, um einen Cluster zu implementieren, indem der äußere Container die Kubernetes-Cluster-Knoten abbildet. Wenn ein Kubernetes-POD auf einem Knoten gestartet wird, wird er mit Containern innerhalb des äußeren Knotcontainers umgesetzt.
Mit KIND können wir bedarfsgesteuert Kubernetes-Cluster mit mehreren Nodes auf Basis der Container-Funktionen unseres CI-Worker-Nodes erstellen.
Ein KIND-Kubernetes-Cluster:
Die Kapazität des Clusters wird natürlich durch die Kapazität des CI-Worker-Nodes begrenzt, ansonsten verfügt der Kubernetes-Cluster jedoch über viele Funktionen eines Produktionsclusters, einschließlich HA-Funktionen.
Sehen wir uns an, wie eine mit Helm in einem KIND-Cluster bereitgestellte Anwendung getestet werden kann. Dabei handelt es sich um die Anwendung k8s-sentence-age, die auf GitHub verfügbar ist – einschließlich einer GitHub Action, welche die in diesem Blog beschriebene CI-Pipeline umsetzt. Die Anwendung ist ein einfacher Service, der eine Zufallszahl (ein „Alter“) zwischen 0 und 100 zurückgeben kann und zudem passende Prometheus-kompatible Metriken bereitstellt.
KIND installieren
KIND ist eine einzelne ausführbare Datei namens kind, die im Wesentlichen mit der Container-Runtime auf dem CI-Worker kommuniziert. Für jeden Node im Cluster erstellt sie einen (äußeren) Container auf Grundlage von Container-Images, die die Kubernetes-Control-Plane enthalten. Ein Beispiel für die Installation von kind als Teil einer GitHub Action findet ihr hier.
Einen Cluster erstellen
Mit dem Tool kind können unsere CI-Pipelines mit folgendem Befehl einen Kubernetes-Cluster mit einem einzelnen Node erstellen:
kind create cluster --wait 5m
Falls wir sie für unsere Tests benötigen, können wir auch Cluster mit mehreren Nodes erstellen. Dafür ist eine Konfigurationsdatei erforderlich, die die Node-Rollen auflistet:
# config.yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: workerMit der obigen Konfigurationsdatei können wir mit folgendem Befehl einen Cluster mit drei Nodes erstellen:
kind create cluster --config config.yaml
Wir können festlegen, welches Container-Image die KIND-Kubernetes-Nodes verwenden sollen, und damit die Kubernetes-Version steuern:
kind create cluster --image "kindest/node:v1.16.4"
So können wir im Rahmen unserer CI-Pipeline einfach die Kompatibilität mit mehreren Kubernetes-Versionen testen.
Anwendungs-Images erstellen und für KIND verfügbar machen
Die Beispielanwendung k8s-sentences-age ist in einem Container namens „age“ verpackt, die Tests der Anwendung in einem Container namens „age-test“. Diese Container werden wie üblich wie folgt erstellt:
docker build -t age:latest ../appdocker build -t age-test:latest .
Mit folgendem Befehl können wir die neue Version dieser Images für unsere KIND-Kubernetes-Nodes verfügbar machen:
kind load docker-image age:latestkind load docker-image age-test:latest
Beim Laden der Images auf die KIND-Cluster-Nodes wird das Image auf jeden Node im Cluster kopiert.
Einen Test ausführen
Unsere Pipeline stellt die Anwendung mithilfe ihres Helm Charts bereit und führt die Tests für diese bereitgestellte Anwendungsinstanz aus.
Wenn wir die Anwendung mit ihrem Helm Chart bereitstellen, testen wir nicht nur den Anwendungscontainer nach der Bereitstellung in Kubernetes, sondern validieren auch das Helm Chart selbst. Das Helm Chart enthält die YAML-Manifeste, die den Kubernetes-Bauplan der Anwendung definieren. Dies ist besonders wichtig zu validieren – nicht nur mit verschiedenen Kubernetes-Versionen, sondern auch in unterschiedlichen Konfigurationen, etwa mit verschiedenen Kombinationen von Werten für das Helm Chart.
Wir installieren die Anwendung mit folgendem Helm-Befehl. Beachtet, dass wir die Standardwerte des Helm Charts für Image-Repository, Tag und pullPolicy überschreiben, damit das lokale Image verwendet wird.
helm install --wait age ../helm/age \--set image.repository=age \--set image.tag=latest \--set image.pullPolicy=Never
Der Testcontainer wird mithilfe einer Kubernetes-Job-Ressource bereitgestellt. Kubernetes-Job-Ressourcen definieren Workloads, die bis zum Abschluss ausgeführt werden und ihren Abschlussstatus melden. Der Job verwendet das zuvor erstellte lokale Container-Image „age-test“ und stellt über die in Umgebungsvariablen angegebenen URLs eine Verbindung zu den Anwendungs-Pods her. Die URLs verweisen auf den Kubernetes-Service, der vom Helm-Chart erstellt wurde.
apiVersion: batch/v1 kind: Job metadata: name: component-test spec: template: metadata: labels: type: component-test spec: containers: - name: component-test image: age-test imagePullPolicy: Never env: - name: SERVICE_URL value: http://age:8080 - name: METRICS_URL value: http://age:8080/metrics restartPolicy: Never </code>Der Job wird mit diesem Befehl bereitgestellt:
kubectl apply -f k8s-component-test-job.yaml
Testergebnis prüfen
Bevor wir das Ergebnis prüfen können, müssen wir warten, bis der Komponententest-Job abgeschlossen ist. Das Tool kubectl kann auf verschiedene Bedingungen bei unterschiedlichen Ressourcen warten, einschließlich abgeschlossener Jobs. Unsere Pipeline wartet also mit folgendem Befehl auf den Abschluss des Tests:
kubectl wait --for=condition=complete \--timeout=1m job/component-test
Der Komponententest-Job enthält die Testergebnisse in seinen Logs. Damit diese Ergebnisse in der Pipeline-Ausgabe erscheinen, geben wir die Logs des Jobs mit kubectl und einem Label-Selektor aus, der den Job-Pod auswählt.
kubectl logs -l type=component-test
Der Gesamtstatus des Komponententests wird aus dem Job-Pod-Feld .status.succeeded gelesen und wie unten gezeigt in einer Variable namens SUCCESS gespeichert. Wenn der Status auf einen Fehler hinweist, wird die Pipeline mit einem Fehler beendet:
SUCCESS=$(kubectl get job component-test \-o jsonpath='{.status.succeeded}')if [ $SUCCESS != '1' ]; then exit 1; fiecho "Component test successful"
Die vollständige Pipeline findet ihr im Repository k8s-sentences-age auf GitHub.
An dieser Stelle sei darauf hingewiesen, dass das Starten eines Test-Jobs und die Validierung des Ergebnisses genau das ist, was ein helm test macht. Helm test ermöglicht es, Tests formell in Helm-Charts zu integrieren, sodass Nutzer des Charts diese Tests nach der Installation des Charts ausführen können. Daher ist es sinnvoll, die Tests in eure Helm-Charts aufzunehmen und den Testcontainer den Nutzern des Helm-Charts bereitzustellen. Um den obigen Test-Job in das Helm-Chart aufzunehmen, müsst ihr lediglich die unten gezeigte Annotation hinzufügen und die YAML-Datei als Teil des Charts einbinden.
...metadata: name: component-test annotations: "helm.sh/hook": testWenn ein KIND-Cluster nicht ausreicht
In manchen Situationen ist ein lokaler Kubernetes-Cluster auf einem CI-Worker möglicherweise nicht ideal für eure Tests. Das kann beispielsweise der Fall sein, wenn:
Unit-Tests Funktionen aufrufen, beispielsweise Klassen aus der Anwendung verwenden. In diesem Fall befinden sich Anwendung und Tests höchstwahrscheinlich in einem einzigen Container, der ohne Kubernetes ausgeführt werden kann.
Komponententests keine Kubernetes-bezogenen Artefakte umfassen. Hätte das oben gezeigte Beispiel kein Helm-Chart zum Testen, wäre die docker-compose-Lösung ausreichend gewesen.
Tests einen Eigenschaftstest umfassen, beispielsweise zur Messung der Performance und Skalierbarkeit eurer Anwendung. In solchen Situationen benötigt ihr eine Infrastruktur mit stabilerer Kapazität.
Integrationstests, die von anderen Artefakten abhängen, nicht einfach im lokalen KIND-Cluster bereitgestellt werden können, etwa eine große Datenbank mit Kundendaten.
Funktionale Tests, Integrations- oder Abnahmetests die Bereitstellung der gesamten „Anwendung“ erfordern. Manche Anwendungen passen möglicherweise nicht in die begrenzte Größe des KIND-Clusters.
Tests externe Abhängigkeiten haben, beispielsweise Ingress- und Load-Balancing-Lösungen eines Cloud-Anbieters, Speicherlösungen, Schlüsselverwaltungsdienste usw. In manchen Fällen lassen sich diese simulieren, indem beispielsweise eine Datenbank im KIND-Cluster bereitgestellt wird, in anderen Fällen nicht.
Dennoch gibt es viele Fälle, in denen Tests mit einem KIND-Kubernetes-Cluster ideal sind, beispielsweise wenn ihr Kubernetes-bezogene Artefakte wie ein Helm-Chart oder YAML-Manifeste testen möchtet und wenn ein externer Kubernetes-Cluster für CI oder Staging zu viel Wartungsaufwand verursacht oder Ressourcen zu ineffizient nutzt.
- Cloud
- CI/CD
Subscribe to our newsletter
Related blogs