//

Hilfe, mein Container zickt!

20.7.2017 | 6 Minuten Lesezeit

Code-Beispiele mit Testinfra: So testen Sie Docker-Images und vermeiden böse Überraschungen in Containern.

Bei Anwendungen, die in Docker-Containern laufen, verlagert sich ein guter Teil des Infrastruktur- und auch des Applikationssetups in die Container. Dieser Part wird oft weder durch Infrastrukturtests noch durch die fachlichen Tests der Anwendung abgedeckt. Dies kann zu bösen Überraschungen führen, wenn ein Fehler im Docker-Image selbst begründet liegt. Die Fehlersuche gestaltet sich oft langwierig und schwierig.

Es ist jedoch durchaus möglich zu testen, ob Images und die aus ihnen erzeugten Container korrekt aufgebaut sind. Im Gegensatz zu Infrastrukturtests, welche den tatsächlichen Status laufender Infrastruktur testen, kann ein Entwickler auf diese Weise Tests bereits zur Entwicklungszeit ausführen. Ebenso können die Tests in den Build-Prozess eines Images integriert werden, und so Regressionen vermeiden.

Damit schließt sich die Lücke zwischen den fachlichen Tests einer containerisierten Anwendung und der Infrastruktur, auf der diese Anwendung läuft.

In diesem Artikel zeige ich anhand von konkreten Code-Beispielen, wie Sie solche Tests mit Hilfe von Testinfra schreiben können. Sie werden lernen, wie Sie

  • Tests gegen einen laufenden Container ausführen,
  • Images zur Test-Zeit bauen und Container programmatisch starten
  • sowie Images testgetrieben entwickeln.

Als Beispiel dient ein Team aus Entwicklerinnen und Entwicklern, welches das offizielle Jenkins-Image testen und an eigene Bedürfnisse anpassen möchte.

Einen laufenden Container testen

Im offiziellen Jenkins-Container startet ein Java-Prozess unter dem Benutzer jenkins. Unser Beispiel-Team möchte sicherstellen, dass es dieses grundlegende Setup nicht durch eigene Anpassungen gefährdet. Das Team nutzt dazu Testinfra , ein Tool zum Testen von Infrastruktur. Es basiert auf der Programmiersprache Python und dem Test-Framework pytest .

Eine Entwicklerin des Teams schreibt die folgenden beiden Testinfra-Tests:

1# ./tests/image_test.py
2import pytest
3 
4def test_current_user_is_jenkins(host):
5    assert host.user().name == "jenkins"
6    assert host.user().group == "jenkins"
7 
8def test_jenkins_is_running(host):
9    jenkins = host.process.get(comm="java")
10    assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war"
11    assert jenkins.user == "jenkins"
12

Der erste Test stellt sicher, dass der Benutzer jenkins existiert und dass er als aktueller Benutzer gesetzt ist.
Der zweite Test gewährleistet, dass der Jenkins-Prozess läuft und die korrekte Benutzerkennung hat. Die Tests stellen also sicher, dass das Jenkins-Image sich wie erwartet verhält, und dieses Verhalten durch spätere Anpassungen nicht beeinträchtigt wird.

Die Tests setzen voraus, dass ein Jenkins-Container läuft. Der folgende Befehl startet diesen:

1docker run --rm --name=jenkins_test_container jenkins
2

Anschließend startet testinfra mit dem Connection-Backend docker die Tests. Mit dem --hosts Parameter kann dabei definiert werden, gegen welchen Container die Tests ausgeführt werden:

1testinfra --connection docker --hosts=jenkins_test_container
2

Alternativ kann der zu testende Container über die Variable testinfra_hosts im Test-Modul angegeben werden:

1import pytest
2 
3testinfra_hosts = ["docker://jenkins_test_container"]
4

Dadurch ist es möglich, verschiedene Container in unterschiedlichen Modulen zu testen. Die Tests können dann einfach mit dem Kommando testinfra oder pytest gestartet werden.

Container bauen und testen

Praktischer ist es, wenn das Test-Modul den Container programmatisch startet. Dies geht mit Hilfe der Bibliothek docker-py problemlos:

1#./tests/image_test.py
2import pytest
3import docker
4 
5testinfra_hosts = ["docker://jenkins_test_container"]
6 
7@pytest.fixture(scope="module", autouse=True)
8def container():
9    client = docker.from_env()
10    container = client.containers.run('jenkins', name="jenkins_test_container", detach=True)
11    yield container
12    container.remove(force=True)
13

Diese Codezeilen definieren ein Test-Fixture , das vor den Tests einen Container startet und diesen im Nachgang wieder entfernt. Der Parameter scope="module" sorgt dafür, dass nur ein Container für das gesamte Modul gestartet wird. Andernfalls würde jeder einzelne Test einen Container erzeugen und wieder entfernen. autouse=True bewirkt, dass das Fixture erzeugt wird, obwohl die Tests es nicht explizit referenzieren.

Das Team hat mit dieser Test-Suite nun ein gutes Fundament, um ein angepasstes Jenkins-Image zu testen. Einer der Entwickler scheibt folgendes Dockerfile, um ein neues Image auf Basis von jenkins zu definieren:

1# ./src/Dockerfile
2FROM jenkins
3MAINTAINER John Doe <john.doe@example.com>
4

Das Beispiel nimmt noch keine Anpassungen vor. Im ersten Schritt sollen die bestehenden Tests gegen dieses neue Image laufen. Die Bibliothek docker-py kann das Image zur Laufzeit der Tests bauen. Ein Session-scope Fixture in der Datei conftest.py baut das Image einmalig – vor allen Tests des Projekts:

1#./tests/conftest.py
2import docker
3import pytest
4 
5@pytest.fixture(scope="session")
6def client():
7    return docker.from_env()
8 
9@pytest.fixture(scope="session")
10def image(client):
11    return client.images.build(path='./src')
12

Das erste Fixture erzeugt zunächst einen Docker-Client, der im zweiten Fixture das Image baut. Die build-Funktion greift dazu auf das Dockerfile im src-Ordner zu.

Das container-Fixture aus dem Test-Modul muss nun natürlich das frisch gebaute Image verwenden:

1# ./tests/image_test.py
2@pytest.fixture(scope="module", autouse=True)
3def container(client, image):
4    container = client.containers.run(image.id, name="jenkins_test_container", detach=True)
5    yield container
6    container.remove(force=True)
7

Die beiden Fixtures aus conftest.py werden per Namenskonvention in das container-Fixture hinein gereicht. Der Docker-Client startet dort nun einen Container auf Grundlage des zuvor gebauten Images.

Testgetriebene Container-Entwicklung

Die Entwicklerin beginnt nun damit, die Funktionen des Jenkins-Image zu erweitern.
Ganz im Sinne testgetriebener Entwicklung, kann sie die Anforderungen in Form eines Tests abbilden:

1# ./tests/image_test.py
2def test_maven_is_installed(host):
3    assert host.package("maven").is_installed
4

Da das Team Maven-Projekte bauen möchte, muss Maven installiert sein. Der Test prüft mittels host.package, ob das maven Paket installiert ist. Intern wird dazu die Paketverwaltung des Images verwendet. In diesem Beipiel ist es dpkg. Weil das Jenkins-Image die Anforderung noch nicht erfüllt, schlagen die Tests fehl. Testinfra gibt die Fehlermeldung aus, die im Container aufgetreten ist:

1Failed: Unexpected exit code 1 for CommandResult(command="dpkg-query -f '${Status}' -W maven", exit_status=1, stdout=None, stderr='dpkg-query: no packages found matching maven\n')
2

Um dies zu korrigieren, wechselt das Dockerfile per USER-Instruktion zum root Benutzer und installiert anschließend maven:

1# ./src/Dockerfile
2USER root
3RUN apt update && apt install -y maven
4

Der neue Test wird jetzt grün, jedoch schlagen die anderen beiden Tests fehl. Das Sicherheitsnetz hat funktioniert und warnt davor, dass nun der Benutzer root Jenkins ausführt!

1  def test_jenkins_is_running(host):
2        jenkins = host.process.get(comm="java")
3        assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war"
4>       assert jenkins.user == "jenkins"
5E       assert 'root' == 'jenkins'
6E         - root
7E         + jenkins
8

Eine weitere USER-Instruktion nach der Installation korrigiert diesen Fauxpas:

1# ./src/Dockerfile
2USER root
3RUN apt update && apt install -y maven
4USER jenkins
5

Nun laufen alle Tests erfolgreich durch!

1tests/image_test.py::test_current_user_is_jenkins[docker://jenkins_test_container] PASSED
2tests/image_test.py::test_jenkins_is_running[docker://jenkins_test_container] PASSED
3tests/image_test.py::test_maven_is_installed[docker://jenkins_test_container] PASSED
4 
5================================== 3 passed in 3.31 seconds ==================================
6

Ausblick

Mit den gezeigten Beispielen hat das Team ein gutes Basis-Setup und kann nach Bedarf weitere Tests schreiben und das Image an die Anforderungen anpassen. Testinfra ist ein geeignetes Werkzeug, um die Änderungen mit Tests abuzusichern.

Es ist auch möglich eine Reihe von Container-Konfigurationen zu testen, indem verschiedene Test-Module unterschiedliche Container-Fixtures nutzen. Dazu kann docker-py zum Beispiel unterschiedliche Umgebungsvariablen setzen, welche den Aufbau des Containers beeinflussen.
Dabei sollten Test-Autoren jedoch berherzigen, das Verhalten des Dockerfiles und nicht das der containerisierten Anwendung zu testen.

Auch komplexe Setups mit untereinander verlinkten Containern sind denkbar. Zum Beispiel könnte docker-py Mock-Container starten, um den zu testenden Container in eine kontrollierte Umgebung zu isolieren.

Fazit

Mit Testinfra können Teams nicht nur Infrastruktur testen. Auch für Docker-Images ist es ein geeignetes Werkzeug. Tests können mit docker-py Images zur Laufzeit bauen und Container starten. Testinfra bietet eine Reihe von Modulen, die den Aufbau dieser Container überprüfen können. Darüber hinaus stehen Entwicklerinnen und Entwicklern alle Möglichkeiten von pytest offen.

Die Beispiele aus diesem Artikel sind auf Github verfügbar.

Exkurs: Testinfra im Container ausführen
Wenn man ohnehin mit Docker arbeitet liegt es nahe, die Tests ebenfalls im Container auszuführen. Das Image [aveltens/docker-testinfra] enthält alle dazu nötigen Abhängigkeiten. Der Source-Code des Projekts und der Docker-Socket des Hostsystems müssen lediglich über ein Volume in den Container einghängt werden:

1docker run --rm -t \
2  -v $(pwd):/project \
3  -v /var/run/docker.sock:/var/run/docker.sock:ro \
4  aveltens/docker-testinfra
5

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.