Automatisierte Tests für OpenShift Cluster

Keine Kommentare

Wir unterziehen unsere OpenShift Cluster vielen Tests im weitesten Sinne: Monitoring, Health Checks, Lasttests, Sicherheitsprüfungen und Schwachstellenscans, zum Beispiel. Dennoch begegne ich immer wieder Testfällen, die damit noch nicht abgedeckt sind. In vielen Fällen drehen sie sich um den Verwendungszweck des Clusters oder darum, wie die für ihn verantwortliche Organisation aufgebaut ist. Manchmal gibt es keine objektiv richtige oder falsche Antwort, keinen offensichtlichen Rückgabewert, den man im Test überprüfen kann. Ich möchte anhand von drei Beispielen zeigen, weshalb mir diese Tests sinnvoll erscheinen, und außerdem einen Weg vorstellen, sie mit minimalem Aufwand automatisiert auszuführen.

Beim ersten Beispiel geht es um die OpenShift Voreinstellung, nach der alle angemeldeten Nutzer Projekte erstellen (genauer, bestellen) können. Nehmen wir an, wir haben uns entschlossen, dies nicht zuzulassen. Wie stellen wir sicher, dass unser Cluster dieser Regel entspricht?

Stellen wir uns zweitens vor, dass unsere Architektur eine über drei Rechenzentren verteilte Applikation vorsieht, um Hochverfügbarkeit zu gewährleisten. Wir benötigen einen Test der prüft, ob die Infrastruktur dieser Anforderung entspricht.

Das dritte Beispiel versetzt uns in folgende Situation: Wir haben gerade einen längeren ungeplanten Ausfall erlitten. Die Verbindung zwischen zwei Projekten war unterbrochen. Natürlich konzentrieren wir uns zunächst auf die Behebung des Fehlers, aber im nächsten Schritt benötigen wir einen Test, der sicherstellt, dass diese Fehlkonfiguration des Pod Netzwerks nicht noch einmal vorkommt.

Diese drei Szenarien haben einiges gemeinsam. Die Tests erfordern direkten Lesezugriff auf die zentrale etcd Datenbank. Dies allein bedeutet, dass es sich um rechnerisch und netzwerktechnisch teure Tests handelt. Wir sprechen eher von täglichen als stündlichen Tests, nach Möglichkeit zu Schwachlastzeiten. Diese Tests sind besonders hilfreich nach Wartungsarbeiten oder einem Upgrade des Clusters. Wir kehren gleich zu ihnen zurück.

Wie lange dauert es, Tests wie diese aufzusetzen? In den meisten Fällen gar nicht lange. Falls wir nach Inspiration suchen, hilft gewöhnlich ein Blick in das Betriebshandbuch oder die Architekturdokumentation. Diese Tests zu schreiben sollte allen OpenShift Admins leicht fallen und einen Test zu schreiben sollte nicht mehr als fünf Minuten beanspruchen. Kubernetes bietet passende Abstraktionen für alle benötigten Komponenten an.

Aufbau des Testsystems

Diese Zeichnung zeigt die Komponenten des Testsystems. Ein CronJob Objekt löst um halb eins ein Pod Deployment aus. Eine ConfigMap beinhaltet die Tests und der technische User des Pods ermöglicht Lesezugriff auf alle Projekte im Cluster.
Komponenten des Testsystems

Ein CronJob Objekt löst nächtliche Testläufe aus. Die Tests werden auf einem Pod ausgeführt, der mit Kate Wards shUnit2 Test Framework, dem oc Kommandozeilentool und verschiedenen Tools (z.B. curl, psql, mysql, jq, awk) ausgestattet ist. Die Tests selbst werden aus einer ConfigMap eingelesen. Das ConfigMap Objekt wiederum basiert auf einem Ordner voller Testskripte in Git.

Zurück zum CronJob Objekt, das den Testlauf in Gang setzt. shunit2 arbeitet nach und nach die Testsuite (konkret die Testskripte in /etc/openshift-unit.d) ab und gibt die Ergebnisse aus. Aufgrund einer Einschränkung der CronJob API die erst mit Version 1.8 behoben wurde, wird auch bei Fehlern am Ende ‘Erfolg’ (Rückgabewert Null) gemeldet, weil Fehlercodes zu gehäuften Deployments und entsprechender Last für den Cluster führen.

Diese Grafik zeigt drei Blöcke in horizontaler Anordnung. Sie sind beschriftet (links zuerst) 'initial setup (cluster-admin)', 'service account (cluster-reader)' and 'regular use (with restricted SCC and non-privileged SC)'.
Berechtigungen

Aus der Berechtigungssicht wird Administratorzugriff benötigt, um das Projekt aufzusetzen. Von diesem Moment an werden Berechtigungen jedoch bewusst eingeschränkt. Der technische User wird auf Leserechte beschränkt, der Container wird im Security Context Constraint ‘restricted’ und im Security Context ‘non-privileged’ ausgeführt.

Die Ausgabe erfolgt über Standard Output und wird so vom bestehenden Logserver aggregiert. Ohne eigene Tests beizusteuern, gibt der Test Pod die folgenden Zeilen aus:

test_nodes_ready
test_nodes_no_warnings
test_project_quotas
test_cluster_admin_bindings
test_container_resources
test_security_context_privileged
test_high_availability
test_anyuid
test_self_provisioner

Ran 9 tests.

OK

Diese Tests dienen jedoch nur als Platzhalter. Am wertvollsten sind individuell zugeschnittene Tests, die die Regeln, Richtlinien und Entscheidung einer Organisation reflektieren.

Rollenkonzept und Rechtevergabe

An diesem Punkt sollten wir zum ersten in der Einleitung erwähnten Beispiel zurückkehren, d.h. die Unterdrückung der Rolle ‘self-provisioner’ für angemeldete Nutzer. Der Test prüft, dass die Clusteradministratoren den Gruppen system:authenticated und system:authenticated:oauth die entsprechende Clusterrolle entzogen haben. Konkret bedeutet das, dass der Master die folgende Anweisung entgegengenommen hat:

$ oc adm policy remove-cluster-role-from-group \
  self-provisioner \
  system:authenticated system:authenticated:oauth
cluster role "self-provisioner" removed: ["system:authenticated" "system:authenticated:oauth"]

Da wir uns auf das oc Tool stützen können, gestaltet sich die Umsetzung des Tests sehr einfach. Wir fragen lediglich wer im Projekt berechtigt ist, Projekte anzulegen. Im OpenShift Jargon fragen wir, wer das Verb create auf die Ressource projectrequest anwenden darf:

test_self_provisioner() {
  count_self_provisioner=`oc adm policy who-can \
    create projectrequests | \
    grep -c system:authenticated`
  assertEquals " non-admin users may not create project requests;" \
    0 ${count_self_provisioner}
}
suite_addTest test_self_provisioner

Das Testframework shUnit2 macht sich kaum bemerkbar an dieser Stelle. Die Hilfsfunktion suite_addTest fügt mehrere Tests zu einer Suite mit einem Rückgabewert zusammen. Das Framework führt alle Funktionen mit dem Wort test im Namen aus. Testautoren benötigen weiterhin eine kleine Anzahl von frameworkdefinierten ‘assert’ Instruktionen, assertEquals in diesem Fall. Konventionen wie das Leerzeichen am Anfang und das Semikolon am Ende der Meldung dienen der Lesbarkeit:

test_self_provisioner
ASSERT: non-admin users must not create project requests; expected:<0> but was:<1>

Während Infrastrukturtests gewöhnlich auf Objektivität und Testabdeckung abzielen, präsentieren sich Tests wie diese bewusst subjektiv und selektiv. Ein Vergleich mit rspec-puppet kann dies verdeutlichen. Hier ist ein kurzer Auszug aus einem Puppet Manifest mit dazugehörigem rspec-puppet Test:

class bastion::install {
  file { '/home/ec2-user/config.json':
    ensure  => file,
    owner   => 'ec2-user',
    mode    => '0644',
    content => template('bastion/config.json.erb'),
  }
}

rspec-puppet prüft diese Definition wie folgt:

context 'in class Install' do
  it {
    should contain_file('/home/ec2-user/config.json')
      .with_ensure('file')
      .with_owner('ec2-user')
      .with_mode('0644')
  }
end

Es fällt hierbei schwer zu argumentieren, dass einige Eigenschaften (z.B. Nutzer dürfen Projekte anlegen) wichtiger als andere sind (z.B. ich erwarte eine JSON Datei im Benutzerverzeichnis von ec2-user, die nur vom Eigentümer modifiziert werden darf). Vergleichen wir die zwei Tests miteinander, wird klar, dass rspec-puppet eine vollständige Karte der Infrastruktur (also eine Testabdeckung von 100 Prozent) aufbaut, während wir uns auf ausgewählte Sehenswürdigkeiten beschränken. Diese Beschränkung mag willkürlich erscheinen, aber das könnte auch für die Entscheidungen und Richtlinien gelten, denen sie Ausdruck verleihen.

Architektur

Um zum zweiten Beispiel zu kommen: Wie stellen wir die Hochverfügbarkeit bestimmter Dienste sicher? Antiaffinitätsregeln ermöglichen Kontrolle über die Verteilung von Pods auf Applikationshosts (Minions), aber sobald es mehrere Hosts in einer Zone oder auch mehrere virtuelle Hostmaschinen auf einem physischen Host gibt, lohnt es sich, dem Scheduler einen Test zur Seite zu stellen. Ein geeigneter Weg ist die Ermittlung der Hosts und des jeweiligen zone Labels:

test_high_availability() {
  for svc in ${HA_SERVICES} do
    nodes=`oc get po --all-namespaces -o wide | grep ${svc} | \
      awk '{ print $8 }'`
    zones=""
    for node in ${nodes}; do
      zones="${zones} `oc get node/${node} -L zone | awk '{print $6}' | tail -n +2`"
    done
    zone_count=`echo ${zones} | tr ' ' '\n' | sort -u | wc -l`
    ha=false
    if [ "${zone_count}" -gt "2" ]; then
      ha=true
    fi
    assertTrue " ${svc} must be distributed across three zones;" ${ha}
  done
}
suite_addTest test_high_availability

Einmal mehr beginnen wir mit einem schlichten oc Aufruf, dessen Ausgabe wir im Anschluss mit gewöhnlichen Kommandozeilenwerkzeugen aufarbeiten. Das Label zone drückt Antiaffinität, das Label region Affinität aus: Dienste werden über Zonengrenzen hinweg verteilt und in Regionen zusammengeführt. Wir beginnen mit den Hosts der Pods (das Ausgabeparameter wide erzwingt die Angabe der Hosts), extrahieren die Zonen und zählen schließlich die Anzahl der gefundenen Zonen. Um den Test zu bestehen muss jeder in der Variable HA_SERVICES aufgeführte Dienst auf mehr als zwei Zonen verteilt sein.

Störfallaufbereitung

Bisher haben betriebliche Vorgaben und architekturelle Anforderungen die Auswahl der Tests bestimmt. Störfälle sind eine weitere geeignete Quelle von Testfällen. Dafür zu sorgen, dass sie nur einmal vorkommen ist weitaus realistischer als der Versuch, zukünftige Ausfälle zu erahnen und im Vorfeld auszuschließen.

Nehmen wir an, dass unser mandantenfähiger Cluster (mit aktiviertem ‘ovs-multitenant’ Plugin) ein Projekt alice enthält, welches über einen Verbund im Pod Netzwerk auf ein Projekt bob zugreift:

$ oc adm pod-network join-projects --to=bob alice

Für die Dauer des Störfalls war die Verbindung zwischen den zwei Projekten verloren. Aus Sicht des Pod Netzwerks bestand der Verbund nicht mehr. Eine plausible Ursache ist, dass ein weiterer Verbund vom Projekt alice zum Projekt eve erstellt worden war. Die Tatsache, dass die eine Beobachtung (der ursprüngliche Verbund besteht nicht mehr) nicht intuitiv der anderen (ein anscheinend separater Verbund wurde erstellt) folgt, macht dies umso wahrscheinlicher. Die betroffenen Dienste im Projekt alice schlagen fehl und nehmen keine Anfragen mehr entgegen.

Das Problem ist schnell erkannt und behoben, aber als Teil der Aufbereitung des Störfalls benötigen wir einen Test, der prüft, ob der Verbund korrekt eingerichtet ist:

test_join_alice_bob() {
  count_net_ids=`oc get netnamespace | \
    grep 'alice\|bob' | \
    awk '{ print $2 }' | \
    sort -u | \
    wc -l`
  assertEquals " join between alice and bob is broken;" \
    1 ${count_net_ids}
}
suite_addTest test_join_alice_bob

Um den Test nachzuvollziehen müssen wir uns zunächst ins Gedächtnis rufen, was genau die Anweisung oc adm pod-network join-projects bewirkt. Die Netzwerk ID des Quellprojekts wird ersetzt durch die des Zielprojekts (hier durch das --to Parameter gekennzeichnet). Zwei Projekte können einander erreichen, wenn sie dieselbe Netzwerk ID besitzen. (So erklärt sich auch die unglückliche Nebenwirkung des zusätzlichen Verbunds zwischen alice und eve: alice erhält die Netzwerk ID von eve und kann so nicht mehr mit bob kommunizieren. Der Test benötigt lediglich die Netzwerk IDs von alice und bob. Er dedupliziert und zählt dann die IDs. Wenn der Verbund noch besteht, ist das Ergebnis 1.

Wäre das nicht eher ein Anwendungsfall für einen Health Check? Vielleicht, mit der Einschränkung, dass wir hier einen konkrete Eigenschaft der Infrastruktur testen. Wir schließen nicht ein Symptom, sondern eine Ursache aus. Dies ist insbesondere bei der Inbetriebnahme eines neuen Clusters oder nach Wartungsarbeiten wichtig. Dabei ist nicht entscheidend, dass dieser Test nicht alle zehn Sekunden ausgeführt wird.

Sprachwahl

Weshalb Shellskripte und nicht Go? Ich muss gestehen, dass mir diese Entscheidung nicht leicht gefallen ist. Das Tool selbst wäre bestimmt flexibler und eleganter geraten. Die Tests sind anfälliger für Codewiederholung als mir lieb ist. Das exports Skript ist ein Versuch, dieses Problem in den Griff zu bekommen (es bündelt zum Beispiel häufig benötigte Anfragen wie ‘alle Projekte, die von Nutzern angelegt worden sind’). Dennoch vermisst man schmerzlich den Komfort von großen Standardbibliotheken, einzeiligen Webservern, Nebenläufigkeit und so weiter.

Diese Bedenken rechtfertigen aber kaum die Wahl einer anderen Sprache, insbesondere wenn wir in Betracht ziehen, dass die Nutzer selbst die wichtigsten Tests beisteuern. Welche Sprache wäre am ehesten geeignet? Go? JavaScript? Python? Ruby? Jede dieser Sprachen würde viele Nutzer ausschließen, die sich auf andere Sprachen konzentriert haben. Shell Skripte sind den meisten OpenShift Nutzern vertraut und eine nahtlose Erweiterung unseres täglichen Umgangs mit OpenShift. Alle hier besprochenen Tests beruhen auf oc Aufrufen. Keine Standardbibliothek kann eine Programmstruktur retten, deren Hauptelement Systemaufrufe sind. Komplett intuitiv und natürlich fühlt sich das nur in der Shellumgebung an.

Weniger, kürzere Wege

Viele Tests kann man als unentbehrlich bezeichnen. Auf die meisten Tests, die wir hier besprochen haben, trifft das nicht zu. Es geht letztendlich um eine persönliche Einschätzung von Risiko und Nutzen. Es fällt mir viel leichter, einem technischen User anyuid Mächte zu verleihen wenn ich weiß, dass ich spätestens am nächsten Morgen daran erinnert werde, sie wieder zu entfernen.

Der hier beschriebene Ansatz erlaubt uns, speziell auf die Infrastruktur zugeschnittene Tests aufzusetzen, mit minimalem Aufwand an Zeit und Training. Das Ziel ist der kürzeste Weg zu einer Auswahl von Testfällen, nicht eine umfassende Topographie der Infrastruktur.

In meiner Erfahrung gibt es keinen Test, den Kollegen und Kolleginnen nicht eleganter und schlichter ausdrücken können, als es mir zuvor möglich schien. Wer selber Tests wie diese nutzen oder schreiben möchte, kann wie folgt den CronJob einrichten:

$ git clone https://github.com/gerald1248/openshift-unit.git
$ make -C openshift-unit
Gerald Schmidt

Verbringt zu viel Zeit mit Wolken, manchmal öffentlich, manchmal privat. An wolkenfreien Tagen sitzt er an Open Source Projekten von ungewissem Nutzen.

Kommentieren

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