Zum Inhalt springen
Farbschema wählenSprache wählen

Runner-Flotte — Defense-in-Depth und Elastizität

Diese Seite erklärt die Defense-in-Depth- und Elastizitäts-Härtung (ADR 0046) der Runner-Flotte. Sie ergänzt das Sicherheitsmodell der Runner-Flotte und benennt bewusst klar, was GA ist, was experimentell und was noch ein Infrastruktur-Rollout ist. Das frühere Phase-5-Design lieferte hier nur schlanke Code-Hooks (einen infra-terminierten mTLS-Hook, einen microVM-Grant ohne Executor, ein Autoscaling-Signal); die B+C-Welle (Issue #521) macht aus diesen Hooks echten, feature-gegateten, fail-closed Code — einen Mutual-TLS-Hub-Listener, ein microVM-Ausführungs-Backend und einen integrierten Autoscaler — jeder standardmäßig aus/deaktiviert, sodass ein Einzelhost Byte-für-Byte identisch bleibt.

Eine beanspruchte Zuweisung trägt eine monotone Fencing-Epoche. Jedes Mal, wenn der Hub die Zuweisung an einen Runner übergibt — auch bei einer Selbst-Rückforderung nach Lease-Ablauf — wird die Epoche um eins erhöht und der neue Wert an den beanspruchenden Runner zurückgegeben. Der Hub verlangt dann genau diese Epoche bei jedem Bericht und jeder Erneuerung.

Warum das wichtig ist: Ein Runner kann seine Lease überleben (ein langer, stiller Agent-Schritt, eine GC-Pause, eine Netzwerkstörung). Dann behandelt der Hub den Inhaber als tot und die Arbeit kehrt in die Warteschlange zurück; derselbe (oder ein anderer) Runner least die Zeile neu. Ohne Fencing könnte der ursprüngliche — noch laufende — Prozess später gegen die Zeile berichten und die neu vergebene Arbeit überschreiben: das laufspezifische Secret verwerfen, den Lauf terminal setzen. Die Epoche schließt dies konstruktiv: Nur der aktuelle Inhaber trägt die aktuelle Epoche der Zeile; ein zurückgeforderter Zombie trägt eine strikt niedrigere, sein verspäteter Bericht passt zu nichts und wird abgelehnt. Damit wird die bestehende Schutzregel „nur der beanspruchende Runner, nur eine offene Zeile“ beweisbar vollständig.

Dasselbe Fencing schützt die Connector-Warteschlange, inklusive des abandoned-assignment-Pfads für verspätete Wiederholungen: Eine Wiederholung mit aktueller Epoche kann eine vom Hub aufgegebene Zeile noch abschließen (Idempotenz), ein Zombie mit veralteter Epoche jedoch nicht.

Ein lebender Runner erneuert seine Lease in festem Zeittakt (≈ Lease ÷ 3) während eines Laufs — per Timer, nie an Ereignisverkehr gekoppelt, sodass auch ein stiller langer Schritt die Lease am Leben hält. Die Erneuerung erhöht die Epoche nicht (es ist derselbe fortlaufende Lauf) und ist gleichermaßen gefenced, sodass ein zurückgeforderter Zombie nicht erneuern kann.

Die Erneuerung garantiert keine Beschädigung (durch Fencing); sie garantiert keine Exactly-once-Ausführung. Eine verlorene Erneuerung lässt die Lease ablaufen und die Aufgabe erneut laufen — Fencing verhindert nur, dass der veraltete Bericht den Zustand des neuen Laufs beschädigt. Das galt schon vor Phase 5; Phase 5 macht das Berichten sicher.

Der Pickup-Timeout schließt die andere Lücke: ein Runner, der Arbeit beansprucht und dann vor dem Start stirbt. Eine solche Zeile bliebe sonst bis zum vollen Lease-Ablauf liegen. Der bestehende Liveness-Sweep lässt nun jede beanspruchte, aber nie gestartete Zuweisung jenseits eines kurzen Pickup-Fensters scheitern (SUPACLOUD_RUNNER_PICKUP_SECS, Standard 120s, Untergrenze 60s, stets unter der Lease), verwirft ihr ruhendes Secret und meldet den Lauf als fehlgeschlagen zur Neuverteilung.

Ablauf des statischen Tokens (optional, fail-closed)

Abschnitt betitelt „Ablauf des statischen Tokens (optional, fail-closed)“

Der eingeschriebene Runner-JWT-Pfad hat ein kurzes TTL (≤ 1 h) und erneuert sich automatisch: der Runner-Daemon tauscht sein scrnj_…-JWT vor Ablauf neu — standardmäßig bei etwa der halben TTL (abgeleitet aus runners.token_expires_at / dem JWT-exp) oder zu einer expliziten Vorlaufzeit, gesetzt mit SUPACLOUD_RUNNER_TOKEN_REFRESH_SECS. Jede Erneuerung trifft den grant-prüfenden Refresh-Endpunkt des Hubs, sodass ein entzogener Workspace innerhalb einer Token-Lebensdauer wirkt. Das statische scrn_-Token hingegen hat keinen Ablauf und wird nie erneuert — Rotation ist sein einziger Erneuerungs-/Widerrufspfad. Phase 5 ergänzt einen optionalen Ablauf pro Zeile: Wird ein Runner mit einem token_ttl_secs registriert oder rotiert, scheitert sein Token nach diesem Zeitpunkt fail-closed bei der Authentifizierung. Der Standard ist unverändert — NULL-Ablauf, läuft nie ab —, sodass ein bestehender Runner unberührt bleibt, bis sein Token mit TTL rotiert wird. Der JWT-Pfad ist unberührt. Drittanbieter-Edge-Runner sollten weiterhin den JWT-Einschreibepfad bevorzugen; der Token-Ablauf ist Defense-in-Depth für First-Party-Runner.

Ein Runner schreibt sich mit einem Isolations-Grant ein — container oder microvm — und betreibt ein passendes Ausführungs-Backend. Die zwei Stufen tauschen Isolationsstärke gegen Betriebskosten:

  • container (Standard, GA). Jede Aufgabe läuft in einem gehärteten Container auf dem Docker-Host des Runners: cap_drop=ALL, no-new-privileges, ein schreibgeschütztes Rootfs, tmpfs-Mounts und Memory-/CPU-/pids-Limits. Sie teilt den Host-Kernel, die Grenze ist also ein Kernel-Namespace + cgroup. Das ist die richtige Stufe für einen Runner, dem du vertraust.
  • microvm (experimentell, feature-gegated). Jede Aufgabe läuft in einem eigenen kurzlebigen hardware-virtualisierten Gast (eine Firecracker/Kata-artige microVM), sodass ein Kernel-Ausbruch hinter einer VM-Grenze eingeschlossen ist, nicht nur in einem Namespace. Das ist die stärkste Stufe — reserviere sie für nicht vertrauenswürdige oder Edge-Runner. Das Backend wird mit SUPACLOUD_RUNNER_EXECUTION_BACKEND=microvm gewählt und ist nur in ein Binary einkompiliert, das mit dem microvm-backend-Cargo-Feature gebaut wurde (standardmäßig aus, wie ssh-backends); es ohne das Feature zu wählen ist ein harter Boot-Fehler (fail-closed — nie ein stiller Downgrade). SupaCloud liefert keinen Launcher: der Betreiber stellt einen bereit und benennt ihn über SUPACLOUD_RUNNER_MICROVM_CMD.

Grant und Backend werden unabhängig und beide fail-closed erzwungen: eine microVM-pflichtige Aufgabe wird ausschließlich an einen Runner mit microvm-Grant verteilt (der Dispatch-Filter), und dieser Runner liefert echte VM-Isolation nur, wenn er tatsächlich das microVM-Backend betreibt. Ein Runner mit container-Grant ist davon unberührt.

Fairness pro Workspace (Admission auf einem geteilten Pool)

Abschnitt betitelt „Fairness pro Workspace (Admission auf einem geteilten Pool)“

Auf einer von mehreren Workspaces geteilten Flotte kann ein ausgelasteter Workspace sonst den ganzen Pool leeren. Der Fairness-pro-Workspace-Schalter lässt Arbeit beim Dispatch fair pro Workspace zu — Admission-Fairness, keine harte Quote —, sodass kein einzelner Workspace die anderen aushungert. Es ist ein persistierter globaler Schalter (workspace_fairness_enabled, standardmäßig aus, fail-closed), der den Least-loaded-Schalter exakt spiegelt; eine optionale globale Ganzzahl workspace_fair_share_cap begrenzt den in-flight-Anteil, den ein Workspace halten darf. Solange der Schalter aus ist, läuft keine Workspace-Buchführung, und das Dispatching ist Byte-für-Byte identisch zu einer Flotte ohne ihn.

Autoscaling: integrierter Aktuator oder externes Signal

Abschnitt betitelt „Autoscaling: integrierter Aktuator oder externes Signal“

Die Flotte kann bedarfsgerecht wachsen und schrumpfen. SupaCloud berechnet die Entscheidung und kann sie optional selbst umsetzen.

Die Entscheidung wird aus der Fleet-Pull-Warteschlangentiefe berechnet — der Anzahl unbeanspruchter assigned-Zeilen über beide Flotten-Warteschlangen, nicht der backlog_items-Warteschlange der autonomen Auslieferung — plus der Anzahl der Online-Runner, der laufenden Arbeit und wie viele Runner draining/cordoned sind. Sie liefert eine desired_runners-Zahl und eine Begründung, sichtbar im Operator-Dispatch-Metrics-Snapshot und unter GET /api/operator/v1/fleet/autoscale. Eine gewünschte Anzahl von 0 ohne Arbeit ist ein Scale-to-Zero-Signal.

Der integrierte Autoscaler (experimentell). Mit SUPACLOUD_RUNNER_AUTOSCALE=on betreibt der Hub die Entscheidung in einer Schleife und setzt sie selbst um, indem er lokale Docker-Runner-Container startet und entfernt, begrenzt auf SUPACLOUD_RUNNER_AUTOSCALE_MIN/_MAX, gedrosselt durch SUPACLOUD_RUNNER_AUTOSCALE_COOLDOWN_SECS, startet SUPACLOUD_RUNNER_AUTOSCALE_RUNNER_IMAGE auf SUPACLOUD_RUNNER_AUTOSCALE_NETWORK (und übergibt jedem neuen Container ein Runner-Token). Er ist standardmäßig aus — keine Schleife läuft, keine Verhaltensänderung. v1 ist nur lokales Docker: er treibt noch keine Cloud-ASG und kein Kubernetes, und es gibt keine Pro-Cloud-Kostenobergrenze, also behandle ihn als experimentelle Kante und setze _MAX konservativ.

Oder das Signal extern konsumieren. Lass den Autoscaler aus und lass einen externen Controller (KEDA, ein Komodo-Cron, eine Cloud-ASG) die Entscheidung lesen und umsetzen. In beiden Pfaden nutzt das Scale-in das bestehende Drain-Primitiv — laufende Arbeit wird nie gewaltsam beendet:

  1. desired_runners < online ⇒ einige Runner entfernen.
  2. Jedes Opfer per POST /api/runners/{id}/drain {drain_state:"cordoned"} cordonen (der Dispatch-Ranker schließt einen cordonten Runner bereits von neuer Arbeit aus).
  3. GET /api/runners/dispatch-state je cordontem Runner abfragen und warten, bis in_flight null erreicht (das safe_to_remove-Gate).
  4. Erst dann den Runner beenden.

Ein Runner mit laufender Arbeit wird nie als sicher gemeldet. Und selbst ein vorzeitiges Beenden ist unschädlich: Die Arbeit wird zurückgefordert und der Zombie gefenced.

Was ist Code, was Infrastruktur (kein falsches Grün)

Abschnitt betitelt „Was ist Code, was Infrastruktur (kein falsches Grün)“
Fähigkeit Status
Lease-Fencing-Epoche (beide Warteschlangen) Code, getestet
Lease-Erneuerung + Pickup-Timeout Code, getestet
Ablauf/TTL des statischen Tokens + JWT-Auto-Erneuerung Code, getestet
Isolations-Dispatch-Filter (container- vs. microvm-Grant) Code, getestet
Fairness-pro-Workspace-Schalter + Share-Cap Code, getestet (standardmäßig aus)
Autoscaling-Entscheidung + Fleet-Queue-Signal + drain-gated safe_to_remove Code, getestet
Mutual TLS (Hub-Listener + Runner-Client-Identität) Code (der Hub liefert TLS und verlangt + verifiziert optional ein Client-Zertifikat; der Runner präsentiert eine Client-Identität und kann die Hub-CA pinnen). Alles env-gegated, fail-closed, standardmäßig aus; der Live-mTLS-Handshake ist nicht Ende-zu-Ende CI-verifiziert
microVM-Ausführungs-Backend (Firecracker/Kata) Code, experimentell + feature-gegated (microvm-backend, standardmäßig aus). Der Dispatch-Filter ist fail-closed; der microVM-Launcher selbst wird operatorseitig bereitgestellt (SUPACLOUD_RUNNER_MICROVM_CMD) — SupaCloud liefert keinen
Integrierter Autoscaler (lokale Docker-Runner starten/einziehen) Code, experimentell, standardmäßig aus; in v1 nur lokales Docker
Cloud-ASG-/Kubernetes-Autoscaling, Snapshot / Hibernate Infra-Rollout. Konsumiere das GET …/fleet/autoscale-Signal + das drain-gated safe_to_remove aus einem externen Controller

Standardmäßig vertraut der Runner dem Hub über One-Way-TLS (nur Server-Cert). Um einen gegenseitigen Handshake zu verlangen, schalte TLS am Hub mit SUPACLOUD_TLS_CERT_PATH + SUPACLOUD_TLS_KEY_PATH ein und füge SUPACLOUD_TLS_CLIENT_CA_PATH hinzu, um ein Client-Zertifikat gegen diese CA zu verlangen und zu verifizieren; auf jedem Runner präsentiere SUPACLOUD_RUNNER_TLS_CLIENT_CERT_PATH + SUPACLOUD_RUNNER_TLS_CLIENT_KEY_PATH (und pinne optional den Hub mit SUPACLOUD_RUNNER_TLS_CA_PATH). Jedes Paar ist Alles-oder-nichts — eine halb konfigurierte Seite ist ein harter Boot-Fehler (fail-closed), und kein TLS-Environment lässt Klartext-HTTP genau wie zuvor (Byte-für-Byte identischer Standard). Die operatorseitigen Schritte stehen in Einen Runner sicher einrichten → den Runner-Transport mit mTLS absichern. mTLS stattdessen an einem vorgelagerten Proxy zu terminieren bleibt eine gültige bw-infra-Option; der In-Process-Listener ist für Deployments ohne einen solchen.

Die microVM-Stufe ist ein echtes, fail-closed Ausführungs-Backend (siehe Isolationsstufen oben), gegated hinter dem microvm-backend-Cargo-Feature und einem operatorseitig bereitgestellten Launcher — SupaCloud liefert keinen Firecracker/Kata-Launcher. Scale-to-Zero / Hibernate ist das desired_runners = 0-Ende der Autoscaling-Entscheidung: der integrierte Autoscaler entfernt untätige lokale Docker-Runner bis hinunter auf SUPACLOUD_RUNNER_AUTOSCALE_MIN, und ein externer Controller kann ein echtes Scale-to-Zero (oder ein cloud-natives Snapshot/Hibernate) aus demselben Signal treiben. Kein Hibernation-Controller wird in SupaCloud ausgeliefert.