Skip to content

Blog

Welcome to my blog! Here you'll find articles about DevOps, AWS, cloud architecture, and more.

Recent Posts

Label Selectors: Linking Resources Without Foreign Keys

August 8, 2026 - In SQL you link tables through foreign keys. In Kubernetes the link between resources is through labels. A Deployment doesn't have a "my pods" field — it has a selector: "all pods with labels app=web." matchLabels is 8 lines of Go that implement AND logic over map[string]string, and those 8 lines connect Deployments to Pods, Services to Pods, and Pods to Nodes via nodeSelector. Why Kubernetes deliberately rejected foreign keys: 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, but labels survive recreation. Three types of label-based links (Deployment→Pods, Service→Pods, Pod→Node), how mergeLabels copies template and selector labels onto new pods, and the comparison table of foreign keys vs labels for CRUD operations. Honest about the gaps: label matching is a full scan on every reconcile (informers and caches solve this in real Kubernetes), there's 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. Honest about the gaps: eventual consistency means a pod is never Running instantly, a reconcile that compares with == flaps forever against server-added defaults, there's no backoff so a failing act spins at full speed burning CPU and logs, and "self-healing" heals nothing — the deleted pod isn't restored, a different pod with a new name, new IP, and empty disk takes its place. Wider context: the idea predates Kubernetes by two decades (CFEngine 1993, convergent operators, Promise Theory, then Puppet and Terraform), metadata.generation vs status.observedGeneration is the built-in "has the controller even seen my change?" that kubectl rollout status runs on, spec and status are physically separate endpoints with separate RBAC so a controller cannot write spec, real controllers don't poll at all (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 rewriting spec.replicas forever, and kubectl apply once 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 PUT the manifest that ties them together. Why the digest lands on the compressed bytes but the config needs the uncompressed hash (diffID) — two hashes for one layer, and they never match. Honest about the gaps: one layer for the whole rootfs (so the HEAD-check dedup never fires), empty DiffIDs that stricter validators like Harbor or cosign reject, a missing trailing slash on /blobs/uploads/ that some registries answer with 404, an ignored 202 Accepted that means "you're only getting started" and surfaces three steps later as MANIFEST_BLOB_UNKNOWN, an *os.File body that silently becomes Transfer-Encoding: chunked because Go only infers Content-Length for byte readers, and tar mtimes plus gzip levels making the same rootfs hash differently twice. Wider context: cross-repository blob mount is a real endpoint (?mount=&from=) and it's what Mounted from library/nginx means, the manifest digest is over 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, and cosign faked attachments through sha256-<hex>.sig tag 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. Honest about the gaps: layers download sequentially, the token can expire mid-pull and nothing re-fetches it, linux/amd64 is hardcoded (silent exec format error on Apple Silicon or Graviton), the platform match ignores variant, the Manifests[0] fallback can land on an attestation manifest that isn't runnable at all, and the auth server URL is hardcoded instead of discovered from WWW-Authenticate — which is why private GHCR or ECR won't work. Wider context: decode the anonymous token and its own quota is inside it (pull_limit: 100 per 6 hours, signed by the registry), Docker Hub serves blobs via a 307 to a signed CDN URL (and it only works because Go drops Authorization across hosts), half of nginx:latest's index entries are BuildKit SBOM/provenance attestations with platform: unknown/unknown, Windows images carry foreign layers 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 — which buys deduplication (identical layers stored once) and verification (hash mismatch = corruption) for free. How io.MultiWriter(tmpFile, h) streams to disk and hashes in a single pass, why that forces the temp-file → atomic os.Rename dance (you can't know the final name until you know the hash), and how manifests are stored twice — by tag and by digest — with a .content-type sidecar because the filesystem won't hold a MIME type. Honest downsides: no garbage collection (deleted manifests leave orphaned blobs), os.Rename only atomic within one filesystem, and content-addressing's dependence on a strong hash (SHA-1 fell, which is why OCI pins SHA256). Wider context: Git has done this since 2005, CAM hardware named it, Docker only adopted content-addressable image IDs in 1.10, and IPFS/Nix/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 (mismatch → delete and error), manifest push/pull by tag and digest, the enumerated OCI error format (BLOB_UNKNOWN, MANIFEST_UNKNOWN, DIGEST_INVALID) that clients parse by code not message, and why LastIndex routing survives repo names with slashes. Where it breaks: monolithic-only upload holds the whole blob in memory, no chunked PATCH, no pagination. Comparison with the wider ecosystem: /v2/ as the v1→v2 handshake that outlived Registry v1, the spec pulled out of Docker's HTTP API v2, 202 Accepted meaning "keep going", the transport-vs-image-spec split that lets the same API ship Helm charts and SBOMs (OCI artifacts / ORAS), cross-repository blob mount, and why 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. How Shepherd's Event system records Normal and Warning events from the API Server, Scheduler, NodeController, and ReplicationController — keyed by nanosecond timestamp for free chronological ordering in BoltDB. Why events are tied to objects (pod/web-0), not processes, and how that differs from stdout logs. Comparison with real Kubernetes: 1-hour TTL and aggregation with count, the separate Audit Logging mechanism for security, involvedObject and kubectl describe, migration to events.k8s.io/v1, and why Events are notifications — not 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. Why the same Pending means "waiting for the Scheduler" without a NodeName and "waiting for the Agent" with one, how eventual consistency (the same model as git) makes the 5-15 second latency a predictable sum of independent tickers rather than a bug, the one-way Running → Failed transition, and the health-check loop that flips a pod to Failed when a container stops. Comparison with real Kubernetes: phase as a deliberately coarse abstraction versus the real detail in conditions, the Pulling phase where pods hang on slow registries, initContainers, RestartPolicy and CrashLoopBackOff, terminationGracePeriodSeconds, and why readiness/liveness probes are what's missing. 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, the reconcile loop that brings Pending pods to Running, and why Shepherd's per-container IP is a simplification compared to Kubernetes's shared network namespace. Comparison with real Kubernetes: CRI instead of direct runtime calls, the pause infra container, syncLoop and PLEG, static pods, TLS bootstrap, and why 110 max-pods is 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. The two sides of the mechanism (the agent that writes "I'm alive" and the separate controller that decides "you're dead"), why a heartbeat isn't a liveness check but an agreement about a timeout, the split-brain problem when only the heartbeat is lost, and the clock-skew and last-write-wins traps. Comparison with 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. How the ServiceController rebuilds the endpoints list every 5 seconds through label selectors, why a pod needs to match labels, be Running, and have an IP to make the cut, and why a Service is really just a database record someone recomputes — not a proxy. Comparison with real Kubernetes: ClusterIP as a virtual IP that exists on no interface, kube-proxy's iptables/IPVS modes, EndpointSlice, headless services, and the eBPF (Cilium) alternative. 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" — how the ReplicationController makes it so. Creating pods from a deployment template, the loose app=web label-selector link instead of an owned list, scale up and scale down, updating ReadyReplicas, and the full async path from sheepctl to a running container. Why index naming (web-0) and overlapping selectors bite, plus where kubectl 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 constantly compare it with reality and fix the difference. The observe-compare-act pattern, why it makes the system self-healing, the 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's overkill. How Shepherd stores all cluster state in BoltDB — an embedded key-value store in a single file. Buckets as tables, namespaced keys, read/write transactions, watch channels for change notifications, and an event log. Why it's 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.