KubernetesLease
KubernetesLease implementiert das
Lease-Interface gegen die
eingebaute Lease-Ressource von Kubernetes (die
coordination.k8s.io/v1-API). Produktionstauglich: backed
durch etcd, stark konsistent, RBAC-kontrolliert.
import { KubernetesLease } from 'actor-ts/coordination';
const lease = new KubernetesLease({ name: 'my-singleton-lease', owner: process.env.POD_NAME!, ttlMs: 30_000, renewalIntervalMs: 10_000, namespace: process.env.K8S_NAMESPACE!,});Der etcd-backed Store des K8s-API-Servers liefert die
Single-Holder-Garantie. Zwei Pods, die nebenläufig acquire()
aufrufen, produzieren exakt einen Gewinner — unabhängig von
Pod-Scheduling, Netzwerk-Partition zwischen Pods etc.
Konfiguration
Abschnitt betitelt „Konfiguration“interface KubernetesLeaseSettings { // Aus LeaseSettings: name: string; owner: string; ttlMs: number; renewalIntervalMs?: number; acquireRetries?: number; acquireRetryDelayMs?: number;
// K8s-spezifisch: namespace: string; apiBaseUrl?: string; // den In-Cluster-Default überschreiben serviceAccountToken?: string; // den In-Cluster-Default überschreiben}| K8s-Feld | Default | Was |
|---|---|---|
namespace | Pflicht | K8s-Namespace, in dem die Lease-Ressource liegt. |
apiBaseUrl | in-cluster | Die URL des K8s-API-Servers — Default https://kubernetes.default.svc. |
serviceAccountToken | in-cluster | Das Service-Account-Token des Pods — Default /var/run/secrets/kubernetes.io/serviceaccount/token. |
Für Pods, die in-cluster laufen, brauchst du nur namespace und
name (+ die Standard-LeaseSettings-Felder). Das Framework
liest API-URL und Token von den Standard-Locations.
Für Tests / Dev gegen eine lokale K8s-API (kind, minikube)
überschreibe apiBaseUrl + serviceAccountToken.
Das ServiceAccount des Pods braucht Rechte, um Lease-Ressourcen
zu verwalten:
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: actor-ts-lease-holder namespace: my-apprules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get", "create", "update", "patch", "delete"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: actor-ts-lease-holder namespace: my-appsubjects: - kind: ServiceAccount name: actor-tsroleBinding: kind: Role name: actor-ts-lease-holder apiGroup: rbac.authorization.k8s.ioOhne diese rejectet acquire() mit 403 (Forbidden).
Ohne delete funktioniert release(), aber das Lease-Objekt
bleibt nach dem Release stehen (harmlos; das nächste Acquire
nutzt es weiter).
Was erzeugt wird
Abschnitt betitelt „Was erzeugt wird“Der erste acquire()-Aufruf erzeugt ein Lease-Objekt:
$ kubectl get lease -n my-appNAME HOLDER AGEmy-singleton-lease pod-abc-1 30sDas Framework schreibt:
metadata.name— den Lease-Namen.spec.holderIdentity— den Owner.spec.acquireTime— wann dieser Owner ihn übernommen hat.spec.renewTime— letztes Renewal (wird allerenewalIntervalMsaktualisiert).spec.leaseDurationSeconds— abgeleitet austtlMs.
Andere Halter prüfen renewTime + leaseDurationSeconds < now(),
um zu entscheiden, ob der aktuelle Halter stale ist.
Acquire-Ablauf
Abschnitt betitelt „Acquire-Ablauf“Die Atomarität kommt vom optimistic-concurrency CAS via
resourceVersion von K8s — zwei gleichzeitige Versuche, einen
stalen Lease zu beanspruchen, produzieren einen Gewinner.
Renewal
Abschnitt betitelt „Renewal“Während gehalten, patcht das Framework spec.renewTime alle
renewalIntervalMs:
PATCH /apis/coordination.k8s.io/v1/namespaces/<ns>/leases/<name>{ spec: { renewTime: "2025-05-13T12:00:00.000Z" } }Wenn der Patch fehlschlägt:
- Transient (5xx, connection refused) → retry, loggen,
irgendwann aufgeben, wenn
ttlMsohne Erfolg vergeht. - CAS-Conflict (409) → ein anderer Halter hat übernommen;
onLostfeuert.
Verlust-Erkennung
Abschnitt betitelt „Verlust-Erkennung“onLost feuert, wenn:
- Ein Renewal-Patch einen CAS-Conflict zurückgibt.
- Das Framework feststellt, dass der Lease von jemandem anderem modifiziert wurde (ein Probe-GET vor einer kritischen Operation).
- Netzwerk-Partition Renewals länger als
ttlMsverhindert.
Der Handler sollte den eigentumsabhängigen State sofort fallen lassen — siehe Lease-API für den Vertrag.
Jeder Lease-Halter erzeugt:
- 1 GET + (potenziell) 1 CREATE beim Acquire.
- 1 PATCH alle
renewalIntervalMs, solange gehalten. - 1 PATCH (oder DELETE) beim Release.
Für eine 30-Sekunden-TTL mit 10-Sekunden-Renewal sind das ~6 API-Calls pro Minute pro Lease. Centbeträge in jedem moderaten K8s-Deployment.
Für Cluster mit vielen Leases (z. B. einer pro Sharded Entity Type + einer pro Singleton + einer pro Koordinator) ist die Last auf dem API-Server immer noch vernachlässigbar — K8s schafft locker tausende Lease-Writes pro Sekunde.
Wann du es NICHT einsetzt
Abschnitt betitelt „Wann du es NICHT einsetzt“Tests gegen ein echtes K8s
Abschnitt betitelt „Tests gegen ein echtes K8s“Für Integrationstests gegen eine echte K8s-API (kind, minikube, ephemere CI-Cluster):
const lease = new KubernetesLease({ name: 'test-lease-' + crypto.randomUUID(), owner: 'test-runner', ttlMs: 5_000, apiBaseUrl: 'https://localhost:8443', serviceAccountToken: fs.readFileSync('./test-token', 'utf-8'), namespace: 'test',});
await lease.acquire();expect(lease.checkAlive()).toBe(true);await lease.release();Nimm pro Test eindeutige Lease-Namen (Zufalls-UUID-Suffix), damit
parallele Tests sich nicht in die Quere kommen. Aufräumen mit
release() + einer finalen Delete-Runde im Test-Teardown.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Koordination im Überblick — das Gesamtbild.
- Lease-API — der Vertrag,
den
KubernetesLeaseimplementiert. - InMemoryLease — die Dev-/Test-Alternative.
- Kubernetes-Deployment — das breitere K8s-Rezept.
- Singleton mit Lease — der Haupt-Consumer.