Blog

Kubernetes-Deployments in CI-Pipelines testen

APR 24, 2020

Schlanke Kubernetes-Cluster bei Bedarf, die mit KIND auf CI Worker Nodes bereitgestellt werden

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 bedarfsgerecht bereitgestellten Kubernetes-Cluster mit geringem Overhead, der mit KIND – Kubernetes in Docker – bereitgestellt wird.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 erforderlichen Laufzeitabhängigkeiten und ist dadurch plattformübergreifend 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 denselben Gründen containerisieren wie unsere Anwendungen: Wenn ein bestimmter Test auf meinem Rechner zuverlässig validiert, sollte er auch 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 weitergegeben, wenn die containerbasierten Tests erfolgreich sind.

Test operations through network

Wenn wir davon ausgehen, dass die Anwendung ein über das Netzwerk erreichbarer Service ist, der sich per Black-Box-Test über eine Netzwerkverbindung testen lässt, kann ein Setup wie das oben beschriebene einfach wie folgt umgesetzt werden:

  1. Anwendungs- und Testcontainer erstellen, zum Beispiel mit „docker build …“

  2. Eine Instanz des Anwendungscontainers starten und mit einem Netzwerk verbinden, zum Beispiel mit „docker run …“

  3. Eine Instanz des Testcontainers starten und mit demselben Netzwerk wie die Anwendung verbinden, zum Beispiel mit „docker run …“

  4. Der Exit-Code des Testcontainers bestimmt das Testergebnis der Anwendung.

Dies wird in der folgenden Abbildung veranschaulicht.

Test operations through network

Die oben beschriebenen Schritte 2 bis 4 lassen sich auch in einer docker-compose-Definition mit zwei Services beschreiben, zum Beispiel so (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:8080

Ein 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 „Containerebene“. Doch was ist, wenn die Output-Artefakte 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, auf dem die CI-Pipelines deployen können. Dabei sind jedoch einige Punkte zu beachten:

  • Ein gemeinsamer Cluster, auf dem alle CI-Pipelines deployen können, wird im Grunde zu einem Multi-Tenant-Cluster, der sorgfältige Überlegungen hinsichtlich Isolation, Sicherheit und Zuverlässigkeit erfordern kann.

  • Wie dimensionieren wir den CI-Kubernetes-Cluster? Höchstwahrscheinlich ist die Clusterkapazität von der Kapazität der CI-Worker getrennt, das heißt, sie können keine Rechenressourcen gemeinsam nutzen. Dies führt zu einer geringen Auslastung. Außerdem dürfen 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 unerwünscht sein kann.

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 Verhaltenstests oder Skalierbarkeitstests. In vielen Fällen lassen sich die Tests, die unsere CI-Pipelines ausführen sollen, jedoch innerhalb der Kapazität eines einzelnen CI-Worker-Nodes durchführen. Im folgenden Abschnitt wird beschrieben, wie ihr bedarfsgerechte Cluster auf einem Container-fähigen CI-Worker-Node erstellt.

Bedarfsgerechter privater Kubernetes-Cluster mit KIND

Kubernetes-in-Docker (KIND) ist eine Implementierung eines Kubernetes-Clusters mit 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-Nodes bereitstellt. Wenn ein Kubernetes-POD auf einem Node gestartet wird, wird er durch Container innerhalb des äußeren Node-Containers umgesetzt.

With KIND we can create on-demand and multi-node Kubernetes clusters on top of the container capabilities of our CI worker node.

Ein KIND-Kubernetes-Cluster:

KIND Kubernetes cluster

Die Clusterkapazität wird natürlich durch die Kapazität des CI-Worker-Nodes begrenzt, aber darüber hinaus verfügt der Kubernetes-Cluster über viele Fähigkeiten eines Produktionsclusters, einschließlich HA-Funktionen.

Sehen wir uns an, wie ihr eine mit Helm in einem KIND-Cluster bereitgestellte Anwendung testen könnt. Dabei handelt es sich um die Anwendung k8s-sentence-age, die auf GitHub verfügbar ist, einschließlich einer GitHub Action, die die in diesem Blog beschriebene CI-Pipeline implementiert. Die Anwendung ist ein einfacher Service, der eine zufällige Zahl (ein „Alter“) zwischen 0 und 100 zurückgeben kann und außerdem passende Prometheus-kompatible Metriken bereitstellt.

KIND installieren

KIND ist eine einzelne ausführbare Datei namens kind, die grundsätzlich mit der Container-Runtime auf dem CI-Worker kommuniziert. Sie erstellt für jeden Node im Cluster einen (äußeren) Container und verwendet dafür Container-Images mit der Kubernetes-Control-Plane. 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 Node erstellen:

kind create cluster --wait 5m

Bei Bedarf können wir für unsere Tests auch Cluster mit mehreren Nodes erstellen. Cluster mit mehreren Nodes benötigen eine Konfigurationsdatei, die die Node-Rollen auflistet:

# config.yaml   kind: Cluster   apiVersion: kind.x-k8s.io/v1alpha4   nodes:   - role: control-plane   - role: worker   - role: worker

Mit 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 so 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 für die 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 mit ihrem Helm Chart bereit und führt die Tests gegen diese bereitgestellte Anwendungsinstanz aus.

Wenn wir die Anwendung mit ihrem Helm Chart bereitstellen, testen wir nicht nur den Anwendungscontainer bei der Bereitstellung in Kubernetes, sondern validieren auch das Helm Chart selbst. Das Helm Chart enthält die YAML-Manifeste, die den Kubernetes-Blueprint der Anwendung definieren. Besonders wichtig ist es, diese zu validieren – nicht nur mit verschiedenen Kubernetes-Versionen, sondern auch in unterschiedlichen Konfigurationen, beispielsweise mit Kombinationen von Werten, die dem Helm Chart übergeben werden.

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 Test-Container wird mithilfe einer Kubernetes-Job-Ressource bereitgestellt. Kubernetes-Job-Ressourcen definieren Workloads, die vollständig ausgeführt werden und ihren Abschlussstatus melden. Der Job verwendet das zuvor erstellte lokale Container-Image „age-test“ und verbindet sich über die in Umgebungsvariablen angegebenen URLs mit den Anwendungs-PODs. Die URLs verweisen auf den vom Helm-Chart erstellten Kubernetes-Service.

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 Component-Test-Job abgeschlossen ist. Das Tool kubectl kann auf verschiedene Bedingungen für unterschiedliche Ressourcen warten, einschließlich abgeschlossener Jobs. Unsere Pipeline wartet also mit folgendem Befehl, bis der Test abgeschlossen ist:

kubectl wait --for=condition=complete \--timeout=1m job/component-test

Der Component-Test-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-Selector aus, der den Job-Pod auswählt.

kubectl logs -l type=component-test

Der Gesamtstatus des Component-Tests wird aus dem Feld .status.succeeded des Job-PODs gelesen und wie unten gezeigt in einer Variable namens SUCCESS gespeichert. Zeigt der Status einen Fehler an, 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 k8s-sentences-age-Repository auf GitHub.

Es ist erwähnenswert, dass das Starten eines Test-Jobs und die Validierung des Ergebnisses genau das ist, was ein helm test macht. Helm test ermöglicht die formale Integration von Tests in Helm-Charts, 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 Test-Container den Nutzern des Helm-Charts bereitzustellen. Um den obigen Test-Job in das Helm-Chart aufzunehmen, müssen wir lediglich die unten gezeigte Annotation hinzufügen und die YAML-Datei als Teil des Charts einbinden.

...metadata:   name: component-test   annotations:     "helm.sh/hook": test

Wenn ein KIND-Cluster nicht ausreicht

In manchen Situationen ist ein lokaler Kubernetes-Cluster auf einem CI-Worker für eure Testzwecke möglicherweise nicht ideal. 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 wahrscheinlich in einem einzigen Container, der ohne Kubernetes ausgeführt werden kann.

  • Component-Tests 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 Test bestimmter Eigenschaften umfassen, beispielsweise die 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, sich nicht einfach im lokalen KIND-Cluster bereitstellen lassen, etwa eine große Datenbank mit Kundendaten.

  • Funktionale Tests, Integrations- oder Akzeptanztests die gesamte „Anwendung“ bereitstellen müssen. Manche Anwendungen passen möglicherweise nicht in die begrenzte Größe des KIND-Clusters.

  • Tests externe Abhängigkeiten haben, beispielsweise Ingress- und Load-Balancing-Funktionen eines Cloud-Providers, Speicherlösungen oder Key-Management-Services. In manchen Fällen lassen sich diese simulieren, indem beispielsweise eine Datenbank im KIND-Cluster bereitgestellt wird, in anderen Fällen ist das nicht möglich.

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 ein externer Kubernetes-Cluster für CI oder Staging zu viel Wartungsaufwand verursacht oder Ressourcen ineffizient nutzt.

  • Cloud
  • CI/CD

Subscribe to our newsletter