콘텐츠로 이동
한국어

KubernetesLease

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

KubernetesLease implements the Lease interface against Kubernetes’s built-in Lease resource (the coordination.k8s.io/v1 API). Production-grade: backed by etcd, strongly consistent, RBAC-controlled.

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

The K8s API server’s etcd-backed store provides the single-holder guarantee. Two pods concurrently calling acquire() produce exactly one winner, regardless of pod scheduling, network partition between pods, etc.

interface KubernetesLeaseSettings {
// From LeaseSettings:
name: string;
owner: string;
ttlMs: number;
renewalIntervalMs?: number;
acquireRetries?: number;
acquireRetryDelayMs?: number;
// K8s-specific:
namespace: string;
apiBaseUrl?: string; // override the in-cluster default
serviceAccountToken?: string; // override the in-cluster default
}
K8s fieldDefaultWhat
namespacerequiredK8s namespace where the Lease resource lives.
apiBaseUrlin-clusterThe K8s API server URL — defaults to https://kubernetes.default.svc.
serviceAccountTokenin-clusterThe pod’s service account token — defaults to /var/run/secrets/kubernetes.io/serviceaccount/token.

For pods running in-cluster, you only need namespace and name (+ the standard LeaseSettings fields). The framework reads the API URL and token from the standard locations.

For tests / dev pointing at a local K8s API (kind, minikube), override apiBaseUrl + serviceAccountToken.

The pod’s ServiceAccount needs permission to manage Lease resources:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: actor-ts-lease-holder
namespace: my-app
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: actor-ts-lease-holder
namespace: my-app
subjects:
- kind: ServiceAccount
name: actor-ts
roleBinding:
kind: Role
name: actor-ts-lease-holder
apiGroup: rbac.authorization.k8s.io

Without these, acquire() rejects with 403 (forbidden).

Without delete, release() works but leaves the Lease object behind after release (harmless; the next acquire reuses it).

The first acquire() call creates a Lease object:

Terminal window
$ kubectl get lease -n my-app
NAME HOLDER AGE
my-singleton-lease pod-abc-1 30s

The framework writes:

  • metadata.name — the lease name.
  • spec.holderIdentity — the owner.
  • spec.acquireTime — when this owner took it.
  • spec.renewTime — last renewal (updated every renewalIntervalMs).
  • spec.leaseDurationSeconds — derived from ttlMs.

Other holders check renewTime + leaseDurationSeconds < now() to decide whether the current holder is stale.

no

yes

this owner already holds

another holder, still fresh

another holder, stale

acquire

GET the lease object

exists?

CREATE with this owner

if 409 conflict — retry

check holder + renewTime

who holds it?

return true — idempotent

return false — contention

CAS — replace owner if

renewTime matches

The atomicity comes from K8s’s optimistic-concurrency CAS via resourceVersion — two simultaneous attempts to claim a stale lease produce one winner.

While holding, the framework patches spec.renewTime every renewalIntervalMs:

PATCH /apis/coordination.k8s.io/v1/namespaces/<ns>/leases/<name>
{ spec: { renewTime: "2025-05-13T12:00:00.000Z" } }

If the patch fails:

  • Transient (5xx, connection refused) → retry, log, eventually give up if ttlMs elapses without success.
  • CAS conflict (409) → another holder took over; fire onLost.

onLost fires when:

  • A renewal patch returns CAS conflict.
  • The framework observes the lease was modified by someone else (a probe GET before some critical operation).
  • Network partition prevents renewal for longer than ttlMs.

The handler should drop ownership state immediately — see Lease API for the contract.

Each lease holder generates:

  • 1 GET + (potentially) 1 CREATE on acquire.
  • 1 PATCH every renewalIntervalMs while holding.
  • 1 PATCH (or DELETE) on release.

For a 30-second TTL with 10-second renewal, that’s ~6 API calls per minute per lease. Pennies on any modest K8s deployment.

For clusters with many leases (e.g., one per sharded entity type

  • one per singleton + one per coordinator), the API server load is still negligible — K8s easily handles thousands of Lease writes per second.

For integration tests with a real K8s API (kind, minikube, ephemeral CI clusters):

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

Use unique lease names per test (random UUID suffix) so parallel tests don’t fight. Tear down with release() + a final delete sweep in test teardown.