apply  /  talos case study source-grounded canonical
Chapter 07 · grounded in siderolabs/talos

Talos: Where COSI Was Born

This chapter is the origin story: Talos Linux is what COSI was built for. The entire operating system — networking, configuration, services, upgrades, hardware discovery — is modeled as a graph of controllers reconciling typed resources against a central state broker. COSI was extracted from Talos to be reusable. Reading Talos is reading the library at full depth, in full production, on real machines.

How this page was written.

All resource types, namespace names, controller names, file paths, and code excerpts are drawn from the public siderolabs/talos repository (github.com/siderolabs/talos). Resource types live in pkg/machinery/resources/; controllers live in internal/app/machined/pkg/controllers/. The architecture described here is accurate as of the Talos v1.x series. Fit assessment and synthesis are labelled as such.

#What Talos is

Talos Linux is an immutable, API-only operating system purpose-built for running Kubernetes. It has no SSH, no shell, no package manager, no user accounts. Every configuration change is a structured API call. Every observable fact about the machine — its IP addresses, its hostname, the status of its services — is a structured API response. The system is designed around one guarantee: if you know the desired state, the machine will converge to it, deterministically, from any starting point.

🔒

Immutable by design

The OS root is a read-only SquashFS image loop-mounted into memory. Configuration is applied through the machine API — no SSH, no shell, no /etc edits. The entire runtime state lives in COSI.

⚙️

~120 controllers, 13 namespaces

Talos registers ~120 controllers across config, network, etcd, k8s, runtime, hardware, block, kubespan, and other domains. Every subsystem is a controller. Every subsystem communicates only through state.

🌐

API = COSI state

The external Talos API (apid) exposes the COSI state broker over gRPC. talosctl get addresses is a COSI List. talosctl watch is a COSI WatchKind. The dependency graph is inspectable with talosctl inspect dependencies (Graphviz output).

#The origin: COSI as extracted infrastructure

COSI was not designed in the abstract and then applied to Talos — it emerged from Talos. Sidero Labs built the resource/controller engine inside the Talos monorepo, discovered it was the load-bearing architecture for the entire OS, and then extracted it into github.com/cosi-project/runtime so other systems could use the same substrate. The current cosi-project/runtime library is a direct descendant of code that has been running on bare metal in production since Talos v0.x.

What this means for the walkthrough.

Every concept in chapters 01–05 was first real in Talos. The conformance tests in cosi-project/runtime/pkg/controller/conformance/ test exactly what Talos's controllers depend on. If you understand how Talos uses COSI, you understand what the library was designed for. This is the ground truth.

#The topology: machined and its runtimes

On each Talos node, the main system daemon is machined — effectively the init daemon and PID 1. It owns the COSI runtime and registers every controller. The COSI state.State is the machine's memory — everything transient and derived is held entirely in-memory; only the MachineConfig is persisted to disk and is re-loaded as a resource on each boot. All other resources are reconstructed from scratch by their controllers.

machined — PID 1 equivalent · owns COSI state · registers all ~120 controllers COSI State namespaced in-memory store ~8 namespaces · ~50 resource types single source of truth on node COSI Runtime ~120 controllers registered watch → dedup → wake Config controllers parse MachineConfig write typed sub-resources ~12 controllers Network controllers Spec+Status · merge netlink bridge DHCP · DNS · routes ~50 controllers Runtime controllers services · mounts upgrades · extensions ~30 controllers Hardware sysfs probes CPU · memory · disks ~8 controllers Block/Disk volumes · encryption ~15 controllers apid gRPC bridge serves COSI state to external clients talosctl get · watch · apply
All ~120 controllers run inside machined, wired to the same COSI State. apid is a thin gRPC bridge — it adds auth, then proxies directly to the state broker. External tools (talosctl, Omni) are just COSI clients.

#The bootstrap sequence: resources coming to life

COSI state is entirely in-memory. On every boot, it starts empty and reconstructs from scratch. The order in which resources appear is exactly the dependency graph the runtime has computed. The sequence below shows how a Talos control-plane node goes from power-on to a running Kubernetes cluster — every step is a controller waking up because its inputs appeared:

  1. machined starts (PID 1 equivalent). COSI runtime initializes. All namespaces exist but contain zero resources. Controllers are registered but blocked on r.EventCh().
  2. ConfigAcquireController reads persistent config from disk and creates the config.MachineConfig resource (ID: v1alpha1). This is the trigger event for everything else.
  3. ~12 config controllers wake simultaneously. MachineTypeController, TimeServerConfigController, NetworkConfigController, and others all watched MachineConfig. They fan out, writing their typed sub-resources into network, k8s, etcd, etc.
  4. Network controllers cascade. LinkConfigController writes LinkSpec resources → LinkSpecController applies them to the kernel → LinkStatusController reflects actual state back as LinkStatus. Same for addresses and routes. NodeAddressController aggregates AddressStatus → writes NodeAddress.
  5. etcd and Kubernetes controllers unblock. They were waiting for NodeAddress (needed to compute peer/advertise addresses). Now etcd.SpecController writes etcd.Spec → etcd service starts → becomes healthy.
  6. KubeletStaticPodController writes k8s.StaticPod resources for kube-apiserver, kube-controller-manager, kube-scheduler. A local HTTP server (part of machined) serves these manifests to kubelet. Kubernetes control plane comes up.
  7. Cluster controllers join. cluster.Member and cluster.Affiliate resources appear as the node joins the etcd cluster and the Kubernetes API becomes available.

The key property: no step happens before its dependencies are satisfied, and no step polls — everything is triggered by the appearance of the resources it was waiting for. If you change the machine config mid-run, only the controllers whose inputs changed wake up; everything else stays quiet. The dependency graph, not a startup script, is the operating system's boot logic.

#The resource catalog: namespaces and types

Talos organizes every resource into a namespace. The namespace is the first axis of isolation — controllers in the network namespace only declare outputs there; no other controller can write network addresses. Here is the full catalog as of Talos v1.x:

NamespacePackageWhat lives hereWho writes
configresources/configMachineConfig, MachineType, ConfigStatus, ConfigPatchmachined on apply; config controllers
networkresources/networkAddressSpec/Status, LinkSpec/Status, RouteSpec/Status, HostnameSpec/Status, DNSResolvConf, NfTablesAddressSet, NodeAddress, …network controllers exclusively
runtimeresources/runtimeMachineStatus, UniqueMachineToken, ExtensionServiceConfig, KernelModuleSpec, MountStatus, …runtime controllers
hardwareresources/hardwareProcessor, MemoryModule, SystemInformationhardware discovery controllers
clusterresources/clusterMember, Affiliate, Config, Identitycluster and discovery controllers
secretsresources/secretsEtcd, Kubernetes, Root, OSRoot, APIsecrets bootstrap controller
blockresources/blockDisk, Volume, VolumeConfig, VolumeStatus, SystemDiskblock and volume controllers
filesystemsresources/filesystemsMountpoint — active filesystem mountsmount controllers
k8sresources/k8sAPIServerConfig, ControllerManagerConfig, KubeletConfig, StaticPod, StaticPodStatus, Nodename, NodeIP, manifest specs, etcd peer configKubernetes bootstrap controllers
etcdresources/etcdConfig, Spec, Member, PKIStatus — control-plane onlyetcd controllers
kubespanresources/kubespanConfig, Identity, Endpoint, PeerSpec/Status — WireGuard overlayKubeSpan controllers
timeresources/timeStatus, AdjtimeStatus — NTP sync statetime sync controllers
filesresources/filesEtcFileSpec/Status — managed /etc filesfile management controllers
storageresources/storageLVM logical/physical volume specs and statusesstorage/LVM controllers
v1alpha1boot sequence trackers, legacy migration resourcesvarious

Every type name resolves through resource.ResourceType. For example, network.AddressSpecType = "AddressSpecs.net.talos.dev". The .net.talos.dev group suffix is how Talos scopes its types — a convention you'd replicate in your own application with your own domain suffix.

#The config plane

A Talos machine is configured by writing a MachineConfig resource (a YAML document) over the machine API. Internally, machined creates a config.MachineConfig resource in the config namespace with the stable ID "v1alpha1". From that single resource, approximately 12 config controllers fan out into typed sub-resources that downstream controllers can consume without parsing YAML:

MachineConfig config namespace id = "v1alpha1" written by machined on apply config controllers fan-out MachineTypeController TimeServerConfigController NetworkConfigController K8sControlPlaneController InstallConfigController … + ~7 more typed sub-resources config.MachineType config.Timeserver network.LinkSpec (×N) k8s.ControlPlaneConfig block.VolumeConfig … consumed by network / runtime / k8s controllers without touching YAML no YAML parsing past the config plane
The MachineConfig YAML is parsed exactly once at the config plane boundary. Every downstream controller gets a typed, versioned Go struct — no YAML parsing deeper in the stack.

This is the key architectural insight of Talos: structured config input is translated to typed resources at the edge, and the rest of the system never sees raw config again. The network controller doesn't know what YAML looks like. It watches network.LinkSpec resources. If the config plane correctly populated them, the network plane is correct.

The transform.Controller shortcut.

For simple one-input → one-output transformations, Talos uses the transform.NewController helper from pkg/controller/generic. You provide a MapMetadataOptionalFunc (does the output exist?) and a TransformFunc (compute it) — the runtime loop is handled for you. Most config-plane controllers use this pattern: etcd.ConfigController, k8s.APIServerConfigController, and others. It's the idiomatic COSI "transform" controller from chapter 03, with the boilerplate collapsed.

controllers/etcd/config.go — transform.Controller in practicego
// etcd.ConfigController uses the transform.Controller generic — no explicit Run() loop.
// The runtime wakes it when MachineConfig changes; TransformFunc does the work.
func NewConfigController() *transform.Controller[*config.MachineConfig, *etcd.Config] {
    return transform.NewController(
        transform.Settings[*config.MachineConfig, *etcd.Config]{
            Name: "etcd.ConfigController",
            MapMetadataFunc: func(cfg *config.MachineConfig) *etcd.Config {
                return etcd.NewConfig(etcd.NamespaceName, etcd.ConfigID)
            },
            TransformFunc: func(ctx context.Context, r controller.Runtime, logger *zap.Logger,
                cfg *config.MachineConfig, out *etcd.Config) error {
                spec := out.TypedSpec()
                spec.AdvertiseSubnets = cfg.Config().Cluster().Etcd().Subnet()
                spec.ExtraArgs = cfg.Config().Cluster().Etcd().ExtraArgs()
                // ... etc.
                return nil
            },
        },
    )
}
internal/app/machined/pkg/controllers/config/machine_type.go (simplified)go
// When you need full control (multiple inputs, complex logic), plain Controller still applies.
// MachineTypeController shows the hand-rolled pattern.
func (ctrl *MachineTypeController) Inputs() []controller.Input {
    return []controller.Input{{
        Namespace: config.NamespaceName,
        Type:      config.MachineConfigType,
        ID:        optional.Some(config.ActiveID),
        Kind:      controller.InputWeak,
    }}
}

func (ctrl *MachineTypeController) Outputs() []controller.Output {
    return []controller.Output{{Type: config.MachineTypeType, Kind: controller.OutputExclusive}}
}

func (ctrl *MachineTypeController) Run(ctx context.Context, r controller.Runtime, _ *zap.Logger) error {
    for {
        select {
        case <-ctx.Done(): return nil
        case <-r.EventCh():
        }
        cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID)
        // … write config.MachineType based on cfg.Config().Machine().Type()
    }
}

#The network plane: spec/status at scale

The network plane is where COSI earns its keep most visibly in Talos — and it's the most developed network management stack you'll find built on this model. Every network concept has a strict Spec/Status split:

ConceptSpec (desired)Status (observed)Who writes Status
IP addressnetwork.AddressSpecnetwork.AddressStatusAddressStatusController (netlink)
Network interfacenetwork.LinkSpecnetwork.LinkStatusLinkStatusController (netlink)
Routing table entrynetwork.RouteSpecnetwork.RouteStatusRouteStatusController (netlink)
Hostnamenetwork.HostnameSpecnetwork.HostnameStatusHostnameStatusController (syscall)
DNS resolversnetwork.DNSResolvConfkernel writes /etc/resolv.conf
Node's external IPsnetwork.NodeAddressNodeAddressController (derived)

The Status controllers are wrappers around the Linux kernel's netlink API. They do one job: translate kernel state into COSI resources. They never read Spec resources. They are the ingest edge of the system — real-world facts flowing in. The applier controllers (e.g., AddressConfigController) do the other half: they read Spec, compare to Status, and call netlink to reconcile the difference. The two halves never directly coordinate — they talk only through shared resources.

#Multi-source merge: the network controller graph

The most sophisticated pattern in Talos's network plane is multi-source spec merging. A machine's IP address can come from three sources: the MachineConfig, a DHCP response, or the kernel cmdline. Each source is a separate controller that writes AddressSpec resources with a layer weight in the resource ID. A merge controller collects all the sources and produces the final desired spec.

MachineConfig network.address[] static DHCP client DHCPv4 · DHCPv6 lease Kernel cmdline talos.network.interface.* AddressSpec (shared) config/eth0/static layer=3 · prio=high dhcp/eth0/v4 layer=2 · prio=med cmdline/eth0 layer=1 · prio=low MergeController reads all Specs highest-prio wins AddressSpec (final) merged desired address AddressConfigCtrl Spec + Status → netlink AddressStatus actual kernel address kernel netlink
Multiple controllers write AddressSpec resources (shared output) with priority-encoded IDs. The merge controller collects all and produces the winning desired state. The applier then reconciles Spec against Status using netlink.

This is the shared output pattern from chapter 03 at full scale. Multiple controllers contribute to the same resource type without conflict, because each one owns distinct resource IDs (keyed by source and interface). The merge controller is the only one that reads across all of them.

#The runtime plane: services as resources

Every system service in Talos — kubelet, etcd, apid, cri, extension services — is managed by a COSI controller. The pattern is identical to the network plane:

  1. A config controller reads the relevant MachineConfig section and writes a typed config resource (e.g., k8s.KubeletConfig, k8s.EtcdConfig).
  2. A service controller reads that config resource and manages the actual system service via go-runner (Talos's process runner). It starts, restarts, and stops the service based on resource phase changes.
  3. A status controller reflects the service's actual health back as a runtime.ServiceStatus or similar resource.

For example, the kubelet lifecycle: MachineConfigk8s.KubeletConfig (config plane) → KubeletController starts the kubelet process → ServiceStatusController writes runtime.ServiceStatus{Running: true}. If the machine config changes (say, the kubelet extra args), the config plane updates k8s.KubeletConfig, which wakes the KubeletController, which restarts kubelet with the new args. The controller never parses YAML — it reads a typed Go struct.

internal/app/machined/pkg/controllers/k8s/kubelet.go (simplified)go
func (ctrl *KubeletController) Inputs() []controller.Input {
    return []controller.Input{
        {Namespace: k8s.NamespaceName, Type: k8s.KubeletConfigType, Kind: controller.InputWeak},
        {Namespace: secrets.NamespaceName, Type: secrets.KubernetesType, Kind: controller.InputWeak},
        // also watches network.NodeAddress so it can update --node-ip on address change
        {Namespace: network.NamespaceName, Type: network.NodeAddressType, Kind: controller.InputWeak},
    }
}

func (ctrl *KubeletController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
    for {
        select {
        case <-ctx.Done(): return nil
        case <-r.EventCh():
        }
        kubeletCfg, _ := safe.ReaderGetByID[*k8s.KubeletConfig](ctx, r, k8s.KubeletID)
        nodeAddresses, _ := safe.ReaderListAll[*network.NodeAddress](ctx, r)
        // build the args list, reconcile against the running process
        ctrl.runner.Reconcile(ctx, buildArgs(kubeletCfg, nodeAddresses))
    }
}

Notice that KubeletController also watches network.NodeAddress — so when the machine's IP changes (say, a DHCP renewal), the kubelet is automatically restarted with the updated --node-ip flag. The network plane and the service plane are decoupled; they communicate through state. This is exactly the "no controller calls another controller" design principle from chapter 00, at OS scale.

#The Talos API: COSI state over gRPC

From the outside, the Talos API looks like a conventional gRPC service. Under the hood, apid is a thin auth layer on top of the COSI gRPC transport from chapter 05. When you run talosctl get addresses, here's what actually happens:

talosctl commandCOSI operationWhat it returns
get addressesstate.List(AddressStatusType)All AddressStatus resources in the network namespace
get linksstate.List(LinkStatusType)All LinkStatus resources
get machineconfigstate.Get(MachineConfigType, "v1alpha1")The active MachineConfig resource
watch machineconfigstate.WatchKind(MachineConfigType)Streaming change events as the config is updated
apply-configstate.Update(MachineConfigType)Triggers the config plane reconcile cascade
get servicesstate.List(ServiceType)All runtime.Service resources

The COSI gRPC server in apid wraps the state.State with an access-control layer (AdminRole can read all namespaces; OS-role can only read public resources), then connects it directly to the gRPC State service defined in chapter 05. There is no translation layer, no separate API model — the external API is the resource model. This is what the README means by "the interface runs from local to gRPC without changing your controllers."

internal/app/apid/pkg/backend/apid.go (simplified)go
// apid registers the COSI gRPC State service using the same state.State
// that the controller runtime uses. External clients get a live, watched
// view of everything inside machined — with RBAC on top.
func (b *Backend) Register(server *grpc.Server) {
    cosiv1.RegisterStateServer(server,
        server_state.NewState(b.cosState, authz.NewRBACInterceptor(b.roles)),
    )
    machine.RegisterMachineServiceServer(server, &machineService{backend: b})
}

#Omni: distributed COSI at fleet scale

Sidero's Omni is the multi-node control plane that manages fleets of Talos machines. It illustrates the gRPC transport from chapter 05 at real-world scale: Omni maintains a persistent gRPC connection to each Talos node and aggregates their individual COSI states into a single federated view. From Omni's perspective, each node is a remote CoreState — reads and watches go over the wire, and Omni's own controllers react to changes on any of them.

What Omni's COSI runtime watches

Omni watches each node's network.NodeAddress, runtime.MachineStatus, config.MachineConfig, cluster.Member, hardware.SystemInformation, and health resources — across all managed nodes simultaneously. Change events from node A trigger Omni controllers that may update aggregated resources, kubeconfig secrets, or schedule a Talos upgrade.

What Omni writes back

Omni's controllers can write to a node's COSI state as well — most notably by pushing a new MachineConfig (which triggers the full node-side config plane cascade), or by writing cluster.Identity resources during bootstrap. The channel is bidirectional: Omni's state reads drive its own reconcile; its writes drive the node's reconcile.

Omni control plane COSI State (Omni's own) Cluster · Machine · MachineSet · … Omni controllers cluster bootstrap · upgrades · health gRPC client (state.WrapCore) one per node · persistent reads node state into Omni state Talos Node A apid · COSI state MachineStatus · NodeAddress MachineConfig · … gRPC State server Talos Node B apid · COSI state gRPC State server Talos Node N… same pattern Watch + Get omnictl / Web UI reads Omni's COSI state same gRPC transport
Omni's gRPC clients watch each node's COSI state into Omni's own COSI state. Omni controllers then reconcile over the aggregated view — the same reconcile model, one level up the hierarchy.

#Lessons from a real OS

Talos is the longest-running COSI application. Several patterns appear here that you won't find spelled out in the library documentation:

One YAML parse, many typed consumers

Parse user input exactly once at the system boundary. Every downstream controller gets typed Go structs. This eliminates the "each subsystem interprets config differently" class of bugs. In Talos: MachineConfig → typed sub-resources → controllers that never see YAML.

Shared output + merge controller

When multiple sources can contribute to the same final state (static config + DHCP + cmdline for addresses), model each source as a shared-output writer and add a single merge controller that picks the winner by priority. No locks, no callbacks — just resource priority.

Status controllers as the ingest edge

Every fact from the outside world (kernel netlink, sysfs, process health) flows in through a dedicated Status controller that only writes. It never reads Spec resources. The separation is strict: the "what should be" graph and the "what is" graph share no writers.

Cross-plane watching

A service controller can watch a network resource (NodeAddress) — because the information it needs is already in state, not in a separate API call. This is the "controllers communicate only through state" principle delivering a concrete benefit: the kubelet restarts automatically on IP change with zero inter-controller coupling.

Namespace as trust boundary

The secrets namespace holds all TLS material. No controller outside the secrets subsystem has secrets as an output. The namespace boundary, enforced by the engine, is the security boundary. No special ACL required — the ownership rules are the ACL.

The API is the resource model

Talos chose to expose COSI state directly as its external API rather than building a translation layer. This paid off: external tools like Omni can read typed Talos resources with the same safe.StateGetByID they'd use internally. There's one model, everywhere.

#Verdict

Bottom line for COSI learners.

Talos is not a case where COSI was evaluated and chosen — it is the case COSI was designed around. Every chapter of this walkthrough — resources, state, controllers, the runtime engine, gRPC distribution — maps to something Talos does in production at scale on real hardware. The patterns here (typed config fan-out, Spec/Status split, multi-source merge, namespace-as-trust-boundary, API-as-resource-model) aren't guidelines, they're evolved survival traits. If you're building anything that looks like "desired state in, real-world convergence out, observable from the outside" — this is the template. Read the Talos source the same way you'd read the Linux kernel for OS design patterns: it's the canonical application of everything the library is built to enable.

To apply these patterns to your own systems, see Chapter 06 for the decision framework and the domain-to-COSI mapping table.