A small reimplementation of kubelet's static-pod path, in Go.
tinylet watches a directory of YAML pod specs and reconciles them against a local containerd via the Container Runtime Interface (CRI). About 400 lines of logic. No apiserver, no scheduler, no etcd.
I wrote this to figure out what kubelet actually does. Don't run it in production.
make run
make logsDrop a YAML into manifests/:
name: hello
namespace: default
containers:
- name: web
image: docker.io/library/nginx:1.27On the next 2-second tick, tinylet creates the pod sandbox and starts the container. Delete the file and the pod is torn down.
Inspect what's running with crictl:
make crictl ARGS="pods"
make crictl ARGS="ps"- Reads pod YAMLs from
manifests/ - Talks CRI (gRPC over a Unix socket) to a local containerd
- Pulls images, creates pod sandboxes, creates and starts containers
- Tears down pods whose YAMLs were deleted
- Reconciles every 2 seconds, idempotently
Most of what a real kubelet does:
- No apiserver, no node registration, no leases
- No scheduling
- No volumes, probes, resources, env vars, secrets, configmaps
- No networking beyond whatever containerd + CNI bridge gives you
- No log collection, no metrics
tinylet labels every sandbox it creates with tinylet=true and ignores
everything else, so it won't touch pods owned by a real kubelet on the
same containerd.
The Pod spec is deliberately small:
type Pod struct {
Name string
Namespace string
Containers []Container
}
type Container struct {
Name string
Image string
Command []string
Args []string
} manifests/*.yaml containerd
| ^
v |
[manifest.LoadFromDir] |
| |
v |
[reconcile.Run] --diff--> [cri.Client] --gRPC--+
^
|
/run/containerd/containerd.sock
Three packages, each around 100 lines:
internal/manifestreads YAML from disk intoPodstructs.internal/criwraps containerd's two CRI gRPC services.RuntimeServicehandles pods and containers,ImageServicehandles image pulls.internal/reconcileis the loop. Each tick it loads desired state from disk, lists actual state from containerd, and converges the two by callingEnsurePodandStopPodon the CRI client.
Same pattern as real kubelet, but driven by a directory scan instead of an apiserver watch.
make run brings up two containers via docker-compose:
tl-containerd: privileged containerd with a CNI bridge plugintl-kubelet: the tinylet binary, sharing containerd's socket via a named volume and mounting./manifestsread-only
Tested on macOS via Docker Desktop. Should work the same on Linux.
If you have containerd installed locally:
go build -o bin/tinylet ./cmd/tinylet
sudo ./bin/tinylet \
--socket=/run/containerd/containerd.sock \
--manifests=/etc/tinylet/manifests \
--interval=2sYou'll usually need root for the containerd socket.
go test ./...The CRI client is tested against an in-process fake gRPC server, so the tests run with no Docker and no real containerd.
cmd/tinylet/ # main, flag parsing, signal handling
internal/manifest/ # YAML loader + Pod types
internal/cri/ # CRI gRPC client wrapper
internal/reconcile/ # the loop
docker/ # containerd + tinylet images
manifests/ # example pod YAMLs
A reasonable order:
internal/manifest/types.gofor the Pod struct.internal/cri/client.gofor how kubelet talks to containerd. The four methods (Dial,ListPods,EnsurePod,StopPod) are the entire runtime surface kubelet uses for static pods.internal/reconcile/loop.gofor the diff-and-converge body.cmd/tinylet/main.gofor wiring.