internals  /  grpc pkg/state/protobuf
Chapter 05

gRPC & Distribution

The single most leverage-giving fact about COSI: local and remote state are the same interface. A state.State backed by an in-process map and a state.State backed by a gRPC connection to another process are indistinguishable to your controllers. This chapter shows the transport that makes that true — and what it unlocks for distributed control planes.

#The big idea: state as a service

Because controllers only ever talk to state.State, you can put a network in the middle of that interface and nothing upstream notices. The server adapter exposes a local CoreState as a gRPC service; the client adapter implements CoreState by calling that service. Wrap the client in WrapCore and you have a fully-featured remote state.State.

process A — agent / worker your controllers state.State (WrapCore) client.Adapter implements CoreState via StateClient process B — state authority server.NewState(core) RegisterStateServer state.State inmem / bolt CoreState the real store (+ persistence) gRPC unix socket or TCP · streaming Watch
The controllers in process A are byte-for-byte identical whether the store is local or, as here, across a socket in process B.

#The State gRPC service

The service in api/v1alpha1/state.proto mirrors CoreState almost method-for-method, with a couple of additions that exist purely to save round-trips:

RPCKindMaps to
GetunaryCoreState.Get
Listserver-streamCoreState.List (streams resources)
Create / Update / Destroyunarythe CRUD writes
Watchserver-streamboth Watch & WatchKind (events streamed)
Teardownunarynative Teardowner — phase flip in one call
TeardownAndDestroyunaryserver-side wait for finalizers, then destroy — one call instead of teardown+watch+destroy

#Server & client adapters

server.NewState(core)

Wraps any local CoreState as a gRPC StateServer. Marshals resources to the wire format, maps Go errors to gRPC status codes (NotFound, FailedPrecondition for conflicts, …), and delegates Teardown/TeardownAndDestroy natively if the underlying state supports them.

client.NewAdapter(stateClient)

Implements CoreState (plus the optional Teardowner interfaces) on top of a generated StateClient. Adds resilience: exponential-backoff retry on Watch, and transparent fallbacks if the server returns Unimplemented for the newer RPCs.

#Crossing the wire: why registration matters

Recall the spec is opaque. To serialize a concrete *VM the transport consults the protobuf registry you populated in chapter 01. On the way out, protobuf.FromResource turns your resource into a wire v1alpha1.Resource (metadata + marshaled spec). On the way in, the registry looks up the type and reconstructs the concrete *VM — not a generic blob — so the receiving controllers get typed specs via TypedSpec() exactly as if it were local.

registration is mandatory for remotego
// Without this, the gRPC client can't reconstruct your type → runtime error.
func init() {
    protobuf.RegisterDynamic[vm.VMSpec](vm.VMType, &vm.VM{})
}
// Both processes must register the same types — share a package that does it in init().

#Deployment topologies

📦

Monolith

One process: inmem state, all controllers, no gRPC. This is the default and the right starting point. Tests run this way in milliseconds.

🛰️

Central store + agents

One process owns the bolt-backed state and serves gRPC. Many agent processes connect via client.NewAdapter and run their own controllers against the shared store. The store is the only stateful component.

🔌

API surface

Expose server.NewState as your system's API — CLIs, web UIs, and other services do typed CRUD & Watch over the same gRPC, with a state.Filter RBAC rule guarding access.

This is exactly how Talos & Omni scale.

Talos runs a monolith on each node; Omni runs the central-store topology with a persistent backend and many connected workers. You get to pick per-deployment, and migrate between them, without rewriting a single controller — only the line that constructs state.State changes.

#keystorage — keys for encryption at rest

If you use the encryption marshaler from chapter 02, pkg/keystorage manages the master key. It stores one symmetric master key, encrypted independently into multiple key slots — each slot a PGP-encrypted copy under a different public key. That gives you:

  • Multi-party access — several keyholders can each decrypt the same state, each with their own private key.
  • Rotation — add a new slot from an existing one (AddKeySlot) without re-encrypting the data; revoke by dropping a slot.
  • Integrity — an HMAC of the master key detects tampering.

It's serializable to a single blob (protobuf in api/key_storage), so the encrypted key store itself can live next to the encrypted data. This is the mechanism behind Talos's encrypted machine state.

#A full distributed wiring

Server side exposes the store; client side runs controllers against it. Both are a handful of lines:

server.go — the state authoritygo
core := state.WrapCore(namespaced.NewState(inmem.Build))   // or bolt-backed
grpcServer := grpc.NewServer()
v1alpha1.RegisterStateServer(grpcServer, server.NewState(core))

lis, _ := net.Listen("tcp", ":4001")
go grpcServer.Serve(lis)
agent.go — a remote workergo
conn, _ := grpc.NewClient("dns:///state.svc:4001", grpc.WithTransportCredentials(creds))

// the remote store, behind the SAME state.State interface
st := state.WrapCore(client.NewAdapter(
    v1alpha1.NewStateClient(conn),
    client.WithRetryLogger(logger),
))

rt, _ := runtime.NewRuntime(st, logger)
rt.RegisterQController(vmnet.NewVMNetController()) // identical to the monolith
rt.Run(ctx)
Mental checkpoint.

State-as-a-service means one interface from laptop test to fleet. The server/client adapters bridge it over gRPC; the protobuf registry keeps specs typed across the wire; keystorage secures it at rest. The application chapters cash all of this in: greenfield recipes (ch. 06), then the Talos source-grounded case study (ch. 07).