Перейти до змісту

Блог

Ласкаво просимо до мого блогу! Тут ви знайдете статті про DevOps, AWS, хмарну архітектуру та багато іншого.

Останні пости

Docker vs Sheep: що насправді робить контейнерний рантайм

11 жовтня 2026

Docker - це п'ять компонентів один на одному: docker CLI, dockerd, containerd, containerd-shim, runc. Sheep робить ту саму роботу одним бінарником, і порівняння показує, за що саме платиться кожним із тих шарів.

  • Кожен шар - відповідь на конкретний production-біль: dockerd тримає API назовні, containerd відповідає за lifecycle і образи, shim - це один процес на контейнер, який тримає його файлові дескриптори, а runc взагалі не демон, а one-shot tool, що створює namespace'и і виходить.
  • Sheep складає все це в одне: CLI напряму викликає Manager, Manager напряму створює namespace'и. Ні демона, ні gRPC, ні shim.
  • Компонент за компонентом, що втрачено: віддалене управління, переживання рестарту демона, взаємозамінність рантаймів, дедуплікація шарів, вибір storage-бекенду, port mapping, docker build.
  • Що обидва роблять однаково на рівні ядра: clone() з CLONE_NEW*, pivot_root(), монтування proc/sys/dev, cgroups v2, bridge плюс veth, iptables MASQUERADE. Це і є контейнер. Усе решта - UX, надійність, екосистема.
  • Діра на місці shim, конкретно. Вбий Sheep із десятьма запущеними контейнерами - сироти продовжать працювати, але після старту Sheep має лише PID, і exit code втрачено. Docker тримає shim на кожен контейнер саме заради цього.
  • Числа: ~8 МБ проти ~50 МБ, ~2 500 рядків проти 100 000+, 5 залежностей проти ~200, 10 мс старту проти ~2 с на демон.
  • Де кусає монолітність: будь-який баг у менеджері валить увесь рантайм, оновлення Sheep зупиняє всі контейнери, бо немає поділу на data plane і control plane, а один rootfs на образ означає, що десять контейнерів з nginx тримають десять копій файлової системи.
  • Побіжно: containerd навмисно винесли в CNCF, щоб Kubernetes міг брати його без Docker - звідти й дійшло до dockershim removal; runc - це Docker-івський libcontainer, виділений в окремий бінарник; shim існує переважно заради reparenting; а "контейнер" - не обʼєкт ядра взагалі, лише домовленість у user space над namespaces, cgroups і процесами.

Тридцять шоста частина серії Sheep & Shepherd.

JSON serialization для state persistence

4 жовтня 2026

Один формат тримає в Sheep і Shepherd усе: state.json на кожен контейнер, manifest.json на кожен образ, значення в BoltDB як JSON-байти, весь HTTP API і OCI-маніфести згори.

  • Стан контейнера лягає на диск через MarshalIndent із двома пробілами, щоб його можна було просто зробити cat і прочитати. Маніфести образів ідуть звичайним Marshal, бо їх однаково ніхто не читає.
  • Мапінг роблять struct tags: snake_case у JSON, CamelCase у Go, а time.Time сам перетворюється на RFC3339.
  • Завантаження на старті навмисно параноїдальне. Зіпсований state.json пропускається, а не валить процес, а контейнер, який вважає себе running із мертвим PID, переписується в stopped.
  • API стрімить в обидва боки, json.NewDecoder(r.Body) на вхід і json.NewEncoder(w) на вихід, тож нічого не буферизується цілком.
  • Проти protobuf: неявна схема замість .proto-файлу, на 30-50% більше байтів, у 2-5 разів повільніше. Чому на цьому масштабі stdlib усе одно виграє і чому Kubernetes віддає JSON назовні, а зберігає protobuf.
  • Гострі кути названо прямо. omitempty не відрізняє "нуль" від "не задано", і саме тому в Kubernetes API стільки *int32. Unmarshal мовчки ковтає невідомі поля і ховає розходження схем. Версії схеми немає ніде. А падіння посеред WriteFile лишає недописаний JSON, якого не було б з атомарним rename.
  • Побіжно: чому Крокфорд свідомо не дав JSON коментарів, чому Go детерміновано сортує ключі мапи і як []byte тихо стає base64.

Тридцять п'ята частина серії Sheep & Shepherd.

CLI без фреймворків: subcommand routing

2 жовтня 2026

У sheep і sheepctl немає ні cobra, ні urfave/cli, взагалі нічого: тільки os.Args і стандартний пакет flag, близько 1170 рядків на два бінарники і 30+ команд.

  • Маршрутизація - це два switch'і. Перший бере команди, яким не потрібен Manager (init, version, help, де init це re-exec уже зсередини namespace'у), другий - усе решта.
  • parseRunFlags проходить os.Args[2:] руками: прапорці з -- забирають наступний аргумент, булеві на кшталт -d не забирають нічого, перший голий токен стає образом, решта - командою. Ззовні це відчувається точно як docker run.
  • Sheepctl змішує обидва підходи: flag.Parse() для глобальних -api і -n, далі flag.Args() у ручний switch по підкомандах.
  • Вивід іде через невеликий власний cli.Table, який рахує ширину колонок прямо під час додавання рядків.
  • Проти cobra: нуль залежностей і близько 100 рядків маршрутизації проти приблизно 3 МБ бінарника, автодоповнення в шелі та згенерованого help.
  • Що ручний парсер лишає зламаним: --typo тихо стає ім'ям образу, --name у кінці рядка просто зникає, бо i++ вилітає за межі слайсу, а --api в одному бінарнику живе поруч із -api в іншому.
  • Побіжно: чому пакет flag вважає -n і --n одним і тим самим, як сам Docker зрештою переїхав на cobra і що BusyBox робить з os.Args[0].

Тридцять четверта частина серії Sheep & Shepherd.

Goroutines для control loops

26 вересня 2026

Shepherd тримає 6 горутин, і кожна тікає у своєму ритмі: Scheduler кожні 2 секунди, ReplicationController кожні 5, Agent шле heartbeat кожні 10 і звіряє поди кожні 3. Між собою вони не говорять. Спільні в них лише BoltDB Store і один stopCh.

  • Кожен контролер має однакову будову: time.NewTicker, select на два канали і close(stopCh), який бачать усі отримувачі одразу.
  • Store закритий sync.RWMutex, а watchers отримують події через non-blocking send, тож повний канал на 64 події просто пропускається - наступний тік однаково все підхопить.
  • Тікери замість подій обрано свідомо: загублена подія нічого не ламає, а crash recovery це просто черговий тік.
  • Три речі, які тут зроблено погано. Heartbeat і reconcile в Agent живуть в одній горутині, тож довгий запуск контейнера може зробити ноду NotReady. Retry через time.Sleep не чує stopCh і тягне shutdown до 27,5 секунд. Паніка в одному контролері кладе весь процес.
  • Трохи історії по дорозі: регулятор Ватта 1788 року, Максвелл і "On Governors", спільний грецький корінь у "Kubernetes" і "кібернетиці", а ще що саме Go 1.23 змінив у тікерах.

Тридцять третя частина серії Sheep & Shepherd.

syscall пакет Go: mount, clone, pivot_root

19 вересня 2026

Усе, що рантайм реально робить, зрештою впирається в жменю системних викликів, і Go дістає їх через два пакети: stdlib-івський syscall, заморожений ще з Go 1.4, і golang.org/x/sys/unix, куди їде все нове. Mount(), Unmount() і Sethostname() беруться з першого. PivotRoot(), Mknod() і Mkdev() існують лише в другому.

  • Сам clone() назовні не віддається взагалі. Рантайму потрібен контроль над створенням тредів, тому прапорці namespace'ів прокидаються через SysProcAttr.Cloneflags під час fork+exec, а Unshareflags: CLONE_NEWNS робить mount propagation приватною, щоб зміни не протікали назад на хост.
  • Контейнер будують шість mount'ів: proc, sysfs, tmpfs на /tmp, tmpfs на /dev під MS_NOSUID|MS_STRICTATIME, overlay зі своїм рядком lowerdir/upperdir/workdir і bind на себе з MS_BIND|MS_REC, якого вимагає pivot_root.
  • Далі атомарна зміна кореня, зі старим коренем у .pivot_old і лінивим відмонтуванням через MNT_DETACH.
  • П'ять device nodes створюються руками через Mkdev(major, minor) і Mknod(path, S_IFCHR|mode, dev), а /dev/fd, stdin, stdout і stderr це симлінки в /proc/self/fd.
  • Сигнали виявляються всім API управління процесами: SIGTERM, потім SIGKILL, і нульовий сигнал як безкоштовна перевірка живості. Ядро відповідає ESRCH, якщо PID уже немає, і саме так протухле "running" у state.json виправляється при завантаженні.
  • Cgroups взагалі не syscalls: os.WriteFile() у /sys/fs/cgroup це просто open/write/close.
  • Де воно недотягує: мережа це exec.Command з ip, nsenter та iptables замість netlink, тобто повільніше й залежно від того, що встановлено на хості. Mount namespace прив'язаний до OS-треда, тому код, який у нього заходить, має сидіти під runtime.LockOSThread(), інакше Go перекине горутину на тред, якого там уже немає. А syscall.Errno, загорнутий голим %w, стирає різницю між EPERM і EINVAL.
  • Побіжно: chroot лише міняє / для процесу, тоді як pivot_root фізично переставляє кореневу точку монтування, і саме тому рантайми обирають друге; clone() має понад 20 прапорців і є одним із найскладніших викликів у Linux; а /dev/null має major 1, minor 3 на кожній Linux-машині світу відтоді, як реєстр пристроїв зафіксували ще в 1970-х.

Тридцять друга частина серії Sheep & Shepherd.

Build Tags: один код для Linux і macOS

11 вересня 2026

Sheep стоїть на namespaces, cgroups, overlay mounts і bridge networking. Нічого з цього на macOS не існує, а зібратися й запуститися на Mac воно все одно мусить.

  • runtime.GOOS == "linux" всередині if тут не рятує: syscall.CLONE_NEWPID на darwin просто немає як символу, тому компіляція падає ще до того, як якась гілка виконається.
  • Ділити доводиться на рівні файлів. runtime_linux.go за //go:build linux тримає namespaces, cgroups, overlay і device nodes, runtime_stub.go за //go:build !linux тримає голий exec.Command. Той самий трюк ще раз для network_linux.go і network_stub.go.
  • Усе, що над ними (container.go, manager.go, image.go), не має тегу взагалі, і саме в цьому виграш: Manager, який координує життєвий цикл контейнера, викликає startContainer() і ніколи не дізнається, яка під ним платформа.
  • Fallback на overlay виростає з того ж дизайну. На macOS mountOverlay() зі stub повертає помилку, і Manager тихо копіює rootfs, тож контейнер працює, лише повільніше і без copy-on-write.
  • Чим за це платимо: demo mode на macOS не перевіряє жодної реальної ізоляції, тому код, який працює на Mac, може вмерти на Linux через особливості namespace'ів, і ловить це лише CI на Linux. Мережевий stub це мовчазний no-op, який ніколи не скаже "ти без ізоляції". А спільного контракту в runtime.go немає, тому stub і linux-версія можуть непомітно розійтися.
  • Побіжно: //go:build замінив // +build у Go 1.17 саме тому, що зайвий пробіл ламав старий синтаксис; суфікс в імені файлу сам по собі є build constraint, тому foo_linux.go ніколи не потрапить у macOS-збірку, хоч би що було всередині; відсутній порожній рядок між тегом і package перетворює constraint на звичайний коментар; а Docker, containerd і Kubernetes тримаються рівно на тій самій купі *_linux.go / *_windows.go / *_unsupported.go.

Відкриває блок Go Systems Programming. Тридцять перша частина серії Sheep & Shepherd.

Embedded vs External DB: BoltDB vs etcd trade-offs

5 вересня 2026

Kubernetes тримає стан кластера в etcd: 3-5 нод, Raft-консенсус, справжня інфраструктура. Shepherd тримає свій у BoltDB: один файл, один виклик bolt.Open() і нуль інфраструктури. Обидва це key-value, і різниця виринає лише тоді, коли щось ламається.

  • Порівняння йде по розгортанню, розміру, транзакціях, реплікації, watch і бекапу, але справжня прірва саме у watch. У нас це Go-канал у пам'яті з буфером на 64 і select/default, тому рестарт знищує всі підписки, а сплеск подій тихо їх дропає. В etcd watch несе revision, тому клієнт після реконекту догоняє все пропущене.
  • Саме ця різниця й пояснює, чому контролери опитують стан по тікеру, а watch лишається прискорювачем, а не джерелом істини. Це не "відсталий" дизайн, а чесно визнана eventual consistency.
  • Власні межі BoltDB: рівно один writer, тому довгий Update() блокує всі інші записи; bolt.Open() бере ексклюзивний file lock, тому standalone і server на одному --data-dir підвішують другий процес; cp гарячого файлу може дати неконсистентну копію, а правильно робити Tx.WriteTo() всередині read-транзакції; файл не зменшується сам; забутий довгий View() тримає сторінки і роздуває базу.
  • etcd теж не безкоштовний: потребує кворуму, робить два disk sync на кожен запис і буває складнішим за сам кластер.
  • Побіжно: etcd сам використовує bbolt як локальний storage engine, тобто кожна нода etcd всередині це той самий BoltDB з Raft, MVCC і gRPC згори. Оригінальний boltdb/bolt Бен Джонсон заморозив, і світ перейшов на форк команди etcd. Парний розмір кластера etcd витримує не більше відмов, ніж непарний на одиницю менший. Дефолтна квота 2 GB переводить кластер у read-only alarm NOSPACE, який треба знімати руками. ErrCompacted означає, що навіть гарантована доставка watch гарантована лише в межах вікна retention. Дві мета-сторінки на початку .db файлу роблять крах посеред комміту безпечним. А k3s возить із собою kine, etcd API поверх SQLite, Postgres або MySQL, тому Kubernetes можна запустити взагалі без etcd.

Тридцята частина серії Sheep & Shepherd.

Two-Mode Architecture: server + agent

30 серпня 2026

Один бінарник, три режими. --mode server піднімає control plane, --mode agent воркер, --mode standalone обидва в одному процесі. Той самий код, різна гілка в main(), і все, що попередні частини будували окремо (API Server, BoltDB, шедулер, три контролери, агент ноди), нарешті сходиться в одній точці входу.

  • Найцікавіше рішення у standalone: агент ходить до API Server через HTTP, хоча вони в одному процесі. Прямий виклик Go-функції був би швидшим, але створив би другий code path, який ніхто не тестує, і баги жили б саме там. Похід через localhost робить standalone окремим випадком multi-node, а не окремим продуктом.
  • Саме через це агенту потрібен retry на реєстрації. В одному процесі API Server може просто ще не слухати порт, і це чесне відображення того, що в розподіленій системі ніхто не гарантує порядок старту.
  • Де болить: standalone це single point of failure і спільна машина, тому контейнер, який з'їв усю пам'ять, забирає з собою control plane. Retry обмежений 10 спробами, приблизно 27,5 секунди, і здається мовчки. actualAddr[0] індексує рядок без перевірки довжини, тому --addr "" панікує. А switch по --mode не має default, тому --mode serevr стартує, нічого не пише і виходить з кодом 0, тобто друкарська помилка виглядає точнісінько як успішний запуск.
  • Побіжно: Kubernetes віддає ту саму кодову базу чотирма окремими бінарниками, k3s запихає її в один із сабкомандами server/agent, а k0s кладе всередину єдиного статичного файлу ще й containerd з runc. Docker пішов у протилежний бік, розібравши один демон на dockerd/containerd/runc, щоб рестарт демона не вбивав контейнери. Control plane Kubernetes бутстрапить сам себе через static pods, де kubelet піднімає той API Server, у якому потім реєструється. HTTP через loopback дешевший, ніж звучить, бо в lo MTU 65536 байт і трафік не торкається драйвера. Backoff 500ms/1s/1.5s тут лінійний, а не експоненційний, і саме така форма дає thundering herd, щойно агентів стане п'ятдесят. А ідея "один бінарник, багато особистостей" стара, як busybox, що обирає поведінку за argv[0].

Двадцять дев'ята частина серії Sheep & Shepherd.

Graceful Shutdown: signals, channels, cleanup

22 серпня 2026

Shepherd запускає з півдесятка goroutines (scheduler, три контролери, API server, agent), і коли прилітає SIGTERM, усі мають зупинитися акуратно. Увесь фокус це один close(stopCh).

  • Закритий канал віддає zero value необмежену кількість разів, тому кожна goroutine у своєму select прокидається одночасно. Ось чому close кращий за send, який будить рівно одного слухача, і чому ідіома це chan struct{} нульового розміру: значення не несе сенсу, важлива лише подія.
  • Чому звичайний канал тут чистіший за context.WithCancel для простого сигналу "все зупиняється" і як <-sigCh блокує main до сигналу в standalone-режимі.
  • Три відкриті діри: ніхто не чекає, поки reconcile завершиться, тому контролер посеред запису в BoltDB можна обірвати; close(stopCh) двічі панікує, тому закривати має рівно один власник; а api.Shutdown(context.Background()) без deadline зависає назавжди на живому keep-alive.
  • Побіжно: SIGKILL і SIGSTOP це два сигнали, які не перехопити. terminationGracePeriodSeconds у Kubernetes на 30 секунд і docker stop на 10 це той самий ввічливий сценарій SIGTERM-очікування-SIGKILL, який наш close імітує без таймера. PID 1 повністю ігнорує необроблений SIGTERM, і саме заради цього існує tini. net/http має Server.Shutdown ще з Go 1.8, а signal.NotifyContext у Go 1.16 згортає обробник сигналу в context, тому ручний sigCh зникає.

Двадцять восьма частина серії Sheep & Shepherd.

Async Scheduling: чому Pod створюється як Pending

15 серпня 2026

sheepctl apply повертає 201 Created за дві мілісекунди, і под, який щойно створено, не запущений. Він навіть не запланований. Він Pending, завжди, без винятків.

  • Це не баг, а вся модель відмов. API Server записує об'єкт, кидає go scheduler.SchedulePod() і забуває про нього, а reconcile-loop, що тікає щодві секунди, підбирає все, що ця goroutine загубила.
  • Синхронна альтернатива блокує HTTP-worker на секунди, віддає timeout, коли нод немає, і до того ж бреше: 200, який обіцяє Running, знецінюється тієї ж миті, коли agent падає посеред запуску.
  • pod.Spec.NodeName це єдине, що відрізняє "scheduler ще не дивився" від "agent ще не запустив". Повний таймлайн від apply до Running займає близько 3,5 секунди, з яких 3 це просто очікування тіку агента.
  • Відомі діри: fast-path goroutine і reconcile-loop можуть схопити один под одночасно і призначити його двічі, бо немає перевірки-захоплення на NodeName. Под, який нікуди не вміщується, висить Pending вічно і мовчки, бо причина ніколи не пишеться в Status. А без Watch API немає wait --for=Ready, тільки sleep і polling.
  • Побіжно: шедулер Kubernetes навіть не робить PATCH на под, він POSTить в окремий підресурс /binding, щоб RBAC міг дозволити розміщення, не дозволяючи запис у spec. Його крок assume позначає под як розміщений у локальному кеші ще до того, як bind повернеться, інакше він продав би ту саму пам'ять ноди десять разів, поки летить один запит. Pending це родина причин, справжня з яких живе в умові PodScheduled=False, а не у фазі. percentageOfNodesToScore міняє якість розміщення на латентність, бо оцінювати 5000 нод задорого. А SLO від SIG Scalability обіцяє старт 99% подів за 5 секунд, зі свідомо виключеним pull образу.

Двадцять сьома частина серії Sheep & Shepherd.

Label Selectors: як зв'язати ресурси без foreign keys

8 серпня 2026

У SQL ти зв'язуєш таблиці через foreign keys. У Kubernetes зв'язок між ресурсами йде через labels. Deployment не має поля "мої поди", він має selector: усі поди з labels app=web.

  • matchLabels це 8 рядків Go, які реалізують AND-логіку над map[string]string, і ці 8 рядків зв'язують Deployments із Pods, Services із Pods і Pods із Nodes через nodeSelector.
  • Чому Kubernetes свідомо відмовився від foreign keys: под ефемерний, народжується і вмирає десятки разів на день із новим UID. Посилання за ID протухне миттєво, а labels переживають перестворення.
  • Три типи зв'язків через labels (Deployment до Pods, Service до Pods, Pod до Node), як mergeLabels копіює labels із template і selector на нові поди, і порівняльна таблиця foreign keys проти labels для CRUD-операцій.
  • Чим це коштує тут: label matching це повний перебір на кожному reconcile, що в справжньому Kubernetes вирішують informers і кеші. Валідації labels немає. А перетин selectors дозволяє двом Deployments воювати за одні поди.

Двадцять шоста частина серії Sheep & Shepherd.

Desired State vs Actual State: парадигма Kubernetes

1 серпня 2026

Вся архітектура Kubernetes тримається на одній ідеї: ти описуєш, чого хочеш, а система нескінченно виправляє реальність, поки та не збіжиться. Imperative каже "створи A, потім B, потім C" і не має що відповісти, коли B впаде. Declarative каже "має бути три", і наступний цикл просто бачить, що є два.

  • Три контролери в Shepherd (replication, service, node) виявляються одним і тим самим тілом observe-compare-act, лише з різними іменниками.
  • Чим ця модель платить: eventual consistency означає, що под ніколи не стає Running миттєво; reconcile із порівнянням через == вічно флапає проти дефолтів, які додав сервер; backoff'у немає, тому act, що постійно падає, крутиться на повну, спалюючи CPU і логи.
  • А "самовідновлення" нічого не відновлює. Видалений под не повертається. Його місце займає інший, з новим ім'ям, новим IP і порожнім диском.
  • Ідея старша за Kubernetes на два десятиліття: CFEngine 1993, convergent operators, Promise Theory, а вже потім Puppet і Terraform.
  • Побіжно: metadata.generation проти status.observedGeneration це вбудована відповідь на питання "контролер узагалі бачив мою зміну?", і саме на ній тримається kubectl rollout status. spec і status це фізично різні ендпоінти з різними RBAC-правами, тому контролер не може записати spec. Справжні контролери взагалі не опитують API, у них informer плюс watch, а resync міряється годинами і потрібен лише як страховка від загубленої події. Найпоширеніший інцидент у GitOps це HPA і Argo CD, які по черзі переписують spec.replicas до нескінченності. А kubectl apply колись зберігав увесь твій попередній YAML цілим JSON-рядком в анотації, щоб three-way merge взагалі працював.

Двадцять п'ята частина серії Sheep & Shepherd і перша стаття блоку Distributed Systems Patterns.

Push в свій реєстр: layer creation і manifest upload

29 липня 2026

Pull це завантаження. Push це збирання OCI-образу з локального rootfs і відправка його в реєстр.

  • Чотири кроки: запакувати rootfs у tar.gz, рахуючи SHA256 стиснутих байтів на льоту, залити його як blob, залити config JSON як ще один blob, а потім PUT маніфест, який їх зв'язує.
  • Digest рахується по стиснутих байтах, а config'у потрібен хеш нестиснутого tar, тобто diffID. Два хеші на один шар, і вони ніколи не збігаються.
  • Усе, що тут іде не так: один шар на весь rootfs, тому HEAD-дедуплікація не спрацьовує ніколи; порожні DiffIDs, які відкидають строгіші валідатори типу Harbor чи cosign; відсутній слеш у /blobs/uploads/, на який частина реєстрів відповідає 404; проігнорований 202 Accepted, що означає "ти лише почав" і вилазить через три кроки як MANIFEST_BLOB_UNKNOWN; body у вигляді *os.File, який тихо стає Transfer-Encoding: chunked, бо Go виводить Content-Length лише для byte-readerів; і mtime у tar плюс рівень gzip, через які той самий rootfs хешується двічі по-різному.
  • Побіжно: cross-repository blob mount це справжній ендпоінт (?mount=&from=), і саме він стоїть за Mounted from library/nginx. Digest маніфесту рахується по конкретних байтах, тому вивід з відступами ламає pull за digest. sha256:44136fa3... це просто два символи {}, і OCI 1.1 дав цьому ім'я, щоб SBOM і Helm-чарти могли заповнити обов'язкове поле config. Повторний push тега не перезаписує старий маніфест, а осиротює його. Спека обмежує маніфест 4 MiB, а overlay2 128 шарами. zstd-шари існують тому, що pull впирається у розпакування. А cosign імітував приєднання підписів через імена тегів sha256-<hex>.sig, поки не з'явився Referrers API.

У фіналі повний образ, запушений руками лише через curl, tar і shasum, який потім спокійно тягне docker pull. Двадцять четверта частина серії Sheep & Shepherd.

Pull з registry: auth, manifest lists, multi-arch

25 липня 2026

sheep pull nginx це одна команда, за якою чотири кроки: розпарсити reference, отримати bearer-токен, звести manifest list до однієї платформи, а потім стягнути й розпакувати шари. Чому nginx насправді library/nginx і як анонімний auth розділяє auth.docker.io і registry-1.docker.io.

  • Де ця реалізація ламається: шари тягнуться послідовно; токен може протухнути посеред pull'у і ніхто його не перезапитує; linux/amd64 захардкоджений, що дає тихий exec format error на Apple Silicon чи Graviton; у порівнянні платформи проігноровано variant; fallback Manifests[0] може вибрати attestation-маніфест, який запустити неможливо; а адреса auth-сервера захардкоджена замість пошуку через WWW-Authenticate, тому приватний GHCR або ECR не працюватиме.
  • Побіжно: розкодуй анонімний токен і побачиш власну квоту всередині нього, pull_limit: 100 на 6 годин, підписані реєстром. Docker Hub віддає blob'и через 307 на підписаний CDN-URL, і працює це лише тому, що Go викидає Authorization при переході на інший хост. Половина записів в індексі nginx:latest це SBOM і provenance від BuildKit із platform: unknown/unknown. Windows-образи несуть foreign-шари, яких реєстр взагалі не зберігає. Видалення файлу в шарі це насправді створення whiteout-файлу .wh.. А підписані через JWS маніфести schema 1 викинули на користь звичайного content-addressing.

Двадцять третя частина серії Sheep & Shepherd.

Content-Addressable Storage: SHA256 як ключ

22 липня 2026

У Meadow ім'я файлу blob'у це і є SHA256 його вмісту. Саме це безкоштовно дає дедуплікацію (однакові layer'и зберігаються один раз) і верифікацію (хеш не збігся означає пошкоджені дані).

  • io.MultiWriter(tmpFile, h) за один прохід одночасно пише на диск і рахує хеш, і це змушує до танцю temp-файл та atomic os.Rename: фінальне ім'я невідоме, поки невідомий хеш.
  • Маніфест зберігається двічі, за тегом і за digest, із sidecar-файлом .content-type, бо ФС не тримає MIME-тип.
  • Мінуси цілком реальні: немає garbage collection, тому видалений маніфест лишає осиротілі blob'и; os.Rename атомарний лише в межах однієї ФС; а content-addressing залежить від міцного хеша, і саме тому після злому SHA-1 OCI прив'язаний до SHA256.
  • Побіжно: Git робить це з 2005 року, назву дало залізо CAM, Docker перейшов на content-addressable ID образів лише у 1.10, а IPFS, Nix і ZFS масштабують ту саму ідею на мережі, пакети й блоки.

Двадцять друга частина серії Sheep & Shepherd.

OCI Distribution Spec: пишемо свій Docker Registry

18 липня 2026

Docker Registry це просто HTTP-сервер із конкретним набором endpoints. Meadow реалізує OCI Distribution Spec за 370 рядків одним /v2/-хендлером, що маршрутизує за шляхом.

  • Blob upload із перевіркою digest після запису, де розбіжність означає видалення і помилку. Manifest push і pull за тегом і за digest.
  • Перелічений формат помилок OCI (BLOB_UNKNOWN, MANIFEST_UNKNOWN, DIGEST_INVALID), який клієнти парсять за кодом, а не повідомленням, і чому маршрутизація через LastIndex виживає з іменами репо зі слешами.
  • Де ламається: monolithic-only upload тримає весь blob у пам'яті, немає chunked PATCH, немає пагінації.
  • Побіжно: /v2/ це хендшейк v1 до v2, що пережив Registry v1; специфікацію витягнули з Docker HTTP API v2; 202 Accepted означає "продовжуй"; розподіл transport проти image spec дозволяє тому самому API возити Helm-чарти й SBOM'и через OCI artifacts і ORAS; cross-repository blob mount існує; а продакшн пінить за digest, а не за тегом.

Двадцять перша частина серії Sheep & Shepherd.

Event System: audit trail для кластера

15 липня 2026

Коли щось відбувається в кластері, потрібно знати що, коли і чому. Event system Shepherd-а записує Normal і Warning події від API Server, Scheduler, NodeController і ReplicationController, із ключем із nanosecond timestamp, тому BoltDB безкоштовно тримає їх у хронологічному порядку.

  • Events прив'язані до об'єктів (pod/web-0), а не до процесів, і саме це відрізняє їх від stdout-логів.
  • Проти справжнього Kubernetes: TTL 1 година й агрегація з count, окремий Audit Logging для безпеки, involvedObject і kubectl describe, міграція на events.k8s.io/v1, і чому Events це сповіщення, а не Event Sourcing.

Фінальна частина серії Orchestrator. Двадцята частина серії Sheep & Shepherd.

Pod Lifecycle: від Pending до Running

11 липня 2026

Повний асинхронний шлях поду від sheepctl apply до Running, через API Server, Scheduler, Agent і runtime, де жоден компонент ніколи не чекає на інший.

  • Той самий Pending означає "чекає на Scheduler" без NodeName і "чекає на Agent" з ним.
  • Eventual consistency, та сама модель, що й у git, робить латентність у 5-15 секунд прогнозованою сумою незалежних ticker-ів, а не багом.
  • Односторонній перехід Running → Failed і health-check loop, що переводить под у Failed, коли контейнер зупиняється.
  • Проти справжнього Kubernetes: phase як навмисно груба абстракція проти справжньої деталізації в conditions, фаза Pulling, де поди зависають на повільному registry, initContainers, RestartPolicy і CrashLoopBackOff, terminationGracePeriodSeconds, і чому readiness та liveness probes це те, чого тут бракує.

Дев'ятнадцята частина серії Sheep & Shepherd.

Node Agent: kubelet

8 липня 2026

Agent це kubelet Shepherd-а: реєстрація з retry, heartbeats кожні 10 секунд, reconciliation подів кожні 3 секунди і запуск контейнерів для призначених подів.

  • Структура, детекція ресурсів з /proc/meminfo і reconcile loop, що переводить Pending у Running.
  • Чому окремий IP на контейнер у Shepherd це спрощення порівняно зі спільним network namespace у Kubernetes.
  • Проти справжнього Kubernetes: CRI замість прямих викликів runtime, pause infra container, syncLoop і PLEG, static pods, TLS bootstrap і чому ліміт 110 max-pods це насправді про навантаження на control plane.

Вісімнадцята частина серії Sheep & Shepherd.

Node Health: heartbeat і failure detection

4 липня 2026

Як control plane знає, що нода жива? Через heartbeats. Агент шле його кожні 10 секунд, і якщо 30 секунд тиша, NodeController переводить ноду в NotReady, а scheduler перестає ставити на неї поди.

  • Дві сторони механізму: агент, що пише "я живий", і окремий контролер, що вирішує "ти мертвий".
  • Heartbeat це не перевірка живості, а домовленість про timeout. Звідси проблема split-brain, коли губиться лише heartbeat, і пастки clock skew та last-write-wins.
  • Проти справжнього Kubernetes: об'єкт Lease, що замінив переписування повного статусу, taint-based виселення з обмеженням швидкості, і як Φ Accrual detector та SWIM gossip виходять за межі єдиного захардкодженого timeout.

За зразком heartbeat-ів нод Kubernetes. Сімнадцята частина серії Sheep & Shepherd.

Service Discovery: як Service знаходить Pods

1 липня 2026

Поди приходять і йдуть, їхні IP змінюються при кожному рестарті. Service це стабільна точка входу, яка завжди знає, де потрібні поди.

  • ServiceController кожні 5 секунд перебудовує список endpoints через label selectors. Под має збігтися лейблами, бути Running і мати IP, щоб туди потрапити.
  • Тобто Service це насправді просто запис у базі, який хтось перераховує, а не проксі.
  • Проти справжнього Kubernetes: ClusterIP як віртуальний IP, якого немає на жодному інтерфейсі, режими iptables та IPVS у kube-proxy, EndpointSlice, headless-сервіси та альтернатива на eBPF у Cilium.

За зразком Kubernetes Services. Шістнадцята частина серії Sheep & Shepherd.

Replication Controller: scale up і scale down

27 червня 2026

Deployment каже "хочу 3 репліки". Ось як ReplicationController це забезпечує.

  • Створення подів з template, слабкий зв'язок через label-selector app=web замість списку власних подів, scale up і scale down, оновлення ReadyReplicas і весь асинхронний шлях від sheepctl до запущеного контейнера.
  • Чому індексні імена (web-0) і перетин селекторів кусаються, і де тут місце kubectl scale, HorizontalPodAutoscaler та scale-to-zero.

За зразком Kubernetes ReplicaSet. П'ятнадцята частина серії Sheep & Shepherd.

Reconciliation Loop: серце Shepherd

24 червня 2026

Reconciliation loop це серце будь-якого оркестратора: ти описуєш бажаний стан, а контролери постійно звіряють його з реальним і виправляють різницю.

  • Паттерн observe-compare-act і чому він робить систему самовідновлюваною.
  • Три контролери, що працюють паралельно в Shepherd, ідемпотентність як вимога виживання і чому level-triggered кращий за edge-triggered.

За зразком контролерів Kubernetes. Чотирнадцята частина серії Sheep & Shepherd.

Scheduler: як вибрати ноду для поду

20 червня 2026 - Свіжостворений под - Pending і без ноди. Як шедулер Shepherd вибирає найкращу ноду у дві фази: filter (відсіяти непридатні) і score (віддати перевагу найменш завантаженій). Перевірка ресурсів, збіг лейблів, скоринг least-loaded і чому под лишається Pending навіть після планування. За зразком шедулера Kubernetes. Тринадцята частина серії Sheep & Shepherd.

BoltDB замість etcd: embedded state store

17 червня 2026

Kubernetes тримається на etcd, але для навчального проекту це overkill. Shepherd зберігає весь стан кластера в BoltDB, вбудованому key-value сховищі в одному файлі.

  • Buckets як таблиці, ключі з namespace, транзакції на читання і запис, watch-канали для сповіщень про зміни і журнал подій.
  • Чому це ідеально для навчання і де воно поступається etcd.

Дванадцята частина серії Sheep & Shepherd.

Kubernetes API Server за 300 рядків

13 червня 2026 - Kubernetes API Server - центр всього кластера. Як Shepherd реалізує REST API з CRUD для pods, services, deployments та nodes за ~300 рядків Go, використовуючи тільки net/http. Асинхронне планування, namespaced ресурси та logging middleware. Одинадцята частина серії Sheep & Shepherd.

Docker CLI за 500 рядків Go

10 червня 2026 - Subcommand routing, flag parsing і форматований вивід - все без CLI-фреймворків. Як Sheep реалізує run, ps, stop, rm та ще 11 Docker-подібних команд в одному файлі тільки на стандартній бібліотеці Go. Десята частина серії Sheep & Shepherd.

Container Lifecycle: state machine від Created до Removed

7 червня 2026 - Контейнер проходить три стани - created, running, stopped. Як state machine керує Create/Start/Stop/Remove, чому кожен перехід зберігається в state.json, і як signal 0 перевіряє, чи пережив контейнер рестарт демона. Дев'ята частина серії Sheep & Shepherd.

Image Management: tar-архів → rootfs → контейнер

2 червня 2026 - Образ контейнера - це по суті архів із файловою системою. Як tar-архів перетворюється на rootfs через import і bootstrap, як OCI whiteout-файли видаляють файли між шарами, і чому Sheep зберігає повний rootfs замість шарів. Восьма частина серії Sheep & Shepherd.

NAT і iptables: як контейнер бачить інтернет

30 травня 2026 - Адреса контейнера 10.20.0.x приватна - її не маршрутизує жоден роутер. Як ip_forward і одне правило MASQUERADE дають пакетам вийти в інтернет і повернутися назад через conntrack. Сьома частина серії Sheep & Shepherd.

Bridge Networking: даємо контейнеру IP-адресу

25 травня 2026 - Контейнер у новому network namespace не має мережі - навіть loopback не піднятий. Як Linux bridge, veth пари і трохи NAT дають йому IP і маршрут в інтернет. Шоста частина серії Sheep & Shepherd.

OverlayFS: copy-on-write шари як у Docker

20 травня 2026 - Як OverlayFS складає read-only шар образу і per-container read-write шар в одну файлову систему, і чому copy-up дозволяє 10 контейнерам nginx ділити 100MB замість того, щоб кожен тягнув свою копію. П'ята частина серії Sheep & Shepherd.

Cgroups v2: обмежуємо пам'ять, CPU та PIDs

15 травня 2026 - Namespace'и ізолюють, але не обмежують. Як memory.max, cpu.max і pids.max накладають ліміти на ресурси контейнера через віртуальну файлову систему cgroups v2 - четверта частина серії Sheep & Shepherd.

pivot_root: як контейнер отримує свою файлову систему

9 травня 2026 - Як pivot_root(2) міняє кореневу директорію процесу на рівні mount namespace, і чому це правильний примітив ізоляції замість chroot. Третя частина серії Sheep & Shepherd.

Re-Exec Pattern: чому Go і clone() не дружать

2 травня 2026 - Go threading model конфліктує з clone(). Self re-exec вирішує проблему - друга частина серії Sheep & Shepherd.

Linux Namespaces: ізолюємо процес за 50 рядків Go

28 квітня 2026 - Контейнер - це процес з обмеженим видом на систему. Як ізолювати процес за допомогою Linux namespaces за 50 рядків Go - перша частина серії Sheep & Shepherd.

AI-суверенітет: своя модель на DGX Spark замість API

18 квітня 2026 - Як я перестав платити OpenAI і переніс інференс на власну коробку DGX Spark з vLLM. Про залізо, проблеми з CUDA/PyTorch, порівняння з Ollama і простий веб-інтерфейс для керування.

EMM: трейси LangGraph у Phoenix

11 квітня 2026 - Один виклик при старті покриває 15 LangGraph-агентів. Ручні span-и розширюють покриття на voice (Gemini Live tools), avatar (Runway сесії) та Izabella chat (OpenAI/Ollama/Google + MCP tool loop).

A2A Inspector у моєму application і MCP до нього

8 квітня 2026 - Вбудований UI плюс MCP-сервер: перегляд Agent Card, tasks/submit і tasks/status з IDE без виходу з монорепи.

EMM A2A Phase 4: Auth, Rate Limiting, Observability

4 квітня 2026 - X-API-Key, rate limiting, структуроване логування. A2A endpoints тепер захищені як інші API.

AI Reliability Engineering: сертифікація від fwdays

31 березня 2026 - Завершив курс AI Reliability Engineering від fwdays. Чому надійність AI-систем для мене в одному ряду з класичним SRE.

EMM A2A Phase 3: Stream task status

28 березня 2026 - SSE замість polling. GET /api/a2a/tasks/{id}/stream. Теорія, схеми, capabilities.streaming.

EMM A2A Phase 2+: TaskStore і tasks/status

21 березня 2026 - Task lifecycle в A2A: submit → taskId → poll status. InMemoryTaskStore, TTL 1 год. Схеми.

Технологічні ризики у production: як не програти, коли проект застаріє

14 березня 2026 - PostgreSQL, MinIO, lakeFS: коли вибираєш технології для production, думай не тільки про features - що буде через 5 років?

EMM A2A Phase 2: Task Manager (list_board)

14 березня 2026 - Другий skill - list_board. Маршрутизація по skillId, схеми взаємодії, що змінилось.

EMM A2A Phase 1: Process Manager як A2A сервер

7 березня 2026 - Process Manager - перший агент з A2A інтерфейсом. Теорія протоколу, діаграми взаємодії, що реалізовано.

Як я став AWS Community Builder

5 березня 2026 - Кілька років тому це був звичайний DevOps - деплої, скрипти. Потім почав думати кластерами замість серверів. Ось як це привело до AWS Community Builders.

Розробка та тестування AI-агентів: від LangGraph до production

12 лютого 2026 - Як писати, тестувати і дебажити LangGraph агентів? Які patterns працюють для StateGraph? Чому pytest fixtures критичні? Розповідаю про development workflow від першого коду до production deployment.

Версіонування даних в AI-агентах: практичний досвід з lakeFS

8 лютого 2026 - Коли AI-агенти починають класифікувати і переміщувати твої файли, питання 'а як відкотити назад?' стає дуже практичним. Розповідаю про інтеграцію lakeFS - системи версіонування для об'єктного сховища.

Kubernetes deployment для AI-агентів: практичний досвід з LangGraph

3 лютого 2026 - Коли AI-агенти переїжджають з локального Docker Compose в Kubernetes, виникають питання про service discovery, кеш-інг, secrets management. Розповідаю як я задеплоїв 7 microservices з мінімальним downtime.

Створення агента - мультисайтового парсера оголошень квартир: Патерни проєктування та архітектура

16 жовтня 2025 - Побудова мультисайтового парсера оголошень квартир: патерни проєктування та архітектура агента.

Tarot AI Agent: Інноваційний підхід до оцінки ризиків через штучний інтелект

September 11, 2025 - Tarot AI Agent: Інноваційний підхід до оцінки ризиків через штучний інтелект.

Використання AWS ECR як універсального OCI-репозиторію

10 липня 2025 - Використання AWS ECR як універсального OCI-репозиторію для зберігання різних типів артефактів.

Використання AWS ECR як універсального OCI-репозиторію (УК)

10 липня 2025 - Використання AWS ECR як універсального OCI-репозиторію для зберігання різних типів артефактів (українська версія).

Cert Manager в Kubernetes

3 червня 2025 - Налаштування та використання Cert Manager для автоматичного управління SSL сертифікатами в Kubernetes.

Cert Manager в Kubernetes (УК)

3 червня 2025 - Налаштування та використання Cert Manager для автоматичного управління SSL сертифікатами в Kubernetes (українська версія).

Нова архітектура для Ізабелли - C4 діаграми

14 травня 2025 - Розробка нової архітектури системи з використанням C4 діаграм.

Нова архітектура для Ізабелли - Структура

14 травня 2025 - Детальний аналіз структури нової архітектури Ізабелли.

Нова архітектура для Ізабелли

10 травня 2025 - Огляд нового дизайну архітектури для проекту Ізабелла.

Waste Resources - Фінансова оптимізація в хмарі

14 квітня 2025 - Аналіз та оптимізація витрат на хмарні ресурси.

Redis Backup на AWS S3

19 березня 2025 - Автоматизація резервного копіювання Redis на AWS S3.

EMM A2A Phase 1 (EN)

March 7, 2026 - Process Manager is the first agent with an A2A interface. Protocol theory, interaction diagrams, what's implemented.

RDS Import з Terraform

17 лютого 2025 - Імпорт існуючих RDS інстансів в Terraform.

RDS Import з Terraform (EN)

17 лютого 2025 - Імпорт існуючих RDS інстансів в Terraform (англійська версія).

AWS Lambda Cost Optimization

26 лютого 2025 - Стратегії оптимізації витрат на AWS Lambda.

Kubernetes Onboarding з Flux

10 березня 2025 - Автоматизація онбордингу в Kubernetes з використанням Flux.

RDS Migration Cases

9 лютого 2025 - Різні сценарії міграції баз даних на AWS RDS.

SPA Deployment на S3 з CloudFront

12 лютого 2025 - Розгортання Single Page Application на AWS S3 з CloudFront.

Karpenter Properties

8 грудня 2024 - Властивості та налаштування Karpenter для Kubernetes.

AI Stable Diffusion

8 лютого 2025 - Використання Stable Diffusion для генерації зображень.

ARM vs AMD

5 грудня 2024 - Порівняння ARM та AMD архітектур для хмарних рішень.

My First Blog Post

27 квітня 2024 - Привітання та вступ до блогу.