Eine Datenplattform mit dbt, Dagster und ArgoCD bauen

Wie ich eine ELT-Analytics-Plattform für eine IoT-Thermostatflotte gebaut habe: Dagster für Extraktion und Laden, dbt für Transformationen, Sqitch für Schema-Migrationen, ArgoCD für das Deployment und eine Allowlist für personenbezogene Daten.

Ausgangslage

Bei vilisto betreiben wir mehr als 100.000 smarte Thermostate an rund 400 Kundenstandorten. Die operativen Systeme (ein geshardetes Backend, eine Batterie-Datenbank, ML-Evaluierungspipelines) halten jeweils einen Teil der Daten, und keines davon ist für Analytics gebaut. Eine Frage wie "wie viele Geräte sind letzte Woche pro Firmware-Version offline gegangen" erforderte ein Einmal-Skript gegen Produktions-APIs.

Die Datenplattform extrahiert aus diesen Quellen, lädt in ein PostgreSQL-Warehouse, transformiert die Daten mit dbt und speist Grafana-Dashboards. Deployt wird sie mit ArgoCD.

Architektur

Die Plattform folgt dem ELT-Muster mit einer Medallion-Struktur (Bronze, Silver, Gold):

Operative Systeme (geshardete Backend-API, MinIO, Batterie-Datenbank)
  -> [Extract + Load] Dagster-Assets -> PostgreSQL Raw-Schema (Bronze)
  -> [Transform] dbt Staging Views (Silver) -> Mart Tables (Gold)
  -> [Visualize] Grafana-Dashboards

Sie besteht aus fünf Komponenten:

  1. Sqitch verwaltet das Raw-Schema. Migrationen laufen in einem Kubernetes Init-Container, bevor die Hauptanwendung startet.
  2. Dagster orchestriert Extraktion und Laden. Jede Quelle ist ein Dagster-Asset mit eigenem Schedule.
  3. dbt wandelt rohes JSONB in typisierte, indizierte Tabellen um. Staging-Modelle entpacken und deduplizieren, Mart-Modelle joinen und legen Indizes an.
  4. Helm und ArgoCD übernehmen das Deployment. Schema-Migrationen, Secrets und der Dagster-Webserver sind in Helm-Templates definiert.
  5. Grafana-Dashboards fragen die Mart-Tabellen direkt ab.

Extraktion aus dem geshardeten Backend

Das Backend ist auf mehrere Shards verteilt. Der API-Client authentifiziert sich gegen jeden Shard und liefert Daten pro Kunde. Jedes Extraktions-Asset ruft die API auf, entfernt sensible Felder und schreibt das Ergebnis per Upsert in das Raw-Schema.

Personenbezogene Daten werden über eine Allowlist behandelt. Nur die aufgeführten Konfigurationsparameter werden ins Warehouse geladen, und ein neues Feld in der API-Antwort wird erst geladen, wenn jemand es in die Liste aufnimmt.

Für tägliche Aggregate (Ventilbewegung, Temperatur, Luftfeuchtigkeit, Batteriespannung) fragt der Client die Daten zuerst pro Kunde ab und fällt auf Abfragen pro Gerät zurück, wenn das fehlschlägt. Bei Kunden mit tausenden Geräten ist die Abfrage auf Kundenebene deutlich schneller, sie ist aber nicht immer verfügbar.

Dagster-Jobs und Ressourcen

Drei geplante Jobs laden Daten in unterschiedlichen Intervallen:

ScheduleDaten
Alle 12 StundenGerätestatus-Snapshots, Thermostat-Hierarchie
TäglichVentilbewegung, Temperatur, Luftfeuchtigkeit, RSSI
TäglichML-Evaluierungsergebnisse aus MinIO

Jedes Asset zeichnet Metadaten auf (Anzahl Kunden, Thermostate und Fehler, Ausführungsdauer), die in der Dagster-UI angezeigt werden. Der Job für die täglichen Aggregate nutzt Dagsters Multiprocess-Executor, um Kunden parallel zu extrahieren.

Entwicklung und Produktion verwenden denselben Asset-Code mit unterschiedlichen Ressourcen. In der Entwicklung sind der Backend-API-Client und der MinIO-Client Stubs, und die Datenbank läuft lokal. In Produktion sind die Ressourcen der geshardete API-Client, die Produktionsdatenbank und MinIO. CI lässt die gesamte Pipeline mit Stub-Daten gegen eine lokale TimescaleDB laufen, inklusive Sqitch-Migrationen und aller dbt-Transformationen, und schlägt fehl, wenn ein Asset nicht materialisiert wird.

dbt-Staging- und Mart-Modelle

Das Raw-Schema speichert API-Antworten als JSONB. Das zentrale Staging-Modell macht aus den rohen Geräte-Snapshots eine Zeile pro Gerät und Tag, mit typisierten Spalten für Firmware-Version, Batteriedaten, Ventilposition, Konnektivitäts-Ratios und Konfigurationsparameter. Es wird inkrementell materialisiert, mit Gerät und Datum als eindeutigem Schlüssel, sodass ein Lauf nicht die gesamte Historie neu verarbeitet.

Dasselbe Modell nutzt Window-Funktionen (lag über die Snapshots eines Geräts, sortiert nach Abfragezeit), um Werte aus aufeinanderfolgenden Snapshots abzuleiten, zum Beispiel ob das Gerät neu gestartet ist (Uptime gesunken) und wie viele Kalibrierungen dazwischen stattfanden.

Mart-Modelle joinen die Staging-Tabellen mit Seed-Daten (Firmware-Rollouts, Startdaten der Datenerfassung) und legen Indizes an. Die größte Mart-Tabelle hat Indizes auf Datum, Kunde, Gerät, Firmware-Version und Hardware-Version, damit Grafana-Abfragen schnell bleiben, während sie wächst.

Alle Mart-Tabellen sind PostgreSQL-unlogged-Tabellen. Sie enthalten abgeleitete Daten, die jeder dbt-Lauf neu aufbaut, brauchen also keine WAL-Dauerhaftigkeit, und Unlogged Tables lassen sich schneller schreiben.

Deployment mit Sqitch und ArgoCD

Schema-Migrationen laufen in einem Kubernetes Init-Container, der sqitch deploy ausführt, bevor der Dagster-Pod startet. Schlägt eine Migration fehl, startet der Pod nicht, ArgoCD markiert die Anwendung als degraded, und kein Job läuft gegen ein halb migriertes Schema.

Secrets (Datenbank-Zugangsdaten, API-Passwörter, MinIO-Keys) liegen als SealedSecrets im Git. Das Helm-Chart enthält Deployment, Service, PVC, ConfigMap, SealedSecrets und ServiceAccount.

ArgoCD synchronisiert Staging automatisch von HEAD. Für Produktion aktualisiere ich den Image-Tag in der Values-Datei für Produktion, merge nach main und genehmige den Sync manuell in der ArgoCD-UI.

Source-Freshness-Checks

dbt-Source-Freshness-Checks laufen in der Pipeline, mit einer Warn- und einer Fehlerschwelle pro Quelle:

QuelleWarnung nachFehler nach
Geräte-Snapshots12 Stunden24 Stunden
Tägliche Aggregate25 Stunden48 Stunden
ML-Evaluierungsergebnisse7 Tage14 Tage

Wenn ein Extraktions-Job keine Daten mehr liefert, ohne einen Fehler zu melden, erscheint die veraltete Quelle in Grafana als Freshness-Warnung.

Bekannte Einschränkungen

Die Plattform begann als Proof of Concept, und drei Entscheidungen spiegeln das wider.

PostgreSQL funktioniert als Warehouse bei der aktuellen Größe, aber die Mart-Tabellen wachsen. Ein spaltenorientierter Speicher wie DuckDB oder ClickHouse wäre langfristig besser geeignet.

Die Extraktoren sind für die internen Systeme von Hand geschrieben. Quellen wie das CRM, Prometheus-Metriken oder GitLab ließen sich mit Airbyte einfacher anbinden als mit weiteren handgeschriebenen Extraktoren.

Die größte Mart-Tabelle ist nicht partitioniert. Monatliche Partitionen würden Backfills und Aufbewahrungsregeln vereinfachen; derzeit halten die Indizes die Abfragen schnell.

Stack

SchichtTool
OrchestrierungDagster
Transformationdbt
Schema-MigrationenSqitch
DatenbankPostgreSQL + TimescaleDB
Object StorageMinIO
VisualisierungGrafana
DeploymentHelm + ArgoCD
CIGitLab CI (pytest + Kaniko + Aqua Scan)
SecretsSealedSecrets