Compose — declarative cluster spec
Status: shipped. Phases 1–5 landed in CLI v0.1.16. See Rollout at the bottom for details.
A lakeshore.yaml at the project root declares the cluster you want to
exist: which queues, which daemons, how many, in which mode. lakeshore up reconciles; lakeshore down tears down. The mental model is
docker-compose for fleets — one file, one verb, idempotent.
Why
08-queues-cpu-vs-gpu/README.md is six imperative commands:
That sequence is the literal serialization of a document we should have. With compose it collapses to:
— and the document lives in the repo, version-controlled with the code that depends on it.
Schema (v1)
Field reference
| Field | Required | Notes |
|---|---|---|
version | yes | integer; v1 is the only one |
project | yes | slug; tags every Worker row spawned by this file |
namespace | no | overrides the auth.yml default |
queues.<name> | no | same shape as lakeshore queues add flags |
modes.<name> | no | inline; merges over the same name from .dreamrc if both exist (inline wins) |
daemons.<name>.mode | yes | references a key under modes: (inline) or .dreamrc |
daemons.<name>.queues | no | defaults to [] (implicit default queue) if omitted |
daemons.<name>.count | no | defaults to 1 |
daemons.<name>.keep_alive_s | no | per-daemon override |
daemons.<name>.setup | no | shell command(s) run once after the daemon registers (e.g. pip install -r requirements.txt). Runs before any invocation is accepted. |
daemons.<name>.startup | no | shell command(s) run every time the daemon process starts (including restarts). Use for environment checks or service warm-up. |
daemons.<name>.python | no | path to the Python interpreter on the worker (e.g. /opt/conda/bin/python-sdk). Overrides the default python3. |
daemons.<name>.depends_on | no | ordering hint for up; advisory only in v1 |
Naming rule
Daemon labels are <group>-<idx>. With daemons.cpu.count: 5, you get
cpu-0, cpu-1, … cpu-4. Labels are global within a project; two
composes with the same project name will collide — by design.
CLI surface
Convention: lakeshore <verb> without flags reads ./lakeshore.yaml.
-f <path> overrides.
State model
Three states matter:
- Desired is the YAML on disk.
- Actual is the set of Worker rows on the CP filtered by
compose_project == <project>. upwalks the diff: add what's missing, leave matching rows alone, optionally prompt to remove orphans (--prune).- No live controller. Reconciliation runs only when
upis invoked. Drift is reported bystatus; the user decides when to reconcile.
Project isolation
A new column on Worker:
up filters and operates only on rows where
composeProject == <yaml.project>. Daemons launched ad-hoc via
lakeshore daemon launch have composeProject == null and are
invisible to compose verbs. This is the same isolation pattern as
docker-compose's com.docker.compose.project label.
Mode resolution
daemons.<name>.mode is resolved in this order:
- Inline under
modes:in thislakeshore.yaml. .dreamrcat the project root.~/.lakeshore/modes/<name>.yml(shared catalog — future).
Inline wins. Conflicting fields between sources are merged at the field
level (inline instance_type: t3.large overrides a .dreamrc
instance_type: t3.medium, but inherits the rest).
Queue resolution
daemons.<name>.queues is a hard reference to queue names. If a
referenced queue doesn't exist on the CP:
- If the queue is also declared under
queues:in this file →upcreates it. - If not →
uperrors and refuses to proceed.
Queues declared under queues: but not referenced by any daemon are
created anyway (idempotent — queues add with same name+config is a
no-op).
What about lakeshore exec from a compose?
Out of scope. Compose declares the cluster; exec dispatches to it. The
two compose because queue references are stable: write
lakeshore exec --queue cpu-queue … and it dispatches to whichever
daemons compose put in cpu-queue.
Non-goals (v1)
- Autoscaling.
count: 5is a hard number, not a target. No HPA controller. Phase 3 work — needs a metric source. - Live drift reconciliation. No daemon process that runs
upcontinuously. Phase 4 — and only if pain demands it. - Cross-namespace composes. One file = one namespace.
- Multi-file composes (compose overrides like
docker-compose.override.yml). Phase 2 if requested. - Volume / network specs. We don't have those primitives.
- Inline secrets. Reference them by env (
${LAKESHORE_PROVIDER_KEY}) or rely on the worker's IAM role / provider creds.
Boundaries with existing concepts
| Concept | Lives in | Compose role |
|---|---|---|
| Mode | .dreamrc or inline modes: | how a daemon is born |
| Queue | CP (declared in queues:) | where a daemon lives, who can dispatch to it |
| Project | project: in this file | "this cluster, owned by this file" |
| Daemon group | a key under daemons: | the named recipe block |
| Daemon | a Worker row | one of count instances of a group |
The compose document doesn't add primitives — it's a layout over the primitives we already have. Mode + queue + count is the existing data shape; this is just the document that says "these specific instances of those should exist."
Rollout
| Phase | Scope | Status |
|---|---|---|
| 0 | This design doc | done |
| 1 | lakeshore.yaml loader + schema validation (no side effects) | shipped |
| 2 | lakeshore up (creates queues + launches daemons; idempotent) | shipped |
| 3 | lakeshore down (drain + optional terminate) | shipped |
| 4 | lakeshore status / ps / logs | shipped |
| 5 | --plan, --wait, --prune flags | shipped |
| 6 | Multi-file overrides, autoscaling, etc. | not committed |
Phases 1–5 shipped in CLI v0.1.16 (lakeshore/src/cli/get-started/compose/).
08-queues-cpu-vs-gpu now collapses to
lakeshore up && lakeshore exec --queue cpu-queue ….
Open questions
- Should
upblock on queue state == archived? Probably yes — an archived queue refuses new work, so daemons launched into it would be inert. Error and tell the operator toqueues unarchivefirst. - What happens to in-flight execs on
down? Default: graceful drain (refuse new work, finish in-flight, then terminate).--forcekills immediately. Need to flesh out the "drain" wire — currentlydaemon kill --terminateis hard-stop only. - Do we surface
compose_projectondaemon list? Yes, as a column when present. Lets operators see which compose owns which daemons at a glance. - What about a
lakeshore initto scaffold alakeshore.yaml? Phase 5 — would template from an existing.dreamrcmode.