Betriebsanleitungen (Runbooks)

Dieses Kapitel beschreibt die Abläufe, die ein Betreiber von Hand ausführt: das Bereitstellen eines FROST‑Servers für einen neuen Mandanten, das Verteilen der Rule Chain, das Stilllegen eines Mandanten und die Wiederherstellung nach einem Verlust des Verschlüsselungsschlüssels. Keiner dieser Abläufe ist automatisiert — bei zwei bis drei Mandanten und einer Handvoll Ausführungen pro Jahr ist die Automatisierung teurer als der Ablauf.

Der Adapter schreibt an keiner Stelle nach ThingsBoard und legt an keiner Stelle einen FROST‑Server an. Alles in diesem Kapitel passiert neben dem Adapter; er erfährt vom Ergebnis nur über die zwei Werte, die am Ende in die Mandantenzeile eingetragen werden.

Die Umgebung, gegen die diese Abläufe stattfinden — Compose‑Dateien, Dienste und Umgebungsvariablen — beschreibt Repository und Entwicklungsumgebung.

Vorbemerkung: Unerreichbarkeit ist die Sicherheitsgrenze

Der Adapter authentifiziert niemanden. Er hat keine API‑Keys, keine Tokens, keine Benutzer — jeder, der ihn über das Netz erreicht, kann GET /tenant aufrufen und die Konfiguration aller Mandanten lesen sowie über POST /ingest/{slug}/…​ in jeden FROST‑Server schreiben. Der einzige Schutz ist, dass er ausschließlich innerhalb des Docker‑Netzes erreichbar ist.

Deshalb gilt für die echte Installation:

Die Portfreigabe 8000:8000 des Services thingsboard_frost_adapter in docker-compose.app.yml ist eine reine Entwicklungshilfe und darf in der echten Installation nicht vorhanden sein. ThingsBoard erreicht den Adapter über das Docker‑Netz als thingsboard-frost-adapter:8000 und benötigt keinen veröffentlichten Port.

Bleibt die Freigabe stehen, genügt ein curl host:8000/tenant vom Docker‑Host aus, um die Registry aller Mandanten zu lesen. Das ThingsBoard‑Geheimnis ist in der Antwort zwar nicht enthalten (API‑Key und Passwort werden nie zurückgeliefert), alles andere schon — unter der Community Edition auch der Benutzername — und schreibend ist der Adapter ohnehin vollständig offen.

Wer eine Shell auf dem Docker‑Host hat, liest ohnehin jede Mandantenkonfiguration; das ist eine bewusst akzeptierte Einschränkung (siehe Bewusst akzeptierte Einschränkungen).

FROST‑Instanz für einen Mandanten bereitstellen

Wird vom Betreiber ausgeführt, bevor die Mandantenzeile angelegt wird — die Registrierung prüft fail‑closed gegen die realen Systeme und schlägt fehl, solange der FROST‑Server nicht steht. Dauer etwa eine Stunde.

Jeder Mandant bekommt eine eigene FROST‑Instanz mit eigener Datenbank und eigenem Keycloak‑Client. Das ist die Topologie, aus der sich alles Weitere ergibt: getrennte Datenhaltung, getrennte Leserechte und ein frost_server_url, der in der Registry eindeutig ist.

1. Datenbank anlegen

Im bestehenden database‑Container (PostGIS):

CREATE DATABASE sensorthings_<slug>;
CREATE USER sensorthings_<slug> WITH PASSWORD '<generiert>';
GRANT ALL PRIVILEGES ON DATABASE sensorthings_<slug> TO sensorthings_<slug>;

Kein Schema anlegen — persistence_autoUpdateDatabase=true baut es beim ersten Start selbst auf.

2. Keycloak‑Client anlegen

Im bestehenden Realm (in der Entwicklungsumgebung FROST-Test) einen Client frost-server-<slug> nach dem Vorbild des bestehenden Clients frost-server anlegen und dessen Secret (auth_keycloakConfigSecret) notieren.

Anschließend dem bestehenden globalen Adapter‑Benutzer die Client‑Rollen dieses neuen Clients zuweisen.

Es wird kein Benutzer pro Mandant angelegt. FROST wertet Client‑Rollen aus, keine Realm‑Rollen — ein einziger Benutzer mit Client‑Rollen an N Clients ist der gesamte Mechanismus. Benutzername und Passwort dieses Kontos stehen deployment‑weit in KEYCLOAK_USERNAME_FROST_SERVER und KEYCLOAK_PASSWORD_FROST_SERVER; pro Mandant unterscheidet sich nur der Client.

3. Compose‑Service ergänzen

Den Block frost_server kopieren und fünf Werte ändern: Port, Service‑Root‑URL, Datenbank, Datenbankbenutzer/‑passwort und Keycloak‑Client. Das Image‑Tag bleibt überall identisch (siehe Image‑Tag).

  frost_server_<slug>:
    image: fraunhoferiosb/frost-server:${FROST_SERVER_TAG:-2.7.3}
    environment:
      - serviceRootUrl=http://${EXTERNAL_IP}:<port>/FROST-Server
      - plugins_modelLoader_enable=true
      - plugins_projects_enable=false             # bewusst aus
      - plugins_multiDatastream_enable=false
      - plugins_actuation_enable=false
      - http_cors_enable=true
      - http_cors_allowed_origins=*
      - persistence_db_driver=org.postgresql.Driver
      - persistence_db_url=jdbc:postgresql://database:5432/sensorthings_<slug>
      - persistence_db_username=sensorthings_<slug>
      - persistence_db_password=<generiert>
      - persistence_autoUpdateDatabase=true
      - auth_provider=de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider
      - auth_keycloakConfigUrl=http://${EXTERNAL_IP}:8180/realms/FROST-Test/clients-registrations/install/frost-server-<slug>
      - auth_keycloakConfigSecret=<Client Secret>
      - auth_allowAnonymousRead=true              # false, wenn dieser Mandant authentifizierte Lesezugriffe will
    ports:
      - <port>:8080
    depends_on:
      database: { condition: service_healthy }
      keycloak_web: { condition: service_healthy }
    healthcheck:
      test: [ "CMD-SHELL", "wget --spider -q http://localhost:8080/FROST-Server/v1.1 || exit 1" ]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 60s

auth_allowAnonymousRead ist die Stelle, an der die Anforderung „manche Kunden wollen ihre Daten hinter einer Authentifizierung“ tatsächlich zu einem Schalter wird — pro Instanz, und genau das ist der Gegenwert der Topologie ein FROST‑Server je Mandant.

4. Starten und Datenbank initialisieren

Den Service hochfahren und die Initialisierung über die Admin‑Seite DatabaseStatus des FROST‑Servers anklicken. Dieser Schritt ist manuell und hat kein API‑Äquivalent.

5. Zwei Werte zurückgeben

An die Registry gehen genau zwei Werte:

Nichts anderes aus diesem Ablauf landet in der Mandantenzeile. Datenbankpasswort und Client‑Secret kennt der Adapter nie.

Danach wird der Mandant über die Verwaltungsoberfläche registriert; die Registrierung überträgt anschließend alle Observed Properties in den neuen, leeren FROST‑Server.

Nach einem Umzug auf Kubernetes wird aus diesem Ablauf ein helm install frost-<slug> gegen das offizielle Chart von Fraunhofer (je Instanz etwa 1 GiB Speicher und 500 m CPU angefordert), mit Wildcard‑DNS plus Ingress für das Routing. Diese Automatisierung ist bewusst nicht gebaut.

Image‑Tag

Alle FROST‑Instanzen laufen auf demselben festgeschriebenen Tag (FROST_SERVER_TAG, Standard 2.7.3), niemals auf latest. Zwei Instanzen auf unterschiedlichen Versionen unterscheiden sich im Verhalten der SensorThings‑API, und ein docker compose pull würde diesen Unterschied unbemerkt erzeugen. Eine Versionsanhebung ist eine Änderung an .env und betrifft dann alle Mandanten gleichzeitig.

Rule Chain je Mandant verteilen

rulechain/thingsboard_frost_adapter.json ist eine exportierte Rule Chain mit vier TbRestApiCallNode und einem TBEL‑Filterknoten, der auf den Gerätenamen Frost Adapter Test Device festgelegt ist.

Die Datei im Repository trägt den Slug fest verdrahtet in der URL, nicht als Templating‑Stufe ({{SLUG}}) — eine solche Datei ließe sich ohne einen Substitutionsschritt gar nicht erst in ThingsBoard importieren, und ein solcher Schritt existiert nicht. Angestrebt ist der Slug des ersten Mandanten: nur so dokumentiert die eingecheckte Chain eine Endpunkt‑Oberfläche, die es wirklich gibt.

Zurzeit steht dort der Platzhalter replace-me-with-tenant-1-slug, weil der Slug des ersten Mandanten bei der Umstellung noch nicht feststand. Solange er darin steht, ist die Datei nicht importierbereit: der Import ergibt vier unsichtbare 404er. Er ist beim ersten Import durch den echten Slug zu ersetzen — und zwar in der Datei im Repository, nicht nur in der ThingsBoard‑Oberfläche.

Ablauf je weiterem Mandant:

  1. rulechain/thingsboard_frost_adapter.json in den ThingsBoard‑Mandanten importieren.

  2. In allen vier TbRestApiCallNode die URL anpassen und den dort stehenden Slug durch den eigenen ersetzen:

    http://thingsboard-frost-adapter:8000/ingest/<slug>/telemetry
    http://thingsboard-frost-adapter:8000/ingest/<slug>/device/update
    http://thingsboard-frost-adapter:8000/ingest/<slug>/device/delete
    http://thingsboard-frost-adapter:8000/ingest/<slug>/asset/update

    Der Slug muss zeichengenau dem Slug der Mandantenzeile entsprechen. Ein falscher Slug ergibt einen 404 mit dem Namen der abgelehnten Entität im Log des Adapters — geschrieben wird nichts.

  3. Prüfen, dass jeder der vier REST‑Knoten seine Failure‑Relation auf einen TbCreateAlarmNode verdrahtet hat. Die ausgelieferte Datei bringt diese Verdrahtung mit; eine von Hand gebaute Chain muss sie ergänzen. Ohne sie verwirft die Rule Chain jeden Fehler des Adapters stillschweigend.

  4. Die Bedingung des TBEL‑Filterknotens anpassen, falls dieser Mandant nach anderen Kriterien veröffentlicht.

Kommt jemals ein fünfter REST‑Knoten oder ein fünfter Endpunkt hinzu, gelten beide Schritte — die URL‑Anpassung und die Failure‑Verdrahtung — auch für ihn.

Mandant stilllegen

Die Reihenfolge ist verbindlich; der 409 beim Löschen erzwingt sie. Die Schritte in der Oberfläche sind unter Einen Mandanten stilllegen beschrieben; hier stehen sie in der Fassung, die der Betrieb braucht.

  1. Die Geräte des Mandanten in ThingsBoard aus der Veröffentlichungsgruppe entfernen. Jedes /device/delete entfernt das zugehörige Thing aus FROST und kaskadierend dessen Datastreams und Observations.

  2. Den Mandanten auf der Seite Tenants der Verwaltungsoberfläche löschen — alternativ DELETE /tenant/{id}, wobei die id aus GET /tenant stammt, da der Ablauf hier nur den Slug kennt. Ein 409 bedeutet: Schritt 1 ist unvollständig, oder der FROST‑Server antwortet nicht.

  3. Erst jetzt die FROST‑Instanz, ihre Datenbank und ihren Keycloak‑Client abbauen.

Wiederherstellung nach falscher Reihenfolge

Wurde der FROST‑Server zuerst abgebaut, lässt sich der Mandant über die API nie mehr löschen: die Löschprüfung fragt seinen FROST‑Server, ob dort noch Things hängen, und bekommt keine Antwort mehr — „nicht erreichbar“ gilt fail‑closed als „nicht freigegeben“.

Die Folge trifft nicht nur diesen Mandanten. Die tote Zeile lässt jede Löschung einer globalen ObservedProperty, eines TelemetryKey und einer UnitOfMeasurement dauerhaft und für alle scheitern, weil auch diese Prüfungen über alle Mandanten laufen und fail‑closed sind. Ein Force‑Flag gibt es bewusst nicht.

Der einzige Ausweg ist eine von Hand ausgeführte Anweisung gegen die Adapter‑Datenbank:

DELETE FROM tenant WHERE slug = '<slug>';   -- tenantobservedproperty wird kaskadierend entfernt

Umbenennen

Ein Mandant wird nicht umbenannt, sondern gelöscht und neu angelegt — der Slug ist unveränderlich, weil er in der Rule‑Chain‑URL eines fremden Systems steht. Praktisch sind das die Schritte 1 und 2 oben, danach eine neue Registrierung gegen denselben frost_server_url, ohne Schritt 3.

Dabei bleiben die Observed‑Property‑Kopien des alten Slugs im FROST‑Server des Mandanten liegen; sie sind nachweislich inert, tauchen aber in der öffentlichen SensorThings‑Sammlung doppelt auf, und jedes weitere Umbenennen legt einen weiteren Satz an. Sie sind deshalb von Hand zu entfernen, solange noch klar ist, welcher Satz der aktuelle ist:

  1. GET {frost_server_url}/v1.1/ObservedProperties?$expand=Datastreams($top=1) — die verwaisten sind die ohne Datastreams.

  2. Vor dem Löschen gegen den eigenen Datenbestand des Adapters gegenprüfen:

    SELECT frost_id FROM tenantobservedproperty WHERE tenant_id = <id>;

    Diese IDs sind der aktuelle Satz und dürfen nicht angefasst werden.

  3. DELETE {frost_server_url}/v1.1/ObservedProperties(<id>) für alle übrigen.

Niemals eine Kopie löschen, an der Datastreams hängen — in SensorThings kaskadiert das auf deren Observations. Hat ein gelisteter Eintrag Datastreams und steht nicht in der Zuordnungstabelle, ist das nicht Umbenennungs‑Rest, sondern der Fall unmanaged_in_use, den der Abgleich meldet: eine echte Anomalie, die eine menschliche Entscheidung braucht.

Verlust des Verschlüsselungsschlüssels

TENANT_SECRET_KEY ist der Fernet‑Schlüssel, mit dem das ThingsBoard‑Geheimnis jedes Mandanten in der Datenbank verschlüsselt ist — der API‑Key unter PE, das Passwort unter CE. Er ist Pflicht, hat keinen Standardwert, keinen Klartext‑Rückfallweg — und keinen Rotationsmechanismus.

Geht er verloren oder wird er neu erzeugt, ist jedes gespeicherte Geheimnis nicht mehr entschlüsselbar. Der Adapter startet weiterhin; der Fehler zeigt sich pro Mandant beim Bau seiner Clients mit einer expliziten Meldung, und andere Mandanten arbeiten weiter — allerdings nur, sofern sie unter einem noch gültigen Schlüssel verschlüsselt wurden, was nach einem vollständigen Schlüsselverlust auf keinen zutrifft.

Die Wiederherstellung besteht darin, den neuen Schlüssel zu setzen und anschließend für jeden Mandanten das ThingsBoard‑Geheimnis über die Verwaltungsoberfläche neu einzugeben. Bei zwei bis drei Mandanten sind das Minuten. Derselbe Ablauf gilt, wenn ein Datenbank‑Dump in eine Installation mit einem anderen Schlüssel eingespielt wird.

Ein neuer Schlüssel wird erzeugt mit:

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Wechsel der ThingsBoard‑Edition

THINGSBOARD_EDITION sagt, welche ThingsBoard‑Installation hinter THINGSBOARD_URL steht: pe (Standard) oder ce. Sie wird nie ermittelt, sondern konfiguriert — ein Client zu bauen hieße sonst, ein Fremdsystem zu fragen, was es ist, und das Urteil darüber zwischenzuspeichern.

Die Edition entscheidet beides zugleich, Client‑Klasse und Zugangsdaten:

Edition Zugangsdaten je Mandant

pe

thingsboard_api_key

ce

thingsboard_username + thingsboard_password, denn die Community Edition kennt keine API‑Key‑Authentifizierung: X-Authorization: ApiKey beantwortet sie mit 401, und es gibt keinen Endpunkt, der einen Schlüssel ausstellt.

Ein Wechsel der Edition entwertet jede bestehende Mandantenzeile. Alle tragen danach die Zugangsdaten der jeweils anderen Edition, es gibt keine Reparatur an Ort und Stelle — die Zeilen müssen neu registriert werden.

Bis dahin lehnt die Registry jedes PATCH auf diese Zeilen mit 422 ab und nennt die konfigurierte Edition sowie die fehlenden Felder. Das ist beabsichtigt und sieht nur dann wie ein Fehler aus, wenn man es nicht erwartet hat.

Der Ablauf auf dem Entwicklungssystem, auf dem der Adapter läuft:

  1. THINGSBOARD_EDITION in .env setzen und den Adapter neu starten.

  2. Jeden Mandanten löschen und neu anlegen. Löschen setzt voraus, dass sein FROST‑Server keine Things mehr hält (siehe Mandant stilllegen) — die Alternative ist ein docker compose down -v und ein Neuaufbau, was auf einem Entwicklungssystem der kürzere Weg ist.

  3. Ende zu Ende prüfen: Frost Adapter Test Device auslösen und im FROST des Mandanten eine Observation mit frischem phenomenonTime bestätigen.

Was mit ablaufenden Tokens passiert

Unter PE trägt der API‑Key jede Anfrage und läuft nicht ab. Unter CE hält der Client ein JWT mit 150 Minuten Lebensdauer, während der Client‑Cache ihn für die Lebensdauer des Prozesses behält — ohne Gegenmaßnahme wäre nach 2,5 Stunden jeder ThingsBoard‑Zugriff dieses Mandanten kaputt, bis jemand den Adapter neu startet.

Die Gegenmaßnahme steht im Cache selbst (clients/tenant_clients.py): Er beantwortet zwei Fragen, und beide lauten „ist dieses Client‑Paar noch brauchbar?“ — hat sich die Zeile geändert (Fingerprint), und läuft die Sitzung ab (exp des Tokens, mit fünf Minuten Sicherheitsabstand). Trifft eines von beidem zu, wird das Paar neu gebaut. Unter PE ist exp schlicht 0, weil ein API‑Key gar kein Token hinterlegt; dort läuft nie etwas ab.

Nicht abgedeckt ist eine Sitzung, die ThingsBoard vorzeitig beendet — ein dort rotiertes Passwort oder eine von einem Administrator beendete Sitzung. Ein rotiertes Passwort muss ohnehin in die Mandantenzeile eingetragen werden, damit der Adapter es benutzen kann, und bewegt damit den Fingerprint; der zweite Fall ist selten und heilt spätestens mit exp. Bis dahin zeigt sich beides als fehlgeschlagene Anfrage und rotes Test connection.

Umstellung auf den mandantenfähigen Adapter

Es gibt keine Migration. Die Umstellung ist docker compose down -v und ein Neuaufbau — der Adapter läuft nur auf dem Entwicklungssystem des Teams, und alles, was er bisher geschrieben hat, ist entbehrlich. Diese eine Tatsache lässt Datenübernahme, Wartungsfenster und verwaiste Redis‑Sperrschlüssel zu einem einzigen zerstörenden Schritt zusammenfallen.

  1. Stoppen und zerstören. docker compose down -v. Die Datenbank des FROST‑Servers, die Postgres‑Datenbank des Adapters und Redis sind danach weg.

  2. Mandantenfähigen Adapter aufbauen. bootstrap() legt das Schema an und füllt die Stammdaten inklusive der kanonischen ObservedProperty‑Zeilen. Es existiert noch kein Mandant, also wird kein FROST‑Server angesprochen. TENANT_SECRET_KEY muss vor diesem Schritt erzeugt und gesetzt sein, sonst startet die Anwendung nicht.

  3. Ersten Mandanten über die Verwaltungsoberfläche registrieren. slug = slugifizierter Name des ThingsBoard‑Mandanten; frost_server_url und keycloak_client_id aus der Bereitstellung (FROST‑Instanz für einen Mandanten bereitstellen); die ThingsBoard‑Zugangsdaten je nach konfigurierter Edition — unter pe der thingsboard_api_key aus dem bisherigen .env, unter ce Benutzername und Passwort eines ThingsBoard‑Benutzers dieses Mandanten (Wechsel der ThingsBoard‑Edition). Welche Felder das Formular anbietet, entscheidet die Oberfläche anhand der Edition, die sie beim Adapter erfragt. Die Registrierung prüft fail‑closed gegen die realen Systeme und überträgt danach alle kanonischen Observed Properties in den leeren FROST‑Server.

  4. Gepflegte Stammdaten von Hand nacherfassen — alle TelemetryKey‑, UnitOfMeasurement‑ und UnitOfMeasurementKey‑Zeilen über die Standardwerte hinaus.

  5. Rule Chain importieren. Die alte Chain löschen und die aktualisierte Datei importieren (Rule Chain je Mandant verteilen).

  6. Ende zu Ende prüfen. Frost Adapter Test Device auslösen — der TBEL‑Filterknoten der Chain ist bereits auf diesen Gerätenamen festgelegt — und im FROST‑Server dieses Mandanten eine Observation mit frischem phenomenonTime bestätigen.

Warum Schritt 6 unersetzlich ist

Schritt 6 ist die einzige Prüfung, die Slug‑Routing, Mandantenauflösung, Client‑Auswahl, Sperrschlüssel und den Schreibvorgang gemeinsam ausübt. Ein 201 bei der Registrierung und ein sauberer Abgleichbericht sind seine Voraussetzung, kein Ersatz: die Rule Chain verwirft Fehler stillschweigend, weshalb „der Adapter ist gesund“ und „Telemetrie kommt an“ nie gleichgesetzt werden dürfen. Schlägt Schritt 6 fehl, ist das Debug‑Fenster der Rule Chain die richtige Stelle — eine falsche URL nach dem Neuimport ist die wahrscheinlichste Ursache.

Der Rückweg

Der Rückweg ist derselbe Wisch in die andere Richtung: vorheriges Image aufbauen, alte Chain importieren, einen leeren FROST‑Server hinnehmen.

Warum die Mandantenzeile getippt wird

Die Mandantenzeile wird getippt, nicht geseedet. Ein Übernahmeskript oder ein Auto‑Seed aus den alten Umgebungsvariablen wurde verworfen: er würde genau die Mandanten‑Kenntnis in bootstrap() zurückholen, die entfernt wurde, die alten Umgebungsvariablen über ihre Nützlichkeit hinaus in Config am Leben halten und ab dem Tag nach der Umstellung toten Code hinterlassen.

Warum der Slug nicht dev heißt

Der Slug ist der slugifizierte Name des ThingsBoard‑Mandanten, nicht dev. Er steht an drei Stellen, die übereinstimmen müssen — in den Rule‑Chain‑URLs, im Namensraum der Redis‑Sperren und in einer unveränderlichen Registry‑Zeile. dev benennt eine Umgebung, und der erste echte Mandant würde diesen Namen für immer tragen. Späteres Auseinanderlaufen, wenn jemand den Anzeigenamen in ThingsBoard ändert, ist harmlos: slug und name sind getrennte Felder.

Bewusst akzeptierte Einschränkungen

Die folgenden acht Punkte sind ausgewählt, nicht übersehen worden. Jeder ist bei zwei bis drei Mandanten günstig und wäre bei zehn eine Überprüfung wert.

  1. Einen Mandanten zu pausieren kostet sein ThingsBoard‑Geheimnis. Es gibt kein enabled‑Feld; Löschen ist der einzige Ausschalter, und da API‑Key bzw. Passwort nur schreibend über die API gehen, bedeutet Wiedereinschalten, sie neu einzugeben.

  2. Fail‑closed‑Prüfung plus hartes Löschen bedeutet: wer einen Mandanten gelöscht hat, während dessen FROST‑Server unten ist, kann ihn nicht wieder registrieren.

  3. Zugangsdaten werden bei der Registrierung bewiesen und danach nie wieder — vom Adapter aus von selbst jedenfalls nicht. Ein rotierter ThingsBoard‑Key zeigt sich als 502 und als rotes Test connection, aber nur, wenn jemand hinschaut oder ein Alarm auslöst.

  4. auto_renewal rettet vor einem toten Prozess, nicht vor einem hängenden. Ein Worker, der ewig in einem FROST‑Aufruf blockiert, erneuert seine Sperre weiter.

  5. Eine einzige öffentliche SensorThings‑URL über alle Mandanten hinweg gibt es nicht mehr, seit jeder Mandant einen eigenen FROST‑Server hat. Nachrüstbar wäre sie später über einen aggregierenden Katalog oder einen Harvester.

  6. Stille ist unsichtbar. Ein untätiger Mandant und eine gelöschte Rule Chain sehen gleich aus: alle Signale grün.

  7. Zwei Entwickler, die gleichzeitig die Testsuite laufen lassen, stören sich gegenseitig.

  8. Wer eine Shell auf dem Docker‑Host hat, liest die Konfiguration jedes Mandanten.