Zum Inhalt springen
Deutsch

HTTP-Endpunkte

Der Management-Server stellt einen kleinen Satz operativer Endpunkte bereit. Die meisten sind read-only; die Admin-Endpunkte (/cluster/down, /cluster/leave) sind Opt-in und standardmäßig aus.

Sicherheit (security audit #8): die read-only-Endpunkte legen dennoch interne Topologie offen — Member-Adressen (host:port), Rollen, den Leader und die Shard-Map. Behandle den Management-Server als sensibel: binde ihn an ein internes Interface und schütze ihn mit den Optionen ipAllowlist (und/oder auth) von managementRoutes(...), statt ihn öffentlich zu exponieren.

Liveness — ist dieser Prozess fundamental gesund?

GET /health
→ 200 OK
{
"status": "UP",
"checks": [
{ "name": "actor-system", "status": true }
]
}

503 mit { "status": "DOWN", ... }, wenn ein Liveness-Check fehlschlägt. Siehe Health Checks.

Readiness — sollte dieser Pod Traffic bekommen?

GET /ready
→ 200 OK
{
"status": "UP",
"clusterReady": true,
"checks": [
{ "name": "cluster-membership", "status": true },
{ "name": "cluster-transport", "status": true },
{ "name": "database", "status": true }
]
}

Das checks-Array enthält jeden Readiness-Check auf der Registry des Systems — die Framework-Checks cluster-membership und cluster-transport plus alles, was du registriert hast. clusterReady ist das Ergebnis von cluster-membership selbst, aus diesem Array zurückgelesen statt separat berechnet: es ist also false, bis der lokale Node Up erreicht, und true, wenn es gar keinen Cluster gibt. 503, wenn ein Check fehlschlägt.

Verfügbar, wenn ein cluster an managementRoutes übergeben wurde.

GET /cluster/members
→ 200 OK
{
"members": [
{
"address": "actor-ts://my-app@10.0.0.5:2552",
"status": "up",
"version": 3,
"roles": ["compute"]
},
...
],
"self": "actor-ts://my-app@10.0.0.5:2552"
}

Voller Membership-Snapshot. Nützlich für:

  • Manuelle Cluster-Zustand-Checks während Incidents.
  • Externe Dashboards, die den Cluster visualisieren.
  • Tests, die das Cluster-Joining verifizieren.
GET /cluster/leader
→ 200 OK
{ "leader": "actor-ts://my-app@10.0.0.5:2552", "isSelf": true }

Die Adresse des Leaders plus isSelf (ist der lokale Node der Leader). leader ist null, bevor ein Leader gewählt wurde. Nützlich zum Monitoring von Leader-Wechseln.

GET /cluster/shards?type=cart
→ 200 OK
{
"typeName": "cart",
"leader": "my-app@10.0.0.5:2552",
"version": 7,
"takenAt": 1710000000000,
"regions": [
{
"key": "my-app@10.0.0.5:2552|/system/cluster/sharding/region-cart",
"address": "my-app@10.0.0.5:2552",
"path": "/system/cluster/sharding/region-cart",
"proxy": false,
"shards": [0, 1, 2, ..., 33]
},
...
],
"shardHome": [
{ "shard": 0, "regionKey": "my-app@10.0.0.5:2552|/system/cluster/sharding/region-cart" },
...
]
}

Zeigt die Shard-zu-Region-Allokation für einen sharded Typ. Nutze für:

  • Verifizieren gleichmäßiger Verteilung nach einem Rebalance.
  • Diagnostizieren heißer Regionen (eine Region mit zu vielen Shards).
  • Manuelle Rebalance-Trigger in der Entwicklung.

Woher die Antwort kommt. Der Node antwortet aus ClusterSharding.shardMap(typeName) — der letzten Karte, die der Coordinator an die Region dieses Nodes gesendet hat. Dafür ist keine Konfiguration nötig: keine DistributedData-Extension, kein coordinatorStateStore, kein Round-Trip zum Leader. Die einzige Voraussetzung ist, dass der antwortende Node an dem Typ teilnimmt — also sharding.start(...) oder sharding.startProxy(...) dafür aufgerufen hat —, denn der Coordinator sendet nur an Regionen, die sich registriert haben. Ein Node, der cart nie erwähnt hat, hat nichts zu berichten und antwortet mit 404.

Früher las dieser Endpunkt den DistributedData-Snapshot des Coordinators, wodurch eine 200 in einer Standardkonfiguration unerreichbar war: nichts im Framework startet diese Extension, und der Snapshot wird nur geschrieben, wenn man sich per coordinatorStateStore dafür entscheidet. Der Store existiert weiterhin und bleibt optional — er verkürzt den Leader-Failover —, aber dieser Endpunkt hängt nicht mehr davon ab.

Den Body lesen:

  • version zählt Broadcasts des Coordinators, keine Shard-Zuweisungen — eine Erhöhung pro Publish, und ein Publish fasst einen Schwung Verschiebungen zusammen. Vergleiche den Wert über zwei Abfragen, um „die Karte hat sich bewegt“ von „die Karte ist unverändert“ zu unterscheiden.
  • takenAt und leader stempelt der antwortende Node beim Eintreffen der Karte, zwei Nodes können also dieselbe version mit leicht verschiedenen Zeitstempeln melden.
  • regions[].key ist <Adresse>|<Region-Pfad>, derselbe Key, den shardHome[].regionKey verwendet. Eine Region, die nichts hostet, steht trotzdem in der Liste, mit leerem shards — „registriert und ohne Shards“ ist die interessante Hälfte einer Rebalance-Frage.
  • Eine 200 mit leerem shardHome ist normal und kein Fehler: regions füllt sich, sobald sich Regionen registrieren, während ein Shard erst dann ein Zuhause bekommt, wenn eine Entity darin adressiert wird.

Opt-in via enableMetricsEndpoint: true.

GET /metrics
→ 200 OK
Content-Type: text/plain; version=0.0.4; charset=utf-8
# HELP actor_messages_processed_total ...
# TYPE actor_messages_processed_total counter
actor_messages_processed_total{class="Worker",path="..."} 12345
...

Prometheus-Textformat. Siehe Prometheus-Exporter.

Antwortet stattdessen mit 503, wenn die installierte MetricsRegistry sich nicht über collect() zurücklesen lässt — die prom-client-Bridge ist die eine im Framework, die das nicht kann, weil sie ihre Schreibvorgänge an prom-client weiterleitet und keinen Snapshot behält:

GET /metrics
→ 503 Service Unavailable
metrics endpoint unavailable: the installed MetricsRegistry does not
support collect() — ...

Ein 200 mit leerem Body wäre schlimmer als der Fehler: eine leere Exposition ist ein gültiger Scrape, also verbucht Prometheus das Target als erreichbar, und jede Serie des Frameworks hört still auf zu existieren. Die Prüfung läuft pro Request, weil die Registry zu jedem Zeitpunkt im Leben des Systems ausgetauscht werden kann.

Opt-in via enableLeaveEndpoint: true.

POST /cluster/leave
→ 202 Accepted
leaving

Der 202-Body ist das Klartext-Wort leaving (kein JSON) — der Leave ist Fire-and-forget.

Löst einen Graceful Cluster-Leave aus. Der Node läuft durch leaving → exiting → removed; Shards rebalancen weg; das Actor-System terminiert (konfigurierbar).

Nutze für:

  • Pod-Außerbetriebnahme vor einem Rolling Update.
  • Manuellen Node-Out während Incidents.

Hinter Authentifizierung gaten — jeder mit Portzugriff kann deinen Node drainen.

Opt-in via enableDownEndpoint: true.

POST /cluster/down
{ "address": "actor-ts://my-app@10.0.0.5:2552" }
→ 202 Accepted
{ "downed": "actor-ts://my-app@10.0.0.5:2552" }

Gibt 202 mit { downed } zurück, wenn das Mitglied gedownt wurde, oder 404, wenn die Adresse unbekannt oder bereits terminal ist.

Force-downt ein Remote-Mitglied per Adresse. Destruktiv — die Actor des Targets stoppen, seine Shards reallokieren woanders.

Nutze für:

  • Split-Brain-Recovery, wenn keine Downing-Strategie konfiguriert ist.
  • Entfernen von hängenden unreachable-Mitgliedern, die sich weigern, sich zu erholen.

Hochrisiko-Endpunkt — hinter Auth + Audit-Logs gaten.

import { path, get, concat, completeJson } from 'actor-ts/http';
import { managementRoutes } from 'actor-ts/management';
const baseRoutes = managementRoutes(system, cluster);
const customRoutes = concat(
baseRoutes,
path('admin',
get(async () => completeJson(200, { appVersion: '1.2.3' })),
),
);
await system.http(port, { host }).bind(customRoutes);

managementRoutes(system, cluster, options) gibt die Basis-Routen zurück; kombiniere mit eigenen via concat. Nützlich, um app-spezifische Admin-Endpunkte neben dem Standard-Satz hinzuzufügen.

Die Read-Endpunkte (/health, /ready, /cluster/members, /cluster/leader, /cluster/shards) und /cluster/down geben JSON zurück. /metrics liefert Prometheus-text/plain, und /cluster/leave gibt den Klartext-Body leaving zurück.

Fehler sind Klartext-Bodies (das complete(status, message) des Frameworks), kein strukturiertes Objekt — z. B. ergibt ein fehlender type-Query-Param ein 400, dessen Body missing query param \type“ ist.

Status-Codes folgen HTTP-Konventionen: 200 für Reads, 202 Accepted für die Admin-Aktionen (/cluster/leave, /cluster/down), 4xx für Client-Fehler (404, wenn eine /cluster/down-Adresse unbekannt ist) und 503, wenn Cluster oder ein Health-Check nicht verfügbar ist.

Die privilegierten Endpunkte (/cluster/down, /cluster/leave, /cluster/members, /cluster/shards, /metrics) akzeptieren standardmäßig anonyme Requests. Das ist nur sicher, wenn der Management-Port auf einem netzwerkisolierten Bind sitzt (separater Service, anderer Port, 127.0.0.1-only) oder hinter einem Reverse-Proxy, der die Auth für dich übernimmt.

Für Produktions-Deployments, die Management-Endpunkte einem breiteren Netz exponieren, hänge die mitgelieferten Middlewares über managementRoutes(system, cluster, { auth, ipAllowlist }) an:

import {
BearerTokenAuth,
IpAllowlist,
} from 'actor-ts/http';
import {
managementRoutes,
} from 'actor-ts/management';
const routes = managementRoutes(system, cluster, {
enableLeaveEndpoint: true,
enableDownEndpoint: true,
enableMetricsEndpoint: true,
// Shared-Secret-Bearer-Token (Rotation über mehrere Einträge).
auth: BearerTokenAuth({
tokens: [process.env.MGMT_TOKEN!],
realm: 'my-app-mgmt',
}),
// Netzwerk-Level-Fence — nur Requests aus diesen CIDRs
// erreichen die Anwendung überhaupt.
ipAllowlist: IpAllowlist({
allow: ['10.0.0.0/8', '127.0.0.1/32'],
// Hinter einem Reverse-Proxy: benenne die Proxys — die
// Client-Adresse wird dann vom Socket-Ende der
// Forwarded-Kette nach innen aufgelöst. Ohne das liest der
// Default den Socket-Peer, und der ist hinter einem Proxy
// der Proxy.
// trustedProxies: ['10.9.9.0/24'],
}),
});

Policy-Trennung:

  • auth wrappt die privilegierte Teilmenge (/cluster/*, /metrics). /health und /ready bleiben anonym — Kubernetes- Liveness-/Readiness-Probes können nicht so leicht einen Authorization-Header mitschicken. Setze authProtectHealth: true, wenn dein Deployment garantiert, dass die Probes Credentials präsentieren können.
  • ipAllowlist wrappt JEDEN Endpunkt inklusive /health und /ready. Netzwerk-Level-Isolation ist unabhängig davon, wer authentifiziert ist.

Fehlermodi:

  • Fehlender / falscher Authorization-Header → 401 Unauthorized mit WWW-Authenticate: Bearer realm="...".
  • Client-IP außerhalb der Allowlist (oder unauflösbar) → 403 Forbidden.

Die Middlewares sind general-purpose — du kannst sie auch außerhalb des Management-Subtrees über den withMiddleware(mw, route)-Builder einsetzen.