Hilfe, mein Container zickt!

2 Kommentare

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:

# ./tests/image_test.py
import pytest
 
def test_current_user_is_jenkins(host):
    assert host.user().name == "jenkins"
    assert host.user().group == "jenkins"
 
def test_jenkins_is_running(host):
    jenkins = host.process.get(comm="java")
    assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war"
    assert jenkins.user == "jenkins"

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:

docker run --rm --name=jenkins_test_container jenkins

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:

testinfra --connection docker --hosts=jenkins_test_container

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

import pytest
 
testinfra_hosts = ["docker://jenkins_test_container"]

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:

#./tests/image_test.py
import pytest
import docker
 
testinfra_hosts = ["docker://jenkins_test_container"]
 
@pytest.fixture(scope="module", autouse=True)
def container():
    client = docker.from_env()
    container = client.containers.run('jenkins', name="jenkins_test_container", detach=True)
    yield container
    container.remove(force=True)

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:

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

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:

#./tests/conftest.py
import docker
import pytest
 
@pytest.fixture(scope="session")
def client():
    return docker.from_env()
 
@pytest.fixture(scope="session")
def image(client):
    return client.images.build(path='./src')

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:

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

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:

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

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:

Failed: 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')

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

# ./src/Dockerfile
USER root
RUN apt update && apt install -y maven

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!

  def test_jenkins_is_running(host):
        jenkins = host.process.get(comm="java")
        assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war"
>       assert jenkins.user == "jenkins"
E       assert 'root' == 'jenkins'
E         - root
E         + jenkins

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

# ./src/Dockerfile
USER root
RUN apt update && apt install -y maven
USER jenkins

Nun laufen alle Tests erfolgreich durch!

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

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:

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

Angelo Veltens ist als Web-Developer tätig. Er begeistert sich sowohl für die Frontend-Entwicklung mit JavaScript, als auch die Backend-seitige Entwicklung mit Frameworks wie Grails oder Spring Boot.
Testautomatisierung auf allen Ebenen von Unit- bis Akzeptanztests, sowie der Aufbau von Continuous-Delivery-Pipelines zählen dabei ebenfalls zu seinen Stärken.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.