Betriebsanleitungen (Runbooks)
- Vorbemerkung: Unerreichbarkeit ist die Sicherheitsgrenze
- FROST‑Instanz für einen Mandanten bereitstellen
- Rule Chain je Mandant verteilen
- Mandant stilllegen
- Verlust des Verschlüsselungsschlüssels
- Wechsel der ThingsBoard‑Edition
- Umstellung auf den mandantenfähigen Adapter
- Bewusst akzeptierte Einschränkungen
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 Bleibt die Freigabe stehen, genügt ein |
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 |
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:
-
frost_server_url=http://<host>:<port>/FROST-Server/v1.1 -
keycloak_client_id=frost-server-<slug>
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 |
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 |
Ablauf je weiterem Mandant:
-
rulechain/thingsboard_frost_adapter.jsonin den ThingsBoard‑Mandanten importieren. -
In allen vier
TbRestApiCallNodedie 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/updateDer Slug muss zeichengenau dem Slug der Mandantenzeile entsprechen. Ein falscher Slug ergibt einen
404mit dem Namen der abgelehnten Entität im Log des Adapters — geschrieben wird nichts. -
Prüfen, dass jeder der vier REST‑Knoten seine
Failure‑Relation auf einenTbCreateAlarmNodeverdrahtet 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. -
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 |
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.
-
Die Geräte des Mandanten in ThingsBoard aus der Veröffentlichungsgruppe entfernen. Jedes
/device/deleteentfernt das zugehörigeThingaus FROST und kaskadierend dessen Datastreams und Observations. -
Den Mandanten auf der Seite Tenants der Verwaltungsoberfläche löschen — alternativ
DELETE /tenant/{id}, wobei dieidausGET /tenantstammt, da der Ablauf hier nur den Slug kennt. Ein409bedeutet: Schritt 1 ist unvollständig, oder der FROST‑Server antwortet nicht. -
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:
-
GET {frost_server_url}/v1.1/ObservedProperties?$expand=Datastreams($top=1)— die verwaisten sind die ohne Datastreams. -
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.
-
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 |
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 |
|---|---|
|
|
|
|
|
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 |
Der Ablauf auf dem Entwicklungssystem, auf dem der Adapter läuft:
-
THINGSBOARD_EDITIONin.envsetzen und den Adapter neu starten. -
Jeden Mandanten löschen und neu anlegen. Löschen setzt voraus, dass sein FROST‑Server keine
Thingsmehr hält (siehe Mandant stilllegen) — die Alternative ist eindocker compose down -vund ein Neuaufbau, was auf einem Entwicklungssystem der kürzere Weg ist. -
Ende zu Ende prüfen:
Frost Adapter Test Deviceauslösen und im FROST des Mandanten eineObservationmit frischemphenomenonTimebestä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.
-
Stoppen und zerstören.
docker compose down -v. Die Datenbank des FROST‑Servers, die Postgres‑Datenbank des Adapters und Redis sind danach weg. -
Mandantenfähigen Adapter aufbauen.
bootstrap()legt das Schema an und füllt die Stammdaten inklusive der kanonischenObservedProperty‑Zeilen. Es existiert noch kein Mandant, also wird kein FROST‑Server angesprochen.TENANT_SECRET_KEYmuss vor diesem Schritt erzeugt und gesetzt sein, sonst startet die Anwendung nicht. -
Ersten Mandanten über die Verwaltungsoberfläche registrieren.
slug= slugifizierter Name des ThingsBoard‑Mandanten;frost_server_urlundkeycloak_client_idaus der Bereitstellung (FROST‑Instanz für einen Mandanten bereitstellen); die ThingsBoard‑Zugangsdaten je nach konfigurierter Edition — unterpederthingsboard_api_keyaus dem bisherigen.env, unterceBenutzername 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. -
Gepflegte Stammdaten von Hand nacherfassen — alle
TelemetryKey‑,UnitOfMeasurement‑ undUnitOfMeasurementKey‑Zeilen über die Standardwerte hinaus. -
Rule Chain importieren. Die alte Chain löschen und die aktualisierte Datei importieren (Rule Chain je Mandant verteilen).
-
Ende zu Ende prüfen.
Frost Adapter Test Deviceauslösen — der TBEL‑Filterknoten der Chain ist bereits auf diesen Gerätenamen festgelegt — und im FROST‑Server dieses Mandanten eineObservationmit frischemphenomenonTimebestä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.
-
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. -
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.
-
Zugangsdaten werden bei der Registrierung bewiesen und danach nie wieder — vom Adapter aus von selbst jedenfalls nicht. Ein rotierter ThingsBoard‑Key zeigt sich als
502und als rotes Test connection, aber nur, wenn jemand hinschaut oder ein Alarm auslöst. -
auto_renewalrettet vor einem toten Prozess, nicht vor einem hängenden. Ein Worker, der ewig in einem FROST‑Aufruf blockiert, erneuert seine Sperre weiter. -
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.
-
Stille ist unsichtbar. Ein untätiger Mandant und eine gelöschte Rule Chain sehen gleich aus: alle Signale grün.
-
Zwei Entwickler, die gleichzeitig die Testsuite laufen lassen, stören sich gegenseitig.
-
Wer eine Shell auf dem Docker‑Host hat, liest die Konfiguration jedes Mandanten.