Blog¶
Welcome to my blog! Here you'll find articles about DevOps, AWS, cloud architecture, and more.
Recent Posts¶
Filesystem Layout: Where Containers Live¶
November 7, 2026
Everything in Sheep is files: container state, images, overlay layers, the IP counter. No database and no daemon holding state in memory. Restart Sheep and it reads the tree under /var/lib/sheep and knows everything that happened before.
- Four directories created on first run:
containers/with onestate.jsonper container,overlay/withupper/work/mergedper container,images/withmanifest.jsonplus a read-onlyrootfs/, andnetwork/with a singleip_counterfile. - The file lifecycle, step by step:
createmakes the directories and mounts overlay,startandstoponly rewritestate.json,rmunmounts and deletes. - Copy-on-write in practice: ten nginx containers share one read-only
rootfs, and each writes only into its ownupper/. The kernel shows the union inmerged/, which becomes the container's/. - Shepherd keeps the whole cluster (pods, services, deployments, nodes, events) in one BoltDB file. Back it up with
cp. - Against
/var/lib/docker: Docker stores each layer separately in a content-addressable store, with link files and short-name symlinks. Sheep keeps a full rootfs per image, which is simpler but has no deduplication. - What is not ideal:
os.WriteFileis not atomic, so a crash mid-write can corruptstate.json. Overlay mounts orphaned by a crash linger in/proc/mounts. Ten images on one Ubuntu base mean ten copies of it. - Fixes worth making: temp file plus
renamefor state, content-addressable layers, GC for orphaned overlays on startup, and mountingmergedonly while a container is running. - Side notes: OverlayFS needs
upperandworkon the same filesystem and an emptywork; Docker'sl/symlinks exist becausemounthas a limit on argument length;/var/libcomes from the FHS; runc keeps its ownstate.jsonper container too.
Part thirty-eight of the Sheep & Shepherd series.
Kubernetes vs Shepherd: A Minimal Orchestrator¶
November 1, 2026
Kubernetes is 30+ components and about two million lines of code. Shepherd runs a multi-node cluster in about 2,400 lines, and building it shows which parts orchestration actually needs and which parts are there for production.
- The minimum is five things: an API server as the single entry point, a persistent store, a scheduler, controllers running reconcile loops, and an agent on each node that turns "pod assigned here" into running containers.
- What got cut, and why: an etcd cluster (BoltDB is enough on one node), admission webhooks, RBAC, Secrets/ConfigMaps, HPA, CRDs, CNI/CSI,
kube-proxy, Ingress, the Watch API, leader election, and rolling updates. - Most of Kubernetes's code is not orchestration but extensibility: CRDs, webhooks, CNI and CSI exist so other people can plug in without touching the core.
- The numbers: ~2,400 lines against ~2,000,000, 5 resource types against 60+, ~15 API endpoints against 200+, ~20 MB of RAM against 2 GB+.
- The known limits: a single point of failure, no TLS, no auth, no DNS, no health checks, and downtime on every update. Ticker polling re-reads the whole state on every interval, and BoltDB pushes every write through a single writer.
- K3s is the production version of the same idea: SQLite instead of etcd, containerd bundled in, and it still passes the Kubernetes conformance tests.
Part thirty-seven of the Sheep & Shepherd series.
Agents & Dragons: Once Upon Agentic AI Workshop¶
October 26, 2026
Monday, 26 October, at the AWS UG Meetup in Warsaw: the Agents & Dragons workshop, where tool calling is a spell and the agent runs a dwarf named Thorin through a quest.
- Not a slide deck. A comic and D&D:
@toolin Python becomes a spell, and the Game Master breaks a stealth check down to the dice. - Familiar pieces (tools, MCP, agent-to-agent) land differently when you have to use them in a quest.
- Thanks to Michał Krzyżanowski for the format, and to Janusz Moroz, Luke Dorosz, and AWS for having us.
Agentic Football Cup at AWS UG Kyiv¶
October 23, 2026
Friday, 23 October, at AWS UG Kyiv: five agents on Amazon Bedrock play football. I-go-go had 65% of the ball and zero shots.
- On
nova-prothe defender took 1.2-1.9 s. Press and Intercept instead of Shoot. Four losses, 0-7. - On
nova-liteanswers came back in about 110 ms, and there were two 1-0 wins. But 77% possession and 75 Shoot commands still meant zero shots on target. - We reached the third-place match and lost it. Bronze went to SonicCrusaders, the tournament to Karpaty Lviv.
Docker vs Sheep: What a Container Runtime Actually Does¶
October 11, 2026
Docker is five components stacked on top of each other - docker CLI, dockerd, containerd, containerd-shim, runc. Sheep is one binary that does the same job, and the comparison shows exactly what each of those layers is actually buying.
- Every layer answers a specific production problem:
dockerdis the API surface,containerdis lifecycle and images, the shim is one process per container holding its file descriptors, andruncis a one-shot tool that creates namespaces and exits. - Sheep collapses all of it: the CLI calls the Manager directly, the Manager creates namespaces directly. No daemon, no gRPC, no shim.
- Component by component, what gets lost: remote control, surviving a daemon restart, interchangeable runtimes, layer deduplication, storage backend choice, port mapping,
docker build. - What both do identically at the kernel level:
clone()withCLONE_NEW*,pivot_root(), proc/sys/dev mounts, cgroups v2, bridge plus veth, iptablesMASQUERADE. That is the whole container. Everything above it is UX, reliability, and ecosystem. - The shim gap, spelled out. Kill Sheep with ten containers running and the orphans keep going, but on restart Sheep only has the PID to go on, so the exit code is gone. Docker keeps a shim alive per container precisely for that.
- The numbers: ~8 MB against ~50 MB, ~2,500 lines against 100,000+, 5 dependencies against ~200, 10 ms startup against ~2 s of daemon.
- Where the monolith bites: any bug in the manager kills the whole runtime, upgrading Sheep stops every container because there is no data plane / control plane split, and one rootfs per image means ten nginx containers hold ten copies of the filesystem.
- Side notes:
containerdwas deliberately spun out to the CNCF so Kubernetes could use it without Docker, which is how dockershim removal eventually happened;runcis Docker's ownlibcontainerextracted into a binary; the shim exists mostly to solve reparenting; and "container" is not a kernel object at all, just a user-space convention over namespaces, cgroups, and processes.
Part thirty-six of the Sheep & Shepherd series.
JSON Serialization for State Persistence¶
October 4, 2026
One format carries everything in Sheep and Shepherd: state.json per container, manifest.json per image, BoltDB values stored as JSON bytes, the whole HTTP API, and the OCI manifests on top.
- Container state goes to disk through
MarshalIndentwith two spaces, so you cancatit and read it. Image manifests use plainMarshal, because nobody reads those. - Struct tags do the mapping: snake_case on the wire, CamelCase in Go, and
time.Timebecomes RFC3339 for free. - Loading on startup is deliberately defensive. A corrupted
state.jsonis skipped rather than fatal, and a container that claims to be running with a dead PID gets rewritten to stopped. - The API streams both ways,
json.NewDecoder(r.Body)in andjson.NewEncoder(w)out, so nothing is buffered whole. - Against protobuf: implicit schema instead of a
.protofile, 30-50% more bytes, 2-5x slower. Why stdlib still wins at this scale, and why Kubernetes serves JSON but stores protobuf. - The sharp edges, spelled out.
omitemptycannot tell "zero" from "unset", which is why Kubernetes APIs are full of*int32.Unmarshalsilently swallows unknown fields and hides schema drift. There is no schema version anywhere. And a crash mid-WriteFileleaves half-written JSON that an atomic rename would have prevented. - Side notes: why Crockford left comments out of JSON, Go's deterministically sorted map keys, and
[]bytequietly becoming base64.
Part thirty-five of the Sheep & Shepherd series.
CLI Without Frameworks: Subcommand Routing¶
October 2, 2026
Sheep and sheepctl carry no cobra, no urfave/cli, nothing: just os.Args and the standard flag package, around 1170 lines across two binaries and 30+ commands.
- Routing is two switches. The first handles commands that need no Manager (
init,version,help, whereinitis the re-exec from inside the namespace), the second handles everything else. parseRunFlagswalksos.Args[2:]by hand:--flags take the next argument,-dstyle booleans take none, the first bare token becomes the image and the rest becomes the command. From the outside it feels exactly likedocker run.- Sheepctl mixes the two approaches:
flag.Parse()for the global-apiand-n, thenflag.Args()into a manual switch for subcommands. - Output goes through a small homegrown
cli.Tablethat tracks column widths as rows are added. - Against cobra: zero dependencies and about 100 lines of routing, versus roughly 3 MB of binary, shell autocompletion and generated help.
- What the manual parser leaves broken:
--typoquietly becomes the image name,--nameat the end of the line disappears becausei++runs off the slice, and--apiin one binary sits next to-apiin the other. - Side notes: why the
flagpackage treats-nand--nas the same thing, how Docker itself eventually moved to cobra, and what BusyBox does withos.Args[0].
Part thirty-four of the Sheep & Shepherd series.
Goroutines for Control Loops¶
September 26, 2026
Shepherd keeps 6 goroutines running, each on its own clock: the Scheduler every 2 seconds, the ReplicationController every 5, the Agent sending a heartbeat every 10 and checking pods every 3. They never talk to each other. All they share is the BoltDB Store and one stopCh.
- Every controller is the same three lines of structure: a
time.NewTicker, aselectover two channels, and aclose(stopCh)that every receiver sees at once. - The Store sits behind a
sync.RWMutex. Watchers get events through a non-blocking send, so a full 64-event channel is simply skipped, because the next tick catches up anyway. - Tickers instead of events, on purpose: a lost event breaks nothing, and crash recovery is just another tick.
- Three things that are wrong with it. The Agent's heartbeat and reconcile share one goroutine, so a slow container start can get the node marked NotReady. The
time.Sleepretry loop never hearsstopChand can hold shutdown for up to 27.5 seconds. A panic in one controller takes the whole process down. - Some history on the way: Watt's 1788 governor, Maxwell's "On Governors", the Greek root shared by "Kubernetes" and "cybernetics", and what exactly Go 1.23 changed about tickers.
Part thirty-three of the Sheep & Shepherd series.
Go's syscall Package: mount, clone, pivot_root¶
September 19, 2026
Everything the runtime does bottoms out in a handful of system calls, and Go reaches them through two packages: the stdlib syscall, frozen since Go 1.4, and golang.org/x/sys/unix, where anything newer lives. Mount(), Unmount() and Sethostname() come from the first. PivotRoot(), Mknod() and Mkdev() exist only in the second.
clone()is never exposed at all. The Go runtime needs control over thread creation, so the namespace flags ride along inSysProcAttr.Cloneflagsduringfork+exec, withUnshareflags: CLONE_NEWNSmaking mount propagation private so nothing leaks back to the host.- Six mounts build the container: proc, sysfs, a tmpfs
/tmp, a/devtmpfs locked down withMS_NOSUID|MS_STRICTATIME, the overlay with itslowerdir/upperdir/workdiroption string, and a self-bind withMS_BIND|MS_RECthatpivot_rootinsists on. - Then the root swap itself, with the old root parked in
.pivot_oldand detached lazily throughMNT_DETACH. - Five device nodes are built by hand from
Mkdev(major, minor)plusMknod(path, S_IFCHR|mode, dev), with/dev/fd, stdin, stdout and stderr as symlinks into/proc/self/fd. - Signals turn out to be the entire process-control API:
SIGTERM, thenSIGKILL, and signal 0 as a free liveness probe. The kernel answersESRCHwhen the PID is gone, which is how a stale "running" in state.json gets corrected on load. - Cgroups are not syscalls at all.
os.WriteFile()into/sys/fs/cgroupis plainopen/write/close. - Where it falls short: networking shells out to
ip,nsenterandiptablesinstead of netlink, so it is slower and depends on what the host has installed. A mount namespace is bound to an OS thread, so anything entering one belongs underruntime.LockOSThread()or Go may reschedule the goroutine onto a thread that no longer exists. Andsyscall.Errnowrapped in a bare%wthrows away the difference betweenEPERMandEINVAL. - Side notes:
chrootonly changes/for a process whilepivot_rootphysically swaps the root mount point, which is why runtimes pick the latter;clone()carries over 20 flags and is one of the most complex calls in Linux; and/dev/nullhas been major 1, minor 3 on every Linux machine in the world since the device registry was fixed in the 1970s.
Part thirty-two of the Sheep & Shepherd series.
Build Tags: One Codebase for Linux and macOS¶
September 11, 2026
Sheep stands on namespaces, cgroups, overlay mounts and bridge networking. None of that exists on macOS, and the thing still has to compile and run on a Mac.
runtime.GOOS == "linux"inside anifdoes not help:syscall.CLONE_NEWPIDis not a symbol on darwin at all, so the compiler fails before any branch could run.- The split happens at file level instead.
runtime_linux.gobehind//go:build linuxholds namespaces, cgroups, overlay and device nodes;runtime_stub.gobehind//go:build !linuxholds a bareexec.Command. Same trick again fornetwork_linux.goandnetwork_stub.go. - Everything above them, container.go, manager.go, image.go, carries no tag at all. That is the real payoff: the Manager coordinating the container lifecycle calls
startContainer()and never learns which platform it got. - The overlay fallback falls out of the same design. On macOS
mountOverlay()from the stub returns an error and the Manager quietly copies the rootfs instead, so the container still runs, just slower and without copy-on-write. - What this costs: demo mode on macOS exercises no real isolation, so code that works on a Mac can still die on Linux namespace quirks and only Linux CI will catch it. The networking stub is a silent no-op that never says "you are not isolated". And there is no shared
runtime.gocontract, so the stub and the Linux version can drift apart unnoticed. - Side notes:
//go:buildreplaced// +buildin Go 1.17 precisely because a stray space could break the old syntax; the filename suffix is itself a build constraint, sofoo_linux.gonever enters a macOS build no matter what is inside it; a missing blank line between the tag andpackageturns the constraint into an ordinary comment; and Docker, containerd and Kubernetes all rest on the same pile of*_linux.go/*_windows.go/*_unsupported.gofiles.
Opens the Go Systems Programming block. Part thirty-one of the Sheep & Shepherd series.
Embedded vs External DB: BoltDB vs etcd Trade-offs¶
September 5, 2026
Kubernetes stores cluster state in etcd: three to five nodes, Raft consensus, real infrastructure. Shepherd stores its state in BoltDB: one file, a single bolt.Open() call, nothing to deploy. Both are key-value stores, and the difference only shows up when something breaks.
- The comparison runs across deployment, size, transactions, replication, watch and backup, but watch is where the real gap sits. Ours is an in-memory Go channel with a 64-slot buffer and a
select/default, so a restart drops every subscription and a burst silently drops events. etcd's watch carries a revision, so a reconnecting client catches up on everything it missed. - That one difference is why the controllers poll on a ticker and treat watch as an accelerator rather than a source of truth. It reads as an honest admission of eventual consistency rather than a backward design.
- BoltDB's own limits: exactly one writer, so a long
Update()blocks every other write;bolt.Open()takes an exclusive file lock, so pointing standalone and server at the same--data-dirhangs the second one; acpof a hot file can be inconsistent, and the right answer isTx.WriteTo()inside a read transaction; the file never shrinks on its own; and a forgotten long-runningView()pins pages and bloats it. - etcd is not free either. It needs quorum, does two disk syncs per write, and can be more complex than the cluster it serves.
- Side notes: etcd itself uses bbolt as its storage engine, so every etcd node is that same BoltDB with Raft, MVCC and gRPC layered on top. The original
boltdb/boltwas frozen by Ben Johnson and the world moved to the etcd team's fork. An even-sized etcd cluster tolerates no more failures than the odd one below it. The default 2 GB backend quota flips a cluster into a read-onlyNOSPACEalarm that needs a manual disarm.ErrCompactedmeans even guaranteed watch delivery is only guaranteed within a retention window. The two meta pages at the head of a.dbfile are what make a crash mid-commit survivable. And k3s ships kine, an etcd API that speaks to SQLite, Postgres or MySQL, so Kubernetes can run with no etcd at all.
Part thirty of the Sheep & Shepherd series.
Two-Mode Architecture: Server + Agent¶
August 30, 2026
One binary, three modes. --mode server starts the control plane, --mode agent starts a worker, --mode standalone starts both in one process. Same code, different main() branch, and everything the previous parts built separately (API Server, BoltDB, scheduler, three controllers, node agent) finally meets in one entry point.
- The interesting decision is in standalone: the agent talks to the API Server over HTTP even though they share a process. A direct Go call would be faster, but it would create a second code path that nobody tests, and the bugs would live exactly there. Going through localhost makes standalone a special case of multi-node instead of a separate product.
- That choice is also why the agent needs registration retry. In the same process the API Server may simply not be listening yet, which is an honest reflection of the fact that no distributed system guarantees startup order.
- Where it hurts: standalone is a single point of failure, and it shares a machine, so a container that eats all the memory takes the control plane with it. The retry is capped at 10 attempts, roughly 27.5 seconds, and then gives up silently.
actualAddr[0]indexes a string without a length check, so--addr ""panics. And theswitchon--modehas nodefault, so--mode serevrstarts, prints nothing and exits 0, which looks exactly like a successful run. - Side notes: Kubernetes ships the same codebase as four separate binaries, k3s crams it into one with
server/agentsubcommands, and k0s packs containerd and runc inside a single static file. Docker went the other way, splitting one daemon intodockerd/containerd/runcso a daemon restart would not kill containers. The Kubernetes control plane bootstraps itself through static pods, where the kubelet starts the API Server it then registers with. Loopback HTTP is cheaper than it sounds becauselohas a 65536-byte MTU and never touches a driver. The 500ms/1s/1.5s backoff here is linear, not exponential, and it is the exact shape that produces a thundering herd once there are fifty agents. And the whole "one binary, many personalities" idea is as old asbusyboxpicking its behavior fromargv[0].
Part twenty-nine of the Sheep & Shepherd series.
Graceful Shutdown: Signals, Channels, Cleanup¶
August 22, 2026
Shepherd runs half a dozen goroutines (scheduler, three controllers, API server, agent), and when SIGTERM lands they all have to stop cleanly. The whole trick is one close(stopCh).
- A closed channel hands out the zero value an unlimited number of times, so every goroutine sitting in its own
selectwakes up at once. That is whyclosebeats a send, which wakes exactly one listener, and why the idiom is a zero-bytechan struct{}: the value carries no meaning, only the event does. - Why a plain channel is cleaner than
context.WithCancelfor a simple "everything stops" signal, and how<-sigChblocksmainuntil the signal arrives in standalone mode. - Three holes left open: nothing waits for reconcile to finish, so a controller mid-write to BoltDB can be cut off;
close(stopCh)twice panics, so exactly one owner may ever close it; andapi.Shutdown(context.Background())with no deadline hangs forever on a lingering keep-alive. - Side notes: SIGKILL and SIGSTOP are the two signals you cannot trap. Kubernetes'
terminationGracePeriodSecondsof 30s anddocker stopat 10s follow the same polite SIGTERM-wait-SIGKILL script ourclosemimics without a timer. PID 1 ignores unhandled SIGTERM entirely, which is the whole reasontiniexists.net/httphas shippedServer.Shutdownsince Go 1.8, andsignal.NotifyContextin Go 1.16 folds the signal handler into a context so the manualsigChdisappears.
Part twenty-eight of the Sheep & Shepherd series.
Async Scheduling: Why a Pod Is Created as Pending¶
August 15, 2026
sheepctl apply returns 201 Created in about two milliseconds, and the pod it just created is not running. It is not even scheduled. It is Pending, always, with no exceptions.
- That is not a bug, it is the failure model. The API Server writes the object, fires
go scheduler.SchedulePod()and forgets about it, and a reconcile loop ticking every two seconds picks up anything that goroutine dropped. - The synchronous alternative blocks an HTTP worker for seconds, times out when there are no nodes, and lies on top of that: a 200 promising Running is void the moment the agent crashes mid-launch.
pod.Spec.NodeNameis the only thing separating "the scheduler has not looked yet" from "the agent has not started yet". The full timeline fromapplyto Running takes about 3.5 seconds, of which 3 are just waiting for the agent's tick.- Known holes: the fast-path goroutine and the reconcile loop can grab the same pod at once and schedule it twice, because there is no check-and-claim on
NodeName. A pod that fits nowhere hangs Pending forever and silently, since no reason is ever written toStatus. And with no Watch API there is nowait --for=Ready, onlysleepand polling. - Side notes: the Kubernetes scheduler does not PATCH the pod, it POSTs to a separate
/bindingsubresource so RBAC can grant placement without granting spec writes. Itsassumestep marks a pod as placed in a local cache before the bind returns, otherwise it would sell the same node memory ten times over while one request is in flight.Pendingis a family of causes whose real reason lives in thePodScheduled=Falsecondition, not the phase.percentageOfNodesToScoretrades placement quality for latency because scoring 5000 nodes is too expensive. And SIG Scalability's SLO promises 99% of pods start within 5 seconds, with image pull deliberately excluded.
Part twenty-seven of the Sheep & Shepherd series.
Label Selectors: Linking Resources Without Foreign Keys¶
August 8, 2026
In SQL you link tables through foreign keys. In Kubernetes the link between resources runs through labels. A Deployment has no "my pods" field, it has a selector: all pods carrying app=web.
matchLabelsis 8 lines of Go implementing AND logic overmap[string]string, and those 8 lines connect Deployments to Pods, Services to Pods, and Pods to Nodes throughnodeSelector.- Why Kubernetes rejected foreign keys on purpose: a pod is ephemeral, born and killed dozens of times a day with a new UID each time. Any reference by ID goes stale instantly, while labels survive recreation.
- Three kinds of label-based link (Deployment to Pods, Service to Pods, Pod to Node), how
mergeLabelscopies template and selector labels onto new pods, and a comparison table of foreign keys against labels for CRUD operations. - What it costs here: label matching is a full scan on every reconcile, which informers and caches solve in real Kubernetes. There is no label validation. And overlapping selectors let two Deployments fight over the same pods.
Part twenty-six of the Sheep & Shepherd series.
Desired State vs Actual State: The Kubernetes Paradigm¶
August 1, 2026
The whole Kubernetes architecture rests on one idea: you describe what you want, and the system keeps correcting reality until it matches. Imperative says "create A, then B, then C" and has nothing to say when B crashes. Declarative says "there should be three", and the next loop notices there are two.
- Three controllers in Shepherd (replication, service, node) turn out to be the same observe-compare-act body with different nouns.
- What that model actually costs: eventual consistency means a pod is never Running instantly; a reconcile that compares with
==flaps forever against server-added defaults; there is no backoff, so a failing act spins at full speed burning CPU and logs. - And "self-healing" heals nothing. The deleted pod is not restored. A different pod with a new name, a new IP and an empty disk takes its place.
- The idea predates Kubernetes by two decades: CFEngine in 1993, convergent operators, Promise Theory, then Puppet and Terraform.
- Side notes:
metadata.generationagainststatus.observedGenerationis the built-in "has the controller even seen my change?" thatkubectl rollout statusruns on.specandstatusare physically separate endpoints with separate RBAC, so a controller cannot write spec. Real controllers do not poll at all, using informers plus watch with a resync measured in hours purely as a lost-event safety net. The most common GitOps incident is an HPA and Argo CD taking turns rewritingspec.replicasforever. Andkubectl applyonce stored your entire previous YAML as a JSON string in an annotation to make three-way merges work.
Part twenty-five of the Sheep & Shepherd series, and the first of the Distributed Systems Patterns block.
Push to Your Own Registry: Layer Creation and Manifest Upload¶
July 29, 2026
Pull is downloading. Push is building an OCI image out of a local rootfs and shipping it.
- Four steps: tar+gzip the rootfs into a layer while hashing the compressed bytes on the fly, upload it as a blob, upload the config JSON as another blob, then
PUTthe manifest that ties them together. - The digest lands on the compressed bytes, but the config needs the uncompressed hash, the diffID. Two hashes for one layer, and they never match.
- Everything that goes wrong here: one layer for the whole rootfs, so the HEAD-check dedup never fires; empty
DiffIDsthat stricter validators like Harbor orcosignreject; a missing trailing slash on/blobs/uploads/that some registries answer with404; an ignored202 Acceptedthat means "you are only getting started" and surfaces three steps later asMANIFEST_BLOB_UNKNOWN; an*os.Filebody that silently becomesTransfer-Encoding: chunkedbecause Go only infersContent-Lengthfor byte readers; and tar mtimes plus gzip levels making the same rootfs hash differently twice. - Side notes: cross-repository blob mount is a real endpoint (
?mount=&from=) and it is whatMounted from library/nginxmeans. The manifest digest covers exact bytes, so pretty-printing breaks pull-by-digest.sha256:44136fa3...is just the two characters{}, and OCI 1.1 named it so SBOMs and Helm charts can fill the mandatory config slot. Re-pushing a tag orphans the old manifest rather than overwriting it. The spec caps manifests at 4 MiB while overlay2 caps layers at 128. zstd layers exist because pull is decompression-bound. Andcosignfaked attachments throughsha256-<hex>.sigtag names until the Referrers API arrived.
Ends with a full image pushed by hand using nothing but curl, tar and shasum, and docker pull consuming it. Part twenty-four of the Sheep & Shepherd series.
Pull from registry: Auth, Manifest Lists, Multi-Arch¶
July 25, 2026
sheep pull nginx is one command hiding four steps: parse the reference, fetch a bearer token, resolve a manifest list down to one platform, then stream and extract layers. Why nginx is really library/nginx, and how the anonymous token flow splits auth.docker.io from registry-1.docker.io.
- Where this implementation breaks: layers download sequentially; the token can expire mid-pull and nothing re-fetches it;
linux/amd64is hardcoded, which turns into a silentexec format erroron Apple Silicon or Graviton; the platform match ignoresvariant; theManifests[0]fallback can land on an attestation manifest that is not runnable at all; and the auth server URL is hardcoded instead of discovered fromWWW-Authenticate, which is why private GHCR or ECR will not work. - Side notes: decode the anonymous token and its own quota is inside it,
pull_limit: 100per 6 hours, signed by the registry. Docker Hub serves blobs through a307to a signed CDN URL, and it only works because Go dropsAuthorizationacross hosts. Half ofnginx:latest's index entries are BuildKit SBOM and provenance attestations withplatform: unknown/unknown. Windows images carryforeignlayers the registry never stores. Deleting a file in a layer actually means creating a.wh.whiteout. And schema 1's JWS-signed manifests were dropped in favour of plain content-addressing.
Part twenty-three of the Sheep & Shepherd series.
Content-Addressable Storage: SHA256 as the Key¶
July 22, 2026
In Meadow a blob's file name is the SHA256 of its content. That buys deduplication (identical layers stored once) and verification (hash mismatch means corruption) for free.
io.MultiWriter(tmpFile, h)streams to disk and hashes in a single pass, which forces the temp-file then atomicos.Renamedance: you cannot know the final name until you know the hash.- Manifests are stored twice, by tag and by digest, with a
.content-typesidecar because the filesystem will not hold a MIME type. - The downsides are real: no garbage collection, so deleted manifests leave orphaned blobs;
os.Renameis only atomic within one filesystem; and content-addressing depends on a strong hash, which is why OCI pins SHA256 after SHA-1 fell. - Side notes: Git has done this since 2005, CAM hardware named it, Docker only adopted content-addressable image IDs in 1.10, and IPFS, Nix and ZFS scale the same idea to networks, packages and blocks.
Part twenty-two of the Sheep & Shepherd series.
OCI Distribution Spec: Writing Our Own Docker Registry¶
July 18, 2026
A Docker Registry is just an HTTP server with a specific set of endpoints. Meadow implements the OCI Distribution Spec in 370 lines behind a single /v2/ handler that routes by path.
- Blob upload with post-write digest verification, where a mismatch means delete and error. Manifest push and pull by tag and by digest.
- The enumerated OCI error format (
BLOB_UNKNOWN,MANIFEST_UNKNOWN,DIGEST_INVALID) that clients parse by code rather than message, and whyLastIndexrouting survives repo names with slashes. - Where it breaks: monolithic-only upload holds the whole blob in memory, there is no chunked
PATCH, and no pagination. - Side notes:
/v2/is the v1-to-v2 handshake that outlived Registry v1; the spec was pulled out of Docker's HTTP API v2;202 Acceptedmeans "keep going"; the transport-versus-image-spec split lets the same API ship Helm charts and SBOMs through OCI artifacts and ORAS; cross-repository blob mount exists; and production pins by digest, not tag.
Part twenty-one of the Sheep & Shepherd series.
Event System: Audit Trail for the Cluster¶
July 15, 2026
When something happens in the cluster, you need to know what, when and why. Shepherd's Event system records Normal and Warning events from the API Server, Scheduler, NodeController and ReplicationController, keyed by nanosecond timestamp so BoltDB orders them chronologically for free.
- Events are tied to objects (
pod/web-0), not to processes, which is exactly what separates them from stdout logs. - Against real Kubernetes: a 1-hour TTL and aggregation through
count, the separate Audit Logging mechanism for security,involvedObjectandkubectl describe, the migration toevents.k8s.io/v1, and why Events are notifications rather than Event Sourcing.
The final part of the Orchestrator series. Part twenty of the Sheep & Shepherd series.
Pod Lifecycle: From Pending to Running¶
July 11, 2026
The full asynchronous journey of a pod from sheepctl apply to Running, through the API Server, Scheduler, Agent and runtime, where no component ever waits for another.
- The same
Pendingmeans "waiting for the Scheduler" without a NodeName and "waiting for the Agent" with one. - Eventual consistency, the same model git uses, turns the 5-15 second latency into a predictable sum of independent tickers rather than a bug.
- The one-way
Running → Failedtransition, and the health-check loop that flips a pod to Failed when a container stops. - Against real Kubernetes:
phaseas a deliberately coarse abstraction versus the real detail inconditions, thePullingphase where pods hang on slow registries, initContainers, RestartPolicy and CrashLoopBackOff,terminationGracePeriodSeconds, and why readiness and liveness probes are the thing missing here.
Part nineteen of the Sheep & Shepherd series.
Node Agent: kubelet¶
July 8, 2026
The Agent is Shepherd's kubelet: registration with retry, heartbeats every 10 seconds, pod reconciliation every 3 seconds, and starting containers for assigned pods.
- Structure, resource detection from
/proc/meminfo, and the reconcile loop that brings Pending pods to Running. - Why Shepherd's per-container IP is a simplification compared to the shared network namespace Kubernetes uses.
- Against real Kubernetes: CRI instead of direct runtime calls, the pause infra container, syncLoop and PLEG, static pods, TLS bootstrap, and why the 110 max-pods limit is really about control-plane load.
Part eighteen of the Sheep & Shepherd series.
Node Health: Heartbeat and Failure Detection¶
July 4, 2026
How does the control plane know a node is alive? Through heartbeats. The agent sends one every 10 seconds, and if 30 seconds pass in silence the NodeController flips the node to NotReady and the scheduler stops placing pods on it.
- Two sides to the mechanism: the agent writing "I am alive", and a separate controller deciding "you are dead".
- A heartbeat is not a liveness check, it is an agreement about a timeout. Hence the split-brain problem when only the heartbeat is lost, plus the clock-skew and last-write-wins traps.
- Against real Kubernetes: the Lease object that replaced full status rewrites, taint-based eviction with rate limiting, and how the Φ Accrual detector and SWIM gossip go beyond a single hardcoded timeout.
Modeled on Kubernetes node heartbeats. Part seventeen of the Sheep & Shepherd series.
Service Discovery: How a Service Finds Its Pods¶
July 1, 2026
Pods come and go and their IPs change on every restart. A Service is the stable entry point that always knows where the right pods are.
- The ServiceController rebuilds the endpoints list every 5 seconds through label selectors. A pod has to match labels, be Running and have an IP to make the cut.
- Which means a Service is really a database record someone recomputes, not a proxy.
- Against real Kubernetes: ClusterIP as a virtual IP that exists on no interface, kube-proxy's iptables and IPVS modes, EndpointSlice, headless services, and the eBPF alternative in Cilium.
Modeled on Kubernetes Services. Part sixteen of the Sheep & Shepherd series.
Replication Controller: Scale Up and Scale Down¶
June 27, 2026
A Deployment says "I want 3 replicas". This is how the ReplicationController makes it so.
- Creating pods from a deployment template, the loose
app=weblabel-selector link instead of an owned list, scale up and scale down, updating ReadyReplicas, and the full async path fromsheepctlto a running container. - Where index naming (
web-0) and overlapping selectors bite, plus wherekubectl scale, the HorizontalPodAutoscaler and scale-to-zero fit in.
Modeled on Kubernetes ReplicaSet. Part fifteen of the Sheep & Shepherd series.
Reconciliation Loop: The Heart of Shepherd¶
June 24, 2026
The reconciliation loop is the heart of any orchestrator: describe the desired state, and controllers keep comparing it with reality and fixing the difference.
- The observe-compare-act pattern and why it makes the system self-healing.
- Three controllers running in parallel in Shepherd, idempotency as a survival requirement, and where level-triggered beats edge-triggered.
Modeled on Kubernetes controllers. Part fourteen of the Sheep & Shepherd series.
Scheduler: How to Pick a Node for a Pod¶
June 20, 2026 - A freshly created pod is Pending with no node assigned. How Shepherd's scheduler picks the best node in two phases - filter (drop infeasible nodes) and score (prefer the least-loaded). Resource checks, label matching, least-loaded scoring, and why a pod stays Pending even after it's scheduled. Modeled on the Kubernetes scheduler. Part thirteen of the Sheep & Shepherd series.
BoltDB Instead of etcd: Embedded State Store¶
June 17, 2026
Kubernetes runs on etcd, but for a learning project that is overkill. Shepherd keeps all cluster state in BoltDB, an embedded key-value store living in a single file.
- Buckets as tables, namespaced keys, read and write transactions, watch channels for change notifications, and an event log.
- Why it is perfect for learning, and where it falls short of etcd.
Part twelve of the Sheep & Shepherd series.
Kubernetes API Server in 300 Lines¶
June 13, 2026 - The Kubernetes API Server is the center of the entire cluster. How Shepherd implements REST API with CRUD for pods, services, deployments and nodes in ~300 lines of Go using only net/http. Asynchronous scheduling, namespaced resources, and logging middleware. Part eleven of the Sheep & Shepherd series.
A Docker CLI in 500 Lines of Go¶
June 10, 2026 - Subcommand routing, flag parsing, and formatted output - all without CLI frameworks. How Sheep implements run, ps, stop, rm and 11 more Docker-like commands in a single file using only the Go standard library. Part ten of the Sheep & Shepherd series.
Container Lifecycle: State Machine from Created to Removed¶
June 7, 2026 - A container moves through three states - created, running, stopped. How the state machine drives Create/Start/Stop/Remove, why every transition is persisted to state.json, and how signal 0 checks if a container survived a daemon restart. Part nine of the Sheep & Shepherd series.
Image Management: tar Archive to rootfs to Container¶
June 2, 2026 - A container image is just an archive with a filesystem. How a tar archive becomes a container rootfs through import and bootstrap, how OCI whiteout files delete files across layers, and why Sheep keeps a full rootfs instead of layers. Part eight of the Sheep & Shepherd series.
NAT and iptables: How a Container Sees the Internet¶
May 30, 2026 - A container's 10.20.0.x address is private - no router will route it. How ip_forward and a single MASQUERADE rule let packets reach the internet and find their way back via conntrack. Part seven of the Sheep & Shepherd series.
Bridge Networking: Giving a Container an IP Address¶
May 25, 2026 - A container in a fresh network namespace has no network at all, not even loopback. How a Linux bridge, veth pairs, and a touch of NAT give it an IP and an internet route. Part six of the Sheep & Shepherd series.
OverlayFS: Copy-on-Write Layers Like Docker¶
May 20, 2026 - How OverlayFS stacks a read-only image layer and a per-container read-write layer into one filesystem, and why copy-up lets 10 nginx containers share 100MB instead of each carrying their own. Part five of the Sheep & Shepherd series.
Cgroups v2: Limiting Memory, CPU, and PIDs¶
May 15, 2026 - Namespaces isolate but don't limit. How memory.max, cpu.max, and pids.max cap container resources through the cgroups v2 virtual filesystem - part four of the Sheep & Shepherd series.
pivot_root: How a Container Gets Its Own Filesystem¶
May 9, 2026 - How pivot_root(2) swaps a process's root directory at the mount namespace level, and why it's the proper isolation primitive instead of chroot. Part three of the Sheep & Shepherd series.
Re-Exec Pattern: Why Go and clone() Don't Get Along¶
May 2, 2026 - Go's threading model conflicts with clone(). The self re-exec pattern fixes it - part two of the Sheep & Shepherd series.
Linux Namespaces: Isolating a Process in 50 Lines of Go¶
April 28, 2026 - A container is a process with a restricted view of the system. How to isolate a process using Linux namespaces in 50 lines of Go - the first part of the Sheep & Shepherd series.
AI sovereignty: your own model on DGX Spark instead of an API¶
April 18, 2026 - How I stopped paying OpenAI and moved inference onto my own DGX Spark box with vLLM. About the hardware, the CUDA/PyTorch pain, an honest comparison with Ollama, and a small web UI to run it all.
EMM: LangGraph traces in Phoenix¶
April 11, 2026 - One init at startup covers 15 LangGraph agents. Manual spans extend coverage to voice (Gemini Live tools), avatar (Runway sessions), and Izabella chat (OpenAI/Ollama/Google + MCP tool loop).
EMM: A2A Inspector in the app and MCP for it¶
April 8, 2026 - Built-in UI plus an MCP server: inspect the Agent Card, run tasks/submit and tasks/status from the IDE without leaving the monorepo.
EMM A2A Phase 4: Auth, Rate Limiting, Observability¶
April 4, 2026 - X-API-Key, rate limiting, structured logging. A2A endpoints now protected like other APIs.
AI Reliability Engineering: certification from fwdays¶
March 31, 2026 - Completed the AI Reliability Engineering course from fwdays. Why AI system reliability belongs in the same conversation as classic SRE.
EMM A2A Phase 3: Stream task status¶
March 28, 2026 - SSE instead of polling. GET /api/a2a/tasks/{id}/stream. Theory, diagrams, capabilities.streaming.
EMM A2A Phase 2+: TaskStore and tasks/status¶
March 21, 2026 - A2A task lifecycle: submit → taskId → poll status. InMemoryTaskStore, 1h TTL. Diagrams.
Production Technology Risks: Planning for When Dependencies Fail¶
March 14, 2026 - PostgreSQL, MinIO, lakeFS: when choosing production technologies, think beyond features - what happens in 5 years?
EMM A2A Phase 2: Task Manager (list_board)¶
March 14, 2026 - Second skill - list_board. Routing by skillId, interaction diagrams, what changed.
EMM A2A Phase 1: Process Manager as A2A Server¶
March 7, 2026 - Process Manager is the first agent with an A2A interface. Protocol theory, interaction diagrams, what's implemented.
How I Became an AWS Community Builder¶
March 5, 2026 - A few years ago it was regular DevOps - deploys, scripts. Then I started thinking in clusters instead of servers. Here's how that led to AWS Community Builders.
Developing and Testing AI Agents: From LangGraph to Production¶
February 12, 2026 - How to write, test, and debug LangGraph agents? Which patterns work for StateGraph? Why are pytest fixtures critical? Development workflow from first code to production deployment.
Data Versioning for AI Agents: Real-World Experience with lakeFS¶
February 8, 2026 - When AI agents start moving your files around, version control stops being theoretical. Here's how integrating lakeFS changed my approach to data management in a LangGraph-based agent platform.
Kubernetes Deployment for AI Agents: Real-World Experience with LangGraph¶
February 3, 2026 - When AI agents move from local Docker Compose to Kubernetes, questions emerge about service discovery, caching, secrets management. How I deployed 7 microservices with minimal downtime.
Building MCP server for self-hosted Jira and Confluence.¶
November 19, 2025 - Building MCP server for self-hosted Jira and Confluence.
Izabella. Create agentic tools. Convertor from pdf to fb2 format.¶
October 30, 2025 - Izabella. Create agentic tools. Convertor from pdf to fb2 format.
Building a multi-site apartment searcher: Design patterns and architecture¶
October 16, 2025 - Building a multi-site apartment searcher: Design patterns and architecture.
Tarot AI Agent: Innovative Approach to Risk Assessment Through Artificial Intelligence¶
September 11, 2025 - Tarot AI Agent: Innovative Approach to Risk Assessment Through Artificial Intelligence.
Using AWS ECR as a Universal OCI Repository¶
July 10, 2025 - Using AWS ECR as a universal OCI repository for storing various types of artifacts.
Cert Manager in Kubernetes¶
June 3, 2025 - Setting up and using Cert Manager for automatic management of SSL certificates in Kubernetes.
New Architecture for Isabella - C4 Diagrams¶
May 14, 2025 - Development of a new system architecture using C4 diagrams.
New Architecture for Isabella - Structure¶
May 14, 2025 - Detailed structure analysis for the new Isabella architecture.
New Architecture for Isabella¶
May 10, 2025 - Overview of the new architecture design for Isabella project.
Waste Resources - Financial Optimization in the Cloud¶
April 14, 2025 - Analysis and optimization of cloud resource costs.
Redis Backup on AWS S3¶
March 19, 2025 - Automation of Redis backup to AWS S3.
RDS Import with Terraform¶
February 17, 2025 - Importing existing RDS instances into Terraform.
RDS Import with Terraform (EN)¶
February 17, 2025 - Importing existing RDS instances into Terraform (English version).
AWS Lambda Cost Optimization¶
February 26, 2025 - Strategies for optimizing AWS Lambda costs.
Kubernetes Onboarding with Flux¶
March 10, 2025 - Automating Kubernetes onboarding using Flux.
RDS Migration Cases¶
February 9, 2025 - Various scenarios for migrating databases to AWS RDS.
SPA Deployment on S3 with CloudFront¶
February 12, 2025 - Deploying Single Page Application on AWS S3 with CloudFront.
Karpenter Properties¶
December 8, 2024 - Properties and configuration of Karpenter for Kubernetes.
AI Stable Diffusion¶
February 8, 2025 - Using Stable Diffusion for image generation.
ARM vs AMD¶
December 5, 2024 - Comparison of ARM and AMD architectures for cloud solutions.