Skip to content
Blog

K3s Architecture: Agents, Datastore, and Embedded Components

Servers vs agents, the supervisor, embedded SQLite and etcd, the agent-side load balancer, and node-password registration — how K3s wires itself together.

Published on • November 14, 2026

AI Assistant

curl | sh gives you a cluster, and that ease hides a fairly opinionated design. K3s has two node roles, a datastore that changes shape with cluster size, and a registration path — websocket plus a client-side load balancer — that most operators never read about until something breaks. The architecture docs are short; this is the long version.

Server and agent: two roles, one binary

The same k3s executable runs both roles:

  • k3s server — runs the complete control plane: kube-apiserver, controller-manager, scheduler, plus a datastore K3s manages for you, plus the add-ons that make the cluster usable (CoreDNS, metrics-server, Traefik ingress, local-path provisioner). Agents register against it.
  • k3s agent — runs kubelet, the container runtime (embedded containerd), and the CNI plugin. No datastore, no apiserver, no scheduler. It has no state beyond its own node object and a locally stored password.

The asymmetry is the point: agents carry no control-plane state, so you can add twenty of them without adding a single etcd member. Cluster membership decisions live entirely on servers.

A server also runs a component K3s calls the supervisor — a small process that manages the datastore lifecycle, TLS bootstrapping, and the handler that agents use to register. It’s why the kubeconfig at /etc/rancher/k3s/k3s.yaml works immediately: the supervisor provisions the CA, the apiserver cert, and the admin client cert before the first kubectl call.

The embedded datastore

K3s bundles a controller-manager-compatible datastore behind a standard Kubernetes apiserver, so from Kubernetes’ perspective it’s just “the database.” What that database is depends on topology, per the datastore docs:

TopologyDefault datastoreNotes
Single serverEmbedded SQLiteZero dependencies; no separate DB process
3+ servers, HAEmbedded etcdRequires --cluster-init on the first server
External HAMySQL or PostgreSQLAll servers point at the same DSN

SQLite in a single server is the design decision that makes a Raspberry Pi a legitimate control plane — no etcd cluster, no JVM, no separate database to back up as its own service. The trade-off is exactly what you’d expect: one machine, one disk, one failure domain.

The switch from SQLite to embedded etcd is not a rolling in-place conversion. You choose it at cluster creation (--cluster-init), because the two are structurally different storage engines.

How agents register

Agents don’t hold a long-lived connection to one apiserver. Registration works like this:

  1. An agent is started with --server https://<server>:6443 and a node token. It seeds its endpoint list from that hostname.
  2. It opens a websocket to the server, which is how the kubelet stream (pods, logs, exec) is carried, and how registration is negotiated.
  3. The agent then syncs the kube-apiserver endpoints from the endpoints list of the kubernetes service in the default namespace — the same object every cluster uses for API discovery.
  4. From then on the agent’s own process runs a client-side load balancer in front of those endpoints.

That last step is the interesting one. The load balancer lives in the agent, not in front of the cluster: when a server goes down, the agent notices its connections failing and fails over to another server’s address on its own. There is no VIP, no cloud load balancer, no keepalived — which is precisely what you want on a factory LAN where nothing else exists to terminate TLS for you.

Consequence: individual server outages are tolerated by agents as long as at least one server is reachable and the endpoint list reflects reality. What agents don’t tolerate is all servers being down — then scheduling and status updates stop, though running pods keep running.

Node passwords

Every joining agent generates a random password, stored locally at /etc/rancher/node/password. The cluster stores a hash of it as a Secret named <node-name>.node-password.k3s in the kube-system namespace. On every registration the agent must present the same password; if it doesn’t match, re-registration is rejected.

This is how K3s gets node-level authentication without asking you to run a CA rotation ceremony on day one. It’s also a footgun worth knowing:

  • Cloned images must be re-identified. If you duplicate an SD card or a VM template, two nodes share a hostname — and the cluster expects one password for that name. Deleting the node object and the corresponding password Secret forces a clean re-registration.
  • --with-node-id appends a unique ID to the node name, so reused hostnames become distinct nodes instead of colliding. Use it when you provision from templates.
  • Creation fails open. If the validating webhook responsible for password Secrets is down, the cluster skips the check rather than blocking joins. That’s deliberate — you don’t want a broken webhook to prevent a recovery node from joining — but it means the password check is not a hard security boundary during webhook outages.

What’s embedded, and why it matters

K3s replaces or bundles the components an upstream install expects you to assemble: containerd, flannel, CoreDNS, kube-proxy, metrics-server, the local-path provisioner, and Traefik ingress. Each is stock upstream software; K3s’ contribution is a tested, version-locked combination.

Operational implications:

  • One version number. k3s --version tells you the Kubernetes version and the versions of everything bundled with it. Upgrades are a binary swap and a service restart.
  • No addon drift. You can’t end up with a CoreDNS manifest three versions behind your apiserver.
  • Overrides are flags, not YAML archaeology. --flannel-backend=none, --disable=traefik, --write-kubeconfig-mode — the bundle is a default, not a cage.

The cost is the usual one for opinionated software: if your platform team has mandated Cilium, a specific ingress controller, and an external database, you’ll spend the first hour setting flags that a kubeadm install would have expressed as manifests. Fair trade for most edge deployments, backwards for some enterprises.

Further reading