RFC: External execution plane — K8s as coordination plane only #305
Replies: 7 comments
|
I also want to add that this is solving for a scale I'm not hitting myself yet. But I do expect that anyone using sympozium for ensemble scheduling in production would run into these limits, and the same goes for k8s gang scheduling at this scale. |
|
Some observations from the code as it stands today, since the repo already contains early evidence on several of these questions: The incremental path has quietly started. KSiig's #302 ( On Q1 (does anyone hit the churn regime): it's worth separating two costs the current design pays. API-server churn at 25–100 concurrent creations is the scale ceiling, but the 5s status-requeue poll and scraping results out of pod logs are latency and fragility costs paid at n=1. The recent sidecar-driven iterations surfaced exactly the failure modes the RFC's distributed-systems list predicts, at single-run scale: a On Q4 (minimum isolation bar): one concrete consequence of pull-model node agents. Today, tool policy is cluster-side config (the #303 On Q2: operating experience argues for the incremental shape with a hard rule borrowed from the radical one: nothing new should learn to depend on pod internals (log-scraping is the cautionary tale here; every consumer of it becomes a migration blocker later). Pods as one registered backend keeps @KSiig — given #302 is effectively the seam in question, your view on where the registry boundary should sit (task mode vs execution backend) would be valuable here. 🤝 Handled by Alex's Repo Steward — replies reviewed by @AlexsJones. Learn more or run your own: https://github.com/AlexsJones/repo-steward |
|
Strong +1 on the direction, though for a different reason than the one the RFC leads with. Some detailed thoughts, roughly grouped as: why I want this, how I think it has to ship, and what I think is unresolved on trust. (Code references below are against Why I want this: the non-Kubernetes fleet, not the scalingPersonally I don't foresee hitting the API-server ceiling anytime soon. The Dirigent number is real but it's not my bottleneck. What I actually want is the thing that falls out of a pull-based execution plane: the ability to run agents on machines that can't accept inbound connections. Concretely, something we've considered internally is hosting a Mac Studio in our office running local models for non-time-sensitive jobs. Today that's technically possible — That tunnel is the problem for us. We're a compliance company and EU data sovereignty is a big part of our posture, so a managed mesh VPN with a non-EU-operated coordination plane isn't something we can adopt casually — and self-hosting the coordination server, or hand-rolling WireGuard, is ongoing operational burden we'd rather not own just to reach one machine. A pull model removes the need entirely by inverting the direction. The Mac dials out to the control plane, outbound only. The office firewall stays default-deny inbound, there's no route from cloud into LAN, and no mesh VPN in scope at all. That's not a convenience difference, it's a different security posture — and it's the same reason GitHub Actions runners are pull-based. The honest trade: this makes the control plane the publicly-reachable component instead of the office network. I think that's clearly the right trade — the control plane is already internet-facing and hardened, an office LAN isn't — but worth stating explicitly rather than glossing. So my vote on Q1 is: pitch this as execution portability, not throughput. The scaling numbers are a nice secondary benefit, but "run agents anywhere, including behind NAT, without inbound access or a third-party network overlay" is the thing that unlocks new deployments. Q2: incremental only — and I don't think #302 is the pathI'd only support an incremental implementation, and I want to push back on #302 being the seam.
ConfigureAgentContainer(task *sympoziumv1alpha1.TaskSpec, agentEnv *[]corev1.EnvVar) error
AdjustSidecars(task *sympoziumv1alpha1.TaskSpec, sidecars []SidecarContext) ([]SidecarAdjustment, error)
It's also structurally in the wrong place. Both existing backends converge on So The seam that already existsI think the incremental path is the Sandbox CR backend, which already does most of what's being asked for:
That's a second execution backend, a per-run opt-in, capability detection, warm pools, and admission policy over both backend selection and isolation runtime — all shipped, all in main. Generalising Which brings me to the strong version of my Q2 position: pods as one of several registered backends, yes — but external execution needs to land as an experimental, off-by-default backend from day one. I realise that's "an experimental feature inside a beta system," but sympozium is at a reasonably stable point now and there are a lot of moving parts here. I've watched architectural reworks live on a branch that drifts from main until it's unmergeable, and the rework dies with it. Shipping behind a flag in main avoids that entirely, and the What the first PR actually isOne caveat on my own proposal: the existing second backend is only partially abstracted, and the boundary it stops at is exactly the one an external plane needs.
So the "second backend" still depends on there being a pod in the same cluster whose logs the controller can scrape. An external node agent can't satisfy that. Same story for phase observation: The first real unit of work therefore isn't "add a backend," it's give results, phase, and logs a backend-neutral contract. Full executor surface, for scoping:
Getting started has to be trivial, or the fleet never materialisesMy biggest adoption worry: if standing up your own worker node is a full-day or multi-day project, almost nobody will do it, and the portability benefit stays theoretical. I'd propose an explicit acceptance bar rather than a vague aspiration:
A single static binary or a Q4: this silently downgrades an isolation control that is currently enforcedThis is my main technical concern, and I think it's sharper than "how do we attest runtimes," because the control already exists. Today Move execution to an external node agent and that same field becomes self-reported. The admission webhook keeps validating the string against I went looking for a cryptographic fix and I don't think one exists for this specific claim. Signing keys live on hardware the operator controls, so a malicious operator signs whatever they like. The adjacent primitives don't close it: TPM/measured boot attests the boot chain, not which runtime a userspace daemon picked later; SPIFFE/SPIRE gives solid identity but says nothing about behaviour. The only real answer is confidential computing (SEV-SNP / TDX), where the CPU vendor signs a measurement of the guest — which would mean Firecracker-on-SNP specifically, not Firecracker generally. So I'd suggest inverting it: stop trying to attest the runtime, attest the operator.
The nice part is this extends That also answers the registration question, and my answer is: not an open self-registration endpoint. Admin-issued join token (short-TTL, single-use, carrying the trust tier) → node exchanges it for a client cert → mTLS thereafter. That's the k3s / Nomad / Buildkite-agent model and it's what people already expect. PKCE is the wrong tool — it's for public OAuth clients with a human in the loop, and a node agent is neither. One prerequisite: trust tiers only work if there's a routing mechanism. If dispatch is "any node pulls anything," tiers are unimplementable. Does the design include node pools/labels plus a selector on the AgentRun? If not I think that's a bigger structural gap than the attestation question itself. Two things I didn't see in the RFCCredential blast radius. This may be bigger than runtime attestation. Today provider API keys are K8s Secrets mounted into a pod in our cluster. Dispatch work to a node agent someone else operates and we're shipping live credentials to a machine we don't control — and work items already carry tool allow-lists, so they're carrying config that implies credential access. The pattern I'd draw from is Claworc's virtual keys: real credentials stay in the control plane and never enter the worker; each run gets a synthetic key, and LLM traffic flows through the control plane, which injects the real credential and records per-instance token usage and cost. Same shape as Vault dynamic secrets or Actions OIDC → short-lived cloud creds. Bonus: per-node usage attribution falls out for free. The tension worth naming: this puts the control plane back on the data path for the fattest flow in the system — bandwidth becomes O(tokens), not O(sandboxes). Survivable, since an LLM call already costs seconds and a proxy hop is noise, but it argues for the broker being a separate stateless egress component rather than living in the controller. And it needs an exemption path for locally-hosted models, otherwise it defeats the data-locality argument above — I specifically don't want my Mac Studio's local inference traffic routed out through a cloud control plane and back. Version skew. Right now the controller pins the Q5: fine with losing
|
|
Thanks for the depth here, this is exactly the response the RFC needed. You've convinced me on the seam. I pitched #302's registry as the executor seam, but your trace is right: Also agreed that the first PR is the boundary work, not a new backend. Results, phase, and logs all assume a same-cluster pod today ( Trust tiers over runtime attestation makes sense to me. Your question about whether On the acceptance bar: adopted as written. One command, one join token, five minutes, works behind NAT. The JetStream analysis (LimitsPolicy/24h/R1 is notification tuning, not dispatch tuning; AckWait becomes the lease duration) is too useful to leave buried in a comment thread. It's going into the RFC's failure-modes section, credited to you. |
|
It seems to me that we're fairly aligned on the direction this should go in. From here, I think it's mostly a matter of defining the milestones in which the new execution plane should be implemented. I'll leave that to you as the maintainer, but happy to provide input if needed! Of course that's barring input from anyone else. |
|
I'll put this on X to get a bit more visibility on it |
|
Sounds good. I really think this is a cool feature, and could be the thing that truly positions Sympozium as something unique in the industry. It already is, no other K8s-native LLM scheduling projects that I can find. But, combining that with external execution planes just brings it up an extra notch. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I've been looking into whether Kubernetes is the right long-term substrate for agent orchestration, prompted by Modal's Scaling to 1 million concurrent sandboxes in seconds. Before we commit to a direction I'd like the community's thoughts, including on a fairly radical option at the end.
The thesis
Split the coordination plane from the execution plane. Kubernetes stays as the coordination plane: CRDs, RBAC, admission, policy, GitOps, observability. The execution plane (where agent sandboxes actually run) becomes pluggable, and nothing strongly consistent sits on the per-run critical path. Networking learned this lesson years ago with the control plane/data plane split; this is the same idea applied to orchestration.
Why (the short version of the evidence)
What this could look like for sympozium
Today the controller is the mechanism: it creates a Job per AgentRun, polls status on a 5s requeue, and scrapes the result from pod logs. The proposal is to invert that. AgentRun becomes a desired-state record, dispatch and status flow over a fast path (we already run NATS for the ipc-bridge), and the controller writes status back lazily.
flowchart LR subgraph K8s coordination plane CR[AgentRun CR - spec is intent] --> C[controller - policy and validation] C -->|lazy batched status| CR end C -->|publish work item| Q[(NATS JetStream - work queue and status stream)] Q -->|claim - pull model| NA[node-agent - any cloud or bare metal] NA -->|status, result, heartbeat| Q Q -->|consume| C NA --> S1[Firecracker / gVisor / runc sandbox]Node agents dial out and pull work, GitHub-runner style. There are no inbound networking assumptions, so the execution plane is cloud-agnostic by construction: EC2, bare metal, homelab. The hard parts are the usual distributed-systems list: idempotent dispatch and fencing, orphan reconciliation in both directions (leases/heartbeats), and status write-back discipline. The hardest part, since we treat agents as adversarial, is that policy enforcement moves into the node agent. NetworkPolicy and SecurityContext don't exist off-cluster, so the agent has to enforce isolation and attest to it.
The radical option I'm weighing
We already have a seam: the controller branches between a Job and an agent-sandbox
SandboxCR. The incremental path is promoting that branch to an executor interface, with pods as one backend among several.The stronger version: agent workloads are never reconciled by our controller at all. Pods stop being the default execution primitive. Every run executes on the execution plane (in-cluster runners or a remote fleet), and Kubernetes holds only intent, policy, and the durable record. The controller's job shrinks from managing pods to enforcing policy and keeping the record.
Questions for you
kubectl get podsvisibility for runs: dealbreaker, or fine if an aggregated API (metrics-server-style, served from memory) shows live runs?Nothing is decided here; this is a research direction, and counter-evidence is as welcome as agreement. If you've scaled K8s past these ceilings with boring mitigations (warm pools, image streaming, Kata RuntimeClasses), I especially want to hear about it.
All reactions