machined & the COSI Runtime
Chapter 01 showed the imperative half of machined: an ordered sequencer that boots, installs, and upgrades the machine. This chapter is the declarative half — how the same PID 1 process hosts a COSI controller runtime: roughly two hundred controllers that continuously reconcile a graph of typed resources toward the machine config. Both runtimes live in one process and, crucially, share one resource state. This is the engine that actually keeps a Talos node correct.
#Talos is COSI's canonical application
Before reading further: the reconciliation engine described here is not Talos code. It is COSI — the Common Operating System Interface — a reusable resource/controller runtime from cosi-project/runtime. COSI defines what a resource is, the State broker that stores and watches them, the Controller interface, and the reconcile loop that drives controllers when their inputs change. Talos imports that engine and fills it with operating-system controllers.
If you want COSI's internals — how resources are typed and versioned, how the State broker dedups and fans out watches, how a controller's reconcile loop is scheduled — read the COSI walkthrough, this site's companion deep-dive on the engine itself. This page is the other direction: how Talos uses COSI. Talos is the canonical, production-scale application of the engine, so the two documents are best read as a pair.
The handoff point is exactly one line in machined's entrypoint: the COSI runtime is started in its own goroutine before any boot sequence runs (c.V1Alpha2().Run(ctx, drainer)), then the sequencer proceeds. From that moment the controller graph is live, watching resources and reconciling, while the sequencer separately marches through its phases.
#Two runtimes in one process
Talos carries two runtimes that look nothing alike and exist for different reasons:
v1alpha1 — the sequencer
Imperative. Ordered phases of concurrent tasks: Initialize → Install → Boot, plus Upgrade, Reset, Reboot. It exists to express one-shot, order-sensitive machine operations — mount the ephemeral partition, then write udev rules, then start services. A controller graph is the wrong tool for "do A strictly before B once." Covered in Chapter 01.
v1alpha2 — the COSI graph
Declarative. ~200 controllers, each declaring inputs and outputs, reconciled continuously by the COSI runtime. It exists to express steady-state truth: as long as the machine config says "this interface has this address," a controller keeps it so — across config edits, link flaps, and restarts. No ordering is written down; dependencies are inferred from which resources a controller reads.
Why keep both? Because boot is genuinely sequential and reconciliation is genuinely continuous, and forcing either into the other's shape is painful. The sequencer gives you a clean place to say "the ephemeral filesystem must be mounted before anything that writes to it." The controller graph gives you self-healing steady state without a script that re-checks everything. Talos uses each where it fits and lets them meet in the middle — at the resource state.
#Wiring v1alpha2 inside v1alpha1
The two runtimes are not peers wired by some outer coordinator; the v1alpha2 controller is constructed by the v1alpha1 Controller and held as a field on it. NewController() builds the v1alpha1 runtime, the sequencer, and the priority lock, then hangs the COSI controller off the same runtime:
ctlr := &Controller{
r: NewRuntime(s, e, l),
s: NewSequencer(),
priorityLock: NewPriorityLock[runtime.Sequence](),
}
ctlr.v2, err = v1alpha2.NewController(ctlr.r)
The important argument is ctlr.r — the same v1alpha1 runtime object the sequencer's tasks will use. By passing it into v1alpha2.NewController, Talos guarantees the controller graph and the sequencer are not looking at two copies of the world. They share the runtime, and through it, the resource state.
#One shared resource state
This is the load-bearing detail of the whole chapter. The COSI runtime is constructed over the resource state that belongs to the v1alpha1 runtime — it does not create its own store:
ctrl.controllerRuntime, err = osruntime.NewRuntime(
v1alpha1Runtime.State().V1Alpha2().Resources(), ctrl.logger)
Read that chain right to left: State() is the v1alpha1 runtime's state object; .V1Alpha2() selects the COSI-flavoured view of it; .Resources() hands back the COSI State broker. osruntime.NewRuntime (COSI's runtime constructor) is handed that broker rather than a fresh one. So when the LoadConfig task in the boot sequence writes a MachineConfig resource, the network and Kubernetes controllers see it on their watches a moment later — same store, no copying, no IPC.
The shared state is the seam between imperative and declarative. The sequencer's tasks read and write the very resources the controllers reconcile. A task can drop a resource into the store and let the graph take over; a controller can publish status the sequencer later checks. The two runtimes coordinate entirely through typed resources rather than function calls — which is exactly the COSI philosophy, applied to an OS.
#Registering ~200 controllers
Once the runtime exists, Talos populates it. v1alpha2_controller.go builds a long slice of controller instances — one per concern — and registers each on the runtime in a loop. Conceptually:
for _, c := range []controller.Controller{
&config.AcquireController{...},
&network.AddressConfigController{},
&network.LinkStatusController{},
&k8s.NodenameController{},
&etcd.SpecController{},
// ... ~200 more across config, network, block,
// k8s, etcd, secrets, perf, cri, hardware ...
} {
if err = ctrl.controllerRuntime.RegisterController(c); err != nil {
return nil, err
}
}
Every controller is a small object that declares, via its Inputs() and Outputs(), which resource types it reads and which it owns. The COSI runtime uses those declarations to build the dependency graph and to wake a controller only when one of its inputs changes — there is no central ordering list to maintain. Add a controller, declare its inputs, and the engine schedules it correctly. (See how COSI schedules controllers for the mechanics.)
/etc detail.
The Talos root filesystem is a read-only squashfs, but controllers must write managed files like /etc/hostname, /etc/resolv.conf, and /etc/os-release. Talos mounts /etc as a tmpfs overlay over the static root and hands controllers a detached writable handle to it. So a controller can own /etc/hostname as cleanly as it owns any in-memory resource — the file is just another reconciled output, rewritten whenever the HostnameStatus resource changes.
#Bridges back into v1alpha1
Controllers are not hermetic; some need to reach the imperative world. The COSI runtime is constructed with a handful of bridges so controllers can act on things that live on the v1alpha1 side:
| Bridge | What a controller does with it |
|---|---|
system.Services | Start, stop, and query the supervised service manager — e.g. an etcd controller asks the service manager to (re)start the etcd service when its rendered spec changes. |
Runtime().Events() | Publish and watch machine events (service state, sequence progress) — the same event stream the boot entrypoint watches and talosctl dmesg/events surface. |
Platform().Mode() | Read the runtime mode (cloud/container/metal/metal-agent), threaded into many controllers as a V1Alpha1Mode field so they behave correctly per platform. |
These bridges are the reason the controller graph can do more than shuffle in-memory resources: it can mount a partition, light up a service, or emit an event the rest of the system reacts to. The split is clean — steady-state truth lives in resources; side effects on the host go through a small set of explicit bridges.
#The transform-controller pattern
The overwhelming majority of Talos's controllers are not bespoke loops — they are instances of COSI's transform controller (and its queueing cousin, the QController). A transform controller maps one input resource to one output resource by a pure function: given an input of type A, produce/maintain an output of type B. The runtime handles the watching, diffing, and cleanup; the author writes only the mapping.
This is why so much of Talos reads as a pipeline of resources: config → spec → status. The machine config is transformed into a desired spec resource; another controller transforms that spec into real-world action and publishes a status. Conceptually:
// "When MachineConfig changes, derive the desired EtcdSpec."
transform.NewController(
transform.Settings[*config.MachineConfig, *etcd.Spec]{
Name: "etcd.SpecController",
// one input resource ...
MapMetadataFunc: func(cfg *config.MachineConfig) *etcd.Spec {
return etcd.NewSpec(etcd.NamespaceName, etcd.SpecID)
},
// ... transformed into one output resource
TransformFunc: func(ctx context.Context, r controller.Reader,
l *zap.Logger, cfg *config.MachineConfig, spec *etcd.Spec) error {
spec.TypedSpec().Image = cfg.Cluster().Etcd().Image()
spec.TypedSpec().AdvertisedAddresses = deriveAddrs(cfg)
return nil
},
},
)
A separate controller then watches EtcdSpec and reconciles the real etcd service through the system.Services bridge, publishing an EtcdStatus. The same three-stage shape — config in, spec derived, status reported — recurs across networking, the kubelet, certificates, and the control plane. Learn it once and most of Talos becomes legible. (The exact transform/QController APIs are documented in the COSI controllers page.)
Controllers don't log into a void. Each writes structured logs through the COSI runtime's logger, and meaningful state changes are published to Runtime().Events(). During boot this is what makes a node observable before Kubernetes exists: talosctl logs, talosctl dmesg, and talosctl events stream controller and service activity straight off the machine, so you can watch reconciliation happen in real time over the API.
#The service manager
Some of the machine's work cannot be a controller reconcile — it is a long-running process: etcd, the kubelet, apid, trustd, containerd, udevd. These are owned by a process-wide singleton service manager, system.Services(runtime) (guarded by a sync.Once). Every managed service implements one interface:
type Service interface {
ID(Runtime) string // "etcd", "kubelet", "apid"
PreFunc(context.Context, Runtime) error // one-shot setup before launch
Runner(Runtime) (runner.Runner, error) // the supervised process itself
PostFunc(Runtime, events.ServiceState) error
Condition(Runtime) conditions.Condition // what must be true to start
DependsOn(Runtime) []string // other services required first
}
// plus optional: Volumes(Runtime) []string, HealthcheckedService
When asked to Start a service, the manager spawns a goroutine per service and blocks until it is running via a runNotify channel, so callers get back-pressure rather than fire-and-forget. Inside that goroutine, ServiceRunner.Run walks a fixed lifecycle: wait on the service's Condition plus synthesized conditions for each DependsOn service (each becomes "service X is running") plus each entry in Volumes being mounted; then run PreFunc, launch the Runner(), supervise it, and finally run PostFunc. The default restart policy is Type: Forever, RestartInterval: 5s — a crashed etcd or kubelet comes straight back.
Shutdown reverses the same dependency graph. The manager stops services in reverse dependency order — the kubelet is torn down before the things it depends on, so a draining node sheds workloads cleanly before its substrate disappears. This is the same dependency-awareness used on the way up, simply run backwards, and it's what makes the Reset and Upgrade sequences from Chapter 01 safe.
That completes the engine. machined hosts both an imperative sequencer and a declarative COSI graph over one shared resource state, registers ~200 controllers that mostly follow the config → spec → status transform pattern, bridges to the host through services and events, and supervises the long-running processes a Kubernetes node needs. Every one of those controllers begins by reading the same input — the machine's desired state. Chapter 03 opens up that machine configuration: where it comes from, how it's validated and versioned, and how it lands in the resource state as the MachineConfig the whole graph reconciles against.