Блог¶
Ласкаво просимо до мого блогу! Тут ви знайдете статті про 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, iptablesMASQUERADE. Це і є контейнер. Усе решта - 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 alarmNOSPACE, який треба знімати руками.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 дешевший, ніж звучить, бо вloMTU 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; fallbackManifests[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-файл та atomicos.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 - Привітання та вступ до блогу.