WorkerPool
WorkerPool is one of Substrate’s CRDs. It says: “I want N pre-warmed
worker pods of a given sandbox class, ready to host actors.” From a single
pool, atecontroller maintains a Kubernetes Deployment.
The spec
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: name: default labels: tier: gpu # matched against ActorTemplate.workerSelectorspec: replicas: 10 # required ateomImage: ghcr.io/agent-substrate/ateom-gvisor:vX # required sandboxClass: gvisor # gvisor (default) | microvm sandboxConfigName: gvisor-default # optional; else the class default template: # optional pod scheduling / resources nodeSelector: { cloud.google.com/gke-nodepool: gpu } tolerations: [...] priorityClassName: high nodeAffinity: {...} resources: requests: { cpu: "2", memory: 4Gi }status: replicas: 10 # actual countFields:
replicas(required,>= 0) - desired worker pod count.ateomImage(required) - the ateom container image to deploy as workers.sandboxClass(gvisordefault, ormicrovm) - the runtime family. It drives the worker pod shape (formicrovm,/dev/kvmmount plus node placement) and which SandboxConfigs are eligible.sandboxConfigName(optional) - a cluster-scopedSandboxConfigto fetch sandbox binaries from. ItssandboxClassmust match this pool’s. If empty, the cluster defaultSandboxConfigfor the class is used.template(optional,WorkerPoolPodTemplate) -nodeSelector,tolerations,priorityClassName,nodeAffinity, andresourcesfor the worker pods.
A real example: kagent-default
Here’s the live WorkerPool from the kagent namespace - installed by
the kagent Helm chart and used by every SandboxAgent running on
substrate today, including the hello-substrate
ActorTemplate:
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: name: kagent-default namespace: kagent labels: app.kubernetes.io/instance: kagent app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: kagent app.kubernetes.io/part-of: kagentspec: ateomImage: localhost:5001/ateom-gvisor:latest replicas: 2status: replicas: 2The spec omits sandboxClass (so it defaults to gvisor) and
sandboxConfigName (so it resolves the cluster default gVisor
SandboxConfig). atecontroller’s WorkerPool reconciler turned that into a
Deployment that takes the pool’s own name, kagent-default (upstream dropped
the old -deployment suffix to avoid redundant object names):
$ kubectl get deploy -n kagent kagent-defaultNAME READY UP-TO-DATE AVAILABLE AGEkagent-default 2/2 2 2 11h
$ kubectl get pods -n kagent -l ate.dev/worker-pool=kagent-defaultNAME READY STATUS RESTARTS AGEkagent-default-7f464cd64c-m2fps 1/1 Running 0 11hkagent-default-7f464cd64c-r6lhv 1/1 Running 0 11hA few things worth pointing out:
ateomImageis the sandbox-hosting pod image, not the agent image. Every pod in this Deployment runs ateom (hereateom-gvisor), the per-node manager that boots sandboxes for actors. Actor containers (e.g. thegolang-adkimage fromhello-substrate) live inside the sandbox, not in this pod’s container spec. See ateom-gvisor.replicas: 2is the whole capacity knob. This pool can host exactly two actors concurrently. Bump it and atecontroller scales the Deployment; new pods register themselves withateapiand Redis without restarting anything.- The pods are
privilegedand mount/var/lib/ateom-gvisorfrom the host. That’s how ateom manages sandbox state across the pod boundary - actors that survive a pod restart do so via files on the host path, not in the pod’s ephemeral storage. - The Deployment selector is
ate.dev/worker-pool=kagent-default. ateapi uses this label to find pool members when reconciling Redis-side worker records. - Owner-referenced by the WorkerPool. Delete the
WorkerPoolCRD and the Deployment (and all its pods) go away.
What atecontroller does with it
flowchart LR
WP[WorkerPool CRD] --> CTR[atecontroller<br/>WorkerPool reconciler]
CTR -- SSA apply --> D["K8s Deployment<br/>{name}"]
D --> P1[pod 1]
D --> P2[pod 2]
D --> PN[...]
P1 -- Ready --> S[ateapi<br/>worker syncer]
P2 -- Ready --> S
PN -- Ready --> S
S --> R[(Redis: idle worker records)]
click CTR "/components/atecontroller/" "atecontroller"
click S "/components/ateapi/" "ateapi"
- Reconciler reads the spec and builds a Deployment apply-config. For
sandboxClass: microvmit also mounts/dev/kvmand pins placement to KVM-capable nodes via aate.dev/sandboxClass=microvmnodeSelector + toleration;spec.templatesettings are merged in. - Server-side-applies a Deployment named
<workerpool-name>(same name as the pool) with the replica count,ateomImage, and pod shape. - Updates
status.replicaswith what K8s reports.
That’s the whole reconciler. It doesn’t insert worker records into Redis - that happens later via ateapi’s worker syncer, which watches the pods directly.
Isolation and eligibility
Pools are the isolation and capacity boundary. Because binding is selector-based, you can use them to:
- Separate sandbox runtimes: a
gvisorpool and amicrovmpool never share workers, and snapshots aren’t portable across classes. - Steer workloads with labels: label a pool (e.g.
tier: gpu) and gate templates to it withworkerSelector. - Scale capacity for different workload classes independently.
- Isolate failure domains.
Scaling
Edit spec.replicas, apply, done. atecontroller updates the Deployment,
new pods come up, the syncer registers them. No actor downtime. The CRD
exposes a scale subresource, so kubectl scale works too.
Scaling down: terminated pods that were hosting actors trigger the “worker died” recovery path - actors are reset from their latest snapshot.
Related
- Worker - what a pool member actually is.
- SandboxConfig - where a pool’s sandbox binaries come from.
- ActorTemplate - the
sandboxClass+workerSelectorthat gate a pool. - Worker lifecycle - the state machine.
- atecontroller - the reconciler.