Kubernetes-API Seed Provider
KubernetesApiSeedProvider liest die Endpoints eines benannten
Service aus der K8s-API und gibt die IPs der bereiten Pods als
Seeds zurück. Funktioniert ohne DNS, ohne SRV, ohne manuelle
Seed-Listen-Pflege — der Service ist dein Vertrag.
import { Cluster, ClusterOptions, KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts';
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace(process.env.K8S_NAMESPACE!) .withServiceName('actor-ts') .withSystemName('my-app') .withPort(2552);const provider = new KubernetesApiSeedProvider( kubernetesApiSeedProviderOptions,);
const seeds = await provider.lookup();const clusterOptions = ClusterOptions.create() .withHost(process.env.POD_IP!) .withPort(2552) .withSeeds(seeds);await Cluster.join( system, clusterOptions,);Für jeden bereiten Pod hinter dem Service actor-ts im
Namespace gibt der Provider <pod-ip>:2552 zurück.
Konfiguration
Abschnitt betitelt „Konfiguration“type KubernetesApiSeedProviderOptionsType = { namespace: string; serviceName: string; systemName: string; port: number; fetchEndpoints?: () => Promise<string[]>; // In-Cluster-API überschreiben pinnedAddresses?: readonly string[]; // CIDRs, in die die Pod-IPs fallen müssen log?: (message: string, error?: unknown) => void;};| Feld | Was |
|---|---|
namespace | K8s-Namespace, der abgefragt wird — typischerweise dein App-Namespace. |
serviceName | Der Service- (oder Endpoints-)Name, dessen bereite Pods den Cluster bilden. |
systemName | Der Actor-System-Name, der auf jede entdeckte Adresse gestempelt wird. |
port | Der Cluster-Transport-Port auf jedem Backing-Pod. |
fetchEndpoints | Überschreibt die Endpoints-Fetch-Funktion — Default ist die In-Cluster-API. |
pinnedAddresses | CIDRs, in die die entdeckten Pod-IPs fallen müssen. Unbesetzt heißt kein Pinning. |
log | Meldet Adressen, die pinnedAddresses verworfen hat. Default: No-op. |
Für Pods, die in-cluster laufen, sind nur die letzten drei Felder
optional — das Framework liest die Endpoints vom Standard-API
unter https://kubernetes.default.svc mit dem gemounteten
ServiceAccount-Token. Die anderen vier Felder sind Pflicht.
Das Pod-CIDR pinnen
Abschnitt betitelt „Das Pod-CIDR pinnen“Ein Endpoints-Objekt darf jede beliebige IP nennen, auch
eine, die keinem Pod im Cluster gehört. pinnedAddresses
begrenzt, wohin ein Schreibzugriff auf dieses Objekt den Bootstrap
umlenken kann:
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace(process.env.K8S_NAMESPACE!) .withServiceName('actor-ts') .withSystemName('my-app') .withPort(2552) .withPinnedAddresses(['10.244.0.0/16']) .withLog((message) => logger.warn(message));Einträge sind hier ausschließlich CIDRs — Endpoints lösen immer
zu IPs auf, ein Host-Suffix könnte also nie greifen und wird bei
der Konstruktion abgelehnt, ebenso eine leere Liste und ein
fehlerhaftes CIDR. Adressen außerhalb der Liste werden
herausgefiltert und über log gemeldet, eine Meldung pro
verworfener Adresse.
Dieser Provider ist ohnehin der besser verteidigte der beiden: der Default-Fetcher pinnt TLS auf die ServiceAccount-CA und erbt das Vertrauensproblem von DNS deshalb nicht so wie der DNS-Provider. RBAC, das Endpoints schreiben darf, ist trotzdem ein sehr viel billigerer Fund als ein CA-Schlüssel — und genau das kostet dieser Pin einen Angreifer.
Das ServiceAccount des Pods braucht Read-Zugriff auf die Endpoints des Service im Namespace:
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: actor-ts-endpoints-reader namespace: my-apprules: - apiGroups: [""] resources: ["endpoints"] verbs: ["get"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: actor-ts-endpoints-reader namespace: my-appsubjects: - kind: ServiceAccount name: actor-tsroleRef: kind: Role name: actor-ts-endpoints-reader apiGroup: rbac.authorization.k8s.ioOhne diese bekommt das Lookup einen 403; Cluster.join retried
endlos.
Was zurückkommt
Abschnitt betitelt „Was zurückkommt“Aus dem Endpoints-Objekt des Service:
- Bereite Pods in
subsets[].addresses[]→ eingeschlossen als<podIP>:<port>. - Nicht bereite / pending Pods (in
notReadyAddresses) → übersprungen (noch keine bereite IP). - Dieser Pod selbst → kann inkludiert sein oder nicht, je nach Readiness-Timing; der Cluster behandelt Self-as-Seed korrekt.
Cluster-weit vs. ReplicaSet
Abschnitt betitelt „Cluster-weit vs. ReplicaSet“Die Membership wird durch den spec.selector des Service selbst
definiert — der Provider braucht nur den Service-Namen:
# Ein Service vor jedem Cluster-Pod:apiVersion: v1kind: Servicemetadata: name: actor-tsspec: clusterIP: None # headless — Endpoints tragen die Pod-IPs selector: app: actor-ts ports: - port: 2552Zeig den Provider mit .withServiceName('actor-ts') auf diesen
Service. Für die meisten Deployments ein Service pro Cluster
— jeder Pod, den der Service selektiert, bootstrappt in einen
Cluster.
Für rollenbasierte asymmetrische Cluster (Worker vs. HTTP-Gateways)
genügt ein einzelner Service, dessen Selector (app=actor-ts)
beide Rollen abdeckt; das Rollen-Tag innerhalb des Clusters ist
getrennt von der Pod-Selektion des Service.
Was beim Start passiert
Abschnitt betitelt „Was beim Start passiert“Wenn du der erste Pod hochkommst, gibt die K8s-API nur diesen
Pod zurück. Die Self-Bootstrap-Logik des Frameworks behandelt
das: Cluster.join mit Seeds, die nur sich selbst enthalten,
befördert sich selbst zum Leader.
Nachfolgende Pods sehen die existierenden Pods und joinen über sie.
Ein Service pro Cluster
Abschnitt betitelt „Ein Service pro Cluster“| Service-Layout | Effekt |
|---|---|
Ein Service, Selector app=actor-ts | Ein Cluster über den Namespace. |
Zwei Services, Selektoren env=prod / env=staging | Separate Prod- und Staging-Cluster in einem Namespace. |
| Ein Service pro benanntem Cluster | Expliziter Cluster-Name; mehrere Cluster in einem Namespace. |
Stabiles Service-Design ist wichtig — die Identität des Clusters ist der Service, auf den du zeigst. Zu ändern, welche Pods der Service selektiert (oder den Service-Namen), splittet den Cluster.
Wann NICHT
Abschnitt betitelt „Wann NICHT“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Discovery im Überblick — das Gesamtbild.
- Kubernetes-Deployment — das vollständige K8s-Rezept.
- Config Seed Provider — Fallback für Non-K8s.
- Aggregate Seed Provider — K8s mit Fallback kombinieren.
- Joining und Seeds — wie Seeds konsumiert werden.
