콘텐츠로 이동
한국어

Kubernetes API seed provider

이 콘텐츠는 아직 번역되지 않았습니다.

KubernetesApiSeedProvider reads the Endpoints of a named Service from the K8s API and returns the ready pod IPs as seeds. Works without DNS, without SRV, without manual seed-list maintenance — the Service is your contract.

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,
);

For every ready pod backing the actor-ts Service in the namespace, the provider returns <pod-ip>:2552.

type KubernetesApiSeedProviderOptionsType = {
namespace: string;
serviceName: string;
systemName: string;
port: number;
fetchEndpoints?: () => Promise<string[]>; // override the in-cluster API
pinnedAddresses?: readonly string[]; // CIDRs the pod IPs must fall inside
log?: (message: string, error?: unknown) => void;
};
FieldWhat
namespaceK8s namespace to query — typically your app’s namespace.
serviceNameThe Service (or Endpoints) name whose ready pods form the cluster.
systemNameThe actor-system name stamped on each discovered address.
portThe cluster-transport port on each backing pod.
fetchEndpointsOverride the Endpoints-fetch function — defaults to the in-cluster API.
pinnedAddressesCIDRs the discovered pod IPs must fall inside. Unset means no pinning.
logReports addresses dropped by pinnedAddresses. Default: no-op.

For pods running in-cluster, only the last three fields are optional — the framework reads the Endpoints from the standard API at https://kubernetes.default.svc using the mounted ServiceAccount token. The other four fields are required.

An Endpoints object may name any IP, including one no pod in the cluster owns. pinnedAddresses bounds what a write to that object can redirect the bootstrap towards:

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));

Entries here are CIDRs only — Endpoints always resolve to IPs, so a host suffix could never match and is rejected at construction, as are an empty list and a malformed CIDR. Addresses outside the list are filtered out and reported through log, one message per dropped address.

This provider is already the better-defended of the two: the default fetcher pins TLS to the ServiceAccount CA, so it does not inherit DNS’s trust problem the way the DNS provider does. RBAC that can write Endpoints is still a much cheaper find than a CA key, which is what this pin costs an attacker.

The pod’s ServiceAccount needs read access to the Service’s Endpoints in the namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: actor-ts-endpoints-reader
namespace: my-app
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: actor-ts-endpoints-reader
namespace: my-app
subjects:
- kind: ServiceAccount
name: actor-ts
roleRef:
kind: Role
name: actor-ts-endpoints-reader
apiGroup: rbac.authorization.k8s.io

Without these, the lookup gets a 403; Cluster.join retries indefinitely.

From the Service’s Endpoints object:

  • Ready pods in subsets[].addresses[] → included as <podIP>:<port>.
  • Not-ready / pending pods (in notReadyAddresses) → skipped (no ready IP yet).
  • This pod itself → may be included or not depending on readiness timing; the cluster handles self-as-seed correctly.

Membership is defined by the Service’s own spec.selector — the provider only needs the Service name:

# One Service fronting every cluster pod:
apiVersion: v1
kind: Service
metadata:
name: actor-ts
spec:
clusterIP: None # headless — Endpoints carry the pod IPs
selector:
app: actor-ts
ports:
- port: 2552

Point the provider at that Service with .withServiceName('actor-ts'). For most deployments, one Service per cluster — every pod the Service selects bootstraps into one cluster.

For role-based asymmetric clusters (workers vs. HTTP gateways), a single Service whose selector (app=actor-ts) covers both roles is enough; the role tag inside the cluster is separate from the Service’s pod selection.

yes

no, first pod

Pod starts → Cluster.join called

KubernetesApiSeedProvider.lookup

GET /api/v1/namespaces/<ns>/

endpoints/<serviceName>

read subsets[].addresses[].ip

return [...podIp:port]

Cluster.join tries each seed

existing cluster?

join successfully

self-bootstrap

If you’re the first pod up, the K8s API returns this pod only. The framework’s self-bootstrap logic handles this: Cluster.join with seeds containing only itself self-promotes to leader.

Subsequent pods see the existing pods and join through them.

Service layoutEffect
One Service, selector app=actor-tsOne cluster across the namespace.
Two Services, selectors env=prod / env=stagingSeparate prod and staging clusters in one namespace.
One Service per named clusterExplicit cluster name; multiple clusters in one namespace.

Stable Service design matters — the cluster’s identity is the Service you point at. Changing which pods the Service selects (or the Service name) splits the cluster.