Zum Inhalt springen
Deutsch

Kubernetes-Deployment

Kubernetes ist das häufigste Deployment-Ziel. Das Framework verträgt sich gut mit K8s, sobald ein paar Dinge sitzen: stabile Identität (für stateful Actors), Seed-Discovery (damit Nodes sich finden) und ein sauberer Shutdown-Pfad (damit Rolling Updates keinen Traffic verlieren).

Diese Seite ist ein funktionierendes Rezept — kopieren, anpassen, deployen.

apiVersion: v1
kind: ServiceAccount
metadata:
name: actor-ts
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: actor-ts-pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: actor-ts
subjects:
- kind: ServiceAccount
name: actor-ts
roleRef:
kind: Role
name: actor-ts-pod-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Service
metadata:
name: actor-ts-cluster
spec:
clusterIP: None # headless — DNS liefert Pod-IPs
selector:
app: actor-ts
ports:
- name: cluster
port: 2552
targetPort: 2552
---
apiVersion: v1
kind: Service
metadata:
name: actor-ts
spec:
selector:
app: actor-ts
ports:
- name: http
port: 80
targetPort: 8080
- name: management
port: 8558
targetPort: 8558
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: actor-ts
spec:
serviceName: actor-ts-cluster
replicas: 3
selector:
matchLabels:
app: actor-ts
template:
metadata:
labels:
app: actor-ts
spec:
serviceAccountName: actor-ts
terminationGracePeriodSeconds: 30
containers:
- name: app
image: ghcr.io/your-org/your-app:1.2.3
ports:
- name: cluster
containerPort: 2552
- name: http
containerPort: 8080
- name: management
containerPort: 8558
env:
- name: ACTOR_TS_HOSTNAME
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: ACTOR_TS_PORT
value: "2552"
- name: K8S_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: K8S_LABEL_SELECTOR
value: "app=actor-ts"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: actor-ts-secrets
key: db-password
readinessProbe:
httpGet:
path: /ready
port: management
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: management
initialDelaySeconds: 15
periodSeconds: 10
lifecycle:
preStop:
exec:
# LB drainen, bevor SIGTERM die App erreicht
command: ["/bin/sh", "-c", "sleep 10"]
ServiceAccount: actor-ts
Role: actor-ts-pod-reader # get + list pods
RoleBinding: bindet beides

Der K8s-API-Seed-Provider muss Pods listen können, die einem Label-Selector entsprechen, um Peers zu finden. Ohne dieses RBAC-Grant bekommt der Seed-Provider 403 und der Cluster bildet sich nie.

clusterIP: None

Ein headless Service liefert die Pod-IPs direkt per DNS (keine ClusterIP-Virtual-Address). Nützlich, wenn Nodes stabile, direkte Peer-Identitäten brauchen — die Heartbeats des Failure Detectors zielen auf spezifische Pod-IPs, nicht auf eine load-balancete Abstraktion.

clusterIP: <default>

Ein normaler Service für HTTP und den Management-Endpunkt — die profitieren von Load Balancing. Cluster-Traffic geht über den Headless Service, App-Traffic über diesen.

NimmWann
StatefulSetStabile Pod-Namen (actor-ts-0, actor-ts-1, …). Nützlich, wenn du vorhersagbare Identität für Entity-Placement willst oder wenn pro-Pod Persistent Volumes gemountet werden.
DeploymentPod-Namen sind zufällig. Okay, wenn deine App stateless ist (keine pro-Pod-Identität erforderlich) und Persistence extern liegt (Cassandra-Journal, geteilter S3-Snapshot-Store).

Für sharded Actors mit remember-entities = true auf pro-Pod Persistent Volumes ist StatefulSet die richtige Wahl. Für extern-persistierten State (Cluster gegen ein geteiltes Postgres/Cassandra) ist Deployment in Ordnung und einfacher.

terminationGracePeriodSeconds: 30

K8s sendet SIGTERM, wartet dann diese Zeitspanne vor SIGKILL. Dimensioniert nach:

  • HTTP-Drain — typischerweise 5-10 s.
  • Cluster-Leave-Gossip — 5-15 s für Konvergenz.
  • Journal-Flush — abhängig vom Journal.

30 s sind ein vernünftiger Default. Erhöhe, wenn dein Cluster groß oder das Failure-Detector-Fenster lang ist.

preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]

Kritisch für saubere Rolling Updates. Der Ablauf:

  1. K8s markiert den Pod als terminating und startet den preStop-Hook parallel zur Load-Balancer-Deregistrierung.
  2. sleep 10 — gibt dem Load Balancer Zeit, keinen neuen Traffic mehr an diesen Pod zu schicken.
  3. Nach dem Sleep sendet K8s SIGTERM.
  4. Die Coordinated-Shutdown-Hooks der App drainen in-flight Requests, verlassen den Cluster usw.

Ohne den Sleep läuft SIGTERM gegen die LB-Deregistrierung — in-flight Requests können “draining”-Antworten sehen.

readinessProbe: /ready
livenessProbe: /health

Die Management-Routen des Frameworks stellen diese Endpunkte bereit.

  • /ready — „soll ein Load Balancer diesem Pod Traffic schicken?” Bereits durch die Checks des Frameworks abgesichert: cluster-membership (dieser Knoten ist up) und cluster-transport (er ist nicht von jedem Peer abgeschnitten). App-spezifische Checks — Datenbank erreichbar, Dependencies warm — kommen mit healthChecksOf(system).addReadiness dazu. Prozessintern gated Cluster.bootstrap genauso: actor-ts.cluster.bootstrap.minimum-members (oder awaitReady: { minimumMembers } im Code) sorgt dafür, dass ein aufgelöster Bootstrap nichts Geringeres bedeutet als „das Cluster hat seine erwartete Größe erreicht” — auf die Replica-Anzahl setzen, genau wie required-contact-points.
  • /health — „würde ein Neustart dieses Pods helfen?” Schlägt er fehl, startet K8s den Pod neu; er hängt deshalb von nichts außerhalb des Prozesses ab — der einzige Liveness-Check des Frameworks ist actor-system. Niemals eine Datenbank oder einen Downstream-Dienst hier eintragen: ein gemeinsamer Ausfall würde die ganze Flotte neu starten, und die Neustarts würden nichts reparieren.
import { ActorSystem } from 'actor-ts';
import { Cluster, ClusterOptions } from 'actor-ts/cluster';
import { KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts/discovery';
import { managementRoutes } from 'actor-ts/management';
const system = ActorSystem.create('my-app');
// 1. Cluster-Join mit K8s-API-Seed-Discovery
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create()
.withNamespace(process.env.K8S_NAMESPACE!)
.withServiceName(process.env.K8S_SERVICE_NAME!)
.withSystemName(system.name)
.withPort(2552);
const seeds = await new KubernetesApiSeedProvider(kubernetesApiSeedProviderOptions).lookup();
const clusterOptions = ClusterOptions.create()
.withHost(process.env.ACTOR_TS_HOSTNAME!)
.withPort(parseInt(process.env.ACTOR_TS_PORT!))
.withSeeds(seeds.map(a => a.toString()))
.withRoles(['compute']);
const cluster = await Cluster.join(system, clusterOptions);
// 2. Management-Endpunkte
const mgmtRoutes = managementRoutes(system, cluster);
await system.http(8558).bind(mgmtRoutes);
// 3. App-HTTP-Server
const http = system.extension(HttpExtensionId);
await http.newServerAt('0.0.0.0', 8080).bind(routes);
// 4. Laufen, bis das Kubelet Stopp sagt, dann geordnet herunterfahren
await system.runUntilTerminated();

Vier Schritte in der Reihenfolge:

  1. Cluster-Join mit Seed-Discovery — der K8s-API-Seed-Provider fragt Pods ab, die app=actor-ts matchen, und nutzt deren IPs als Seeds. Auf dem ersten Pod ist die Seed-Liste nur er selbst (der Auto-Promote-to-Leader-Pfad).
  2. Management-Endpunkte — /health + /ready für die K8s-Probes, /cluster/members zum Debuggen.
  3. App-HTTP — deine Routes, getrennter Port vom Management.
  4. Bis zum Terminieren laufen — installiert die SIGTERM/SIGINT-Handler, blockiert, führt dann die Pipeline aus und löst auf, wenn das System unten ist. Es gibt keinen separaten Teardown-Schritt zu schreiben: beide bind()-Aufrufe haben ihr Unbind in der service-unbind-Phase registriert und Cluster.join den Leave in cluster-leave — der Pod fällt also aus den Service-Endpoints und aus dem Cluster, bevor die Actors stoppen.

Siehe Discovery — Kubernetes API für die vollständigen Optionen des Seed-Providers.

Terminal-Fenster
kubectl rollout restart statefulset/actor-ts

Für jeden Pod, der Reihe nach (StatefulSet) oder beliebig (Deployment):

  1. K8s markiert den Pod als terminating + startet preStop.
  2. 10-Sekunden-LB-Drain.
  3. SIGTERM kommt an.
  4. Coordinated Shutdown läuft:
    • Keine neuen HTTP-Requests mehr annehmen (service-unbind, verdrahtet von bind()).
    • In-flight Requests drainen (service-requests-done, deins).
    • Broker-Verbindungen schließen (service-stop, verdrahtet von jedem Broker-Actor).
    • cluster.leave() ausgeben (cluster-leave, verdrahtet von Cluster.join).
    • Das Actor-System terminieren (actor-system-terminate).
  5. runUntilTerminated() löst auf, seine Signal-Handler gehen ab, und der Prozess beendet sich sauber.
  6. K8s startet einen neuen Pod aus dem neuen Image.
  7. Der neue Pod tritt dem Cluster via Seed-Provider bei.

Für sharded Entities passiert das Rebalancing automatisch — die Shards des gehenden Nodes werden neu zugeteilt; neue Entities respawnen auf dem neuen Pod aus dem Journal.