Local Kubernetes Dev — Part 1: The inner dev loop and why run a cluster locally
Почему цикл разработки в Kubernetes такой медленный, почему код «работает локально, падает в кластере» — и что мы построим за эту серию, чтобы решить обе проблемы.
Представьте: вы написали сервис, он отлично работает у вас на ноутбуке, и теперь его надо выкатить в Kubernetes. Кажется, что осталось всего ничего — собрать контейнер и нажать «деплой». Но именно здесь начинается самое больное место: цикл разработки, который только что был мгновенным, вдруг растягивается на минуты, а код, который «работал локально», падает в кластере по причинам, о которых вы раньше даже не задумывались.
Эта глава — про то, почему так происходит и что мы с этим будем делать. Договоримся сразу о сквозном примере, который пройдёт через всю серию.
Наш герой — сервис `myapp`:
- HTTP API на порту
8080; - написан на Python 3.12 + FastAPI, запускается через
uvicorn; - в режиме разработки используется горячая перезагрузка:
uvicorn --reload; - зависит от PostgreSQL.
Локально мы будем поднимать кластер на k3d (это лёгкий Kubernetes, который запускается в Docker), кластер назовём dev, рабочее пространство (namespace) — myapp, а образы будем складывать во встроенный реестр k3d по адресу k3d-registry.localhost:5000. Не пугайтесь незнакомых слов — все термины разберём по мере появления.
Что такое inner dev loop
Inner dev loop (внутренний цикл разработки) — это тот цикл, который один разработчик прокручивает у себя на машине снова и снова, пока пишет код: изменил код → собрал → запустил/перезапустил → проверил результат → поправил. Это происходит до того, как вы поделитесь изменениями с командой (через commit и push). Документация Telepresence формулирует это так: «один разработчик должен уметь настроить и использовать внутренний цикл, чтобы быстро писать и тестировать изменения», и главное — «чем быстрее цикл обратной связи, тем быстрее разработчик может рефакторить и тестировать снова».
Ключевое слово здесь — скорость обратной связи. Когда вы разрабатываете myapp локально без всякого Kubernetes, цикл выглядит примерно так:
- меняете функцию в Python-файле;
uvicorn --reloadсам подхватывает изменение за доли секунды;- открываете браузер или дёргаете
curl localhost:8080/...; - видите результат.
Весь цикл — секунды. Вы можете повторять его десятки раз в час, почти не отвлекаясь от мыслей о задаче. Это и есть здоровый inner loop.
Важно не путать его с outer loop (внешним циклом). Outer loop начинается после git push: это CI-пайплайн, сборка артефактов, доставка в кластер через GitOps-инструменты вроде ArgoCD или Helm, прогон интеграционных тестов на общем окружении. Эта серия — почти целиком про inner loop: как сделать его быстрым и при этом похожим на прод.
Почему в Kubernetes цикл становится медленным
Как только мы решаем запускать myapp не просто как процесс, а как под (pod — минимальная запускаемая единица в Kubernetes, обёртка вокруг одного или нескольких контейнеров), между «изменил код» и «увидел результат» вклинивается целая цепочка шагов. Документация Telepresence перечисляет четыре дополнительных шага, которых не было при локальном запуске:
- упаковать код в контейнер (
docker build); - написать/обновить манифест Kubernetes;
- отправить образ в реестр (
docker push); - задеплоить контейнер в кластер и дождаться, пока под поднимется.
Теперь цикл для myapp выглядит так:
1# 1. собрать образ
2docker build -t k3d-registry.localhost:5000/myapp:dev .
3
4# 2. отправить в реестр
5docker push k3d-registry.localhost:5000/myapp:dev
6
7# 3. применить манифесты
8kubectl apply -f k8s/
9
10# 4. дождаться нового пода и посмотреть, что он живой
11kubectl rollout status deployment/myapp -n myapp
12kubectl logs -f deployment/myapp -n myappКаждый такой проход — это, по оценке oneuptime, порядка 2–5 минут на итерацию: редактирование кода, сборка образа, push в реестр, обновление манифестов, ожидание перезапуска пода, проверка. Сравните это с секундами при uvicorn --reload— разница на порядки. Тот же источник отмечает, что синхронизация файлов прямо в работающий под с горячей перезагрузкой возвращает итерацию к 1–5 секундам, то есть сокращает время цикла на 95% и более. Именно ради этого ускорения существуют инструменты вроде Tilt, которым мы посвятим отдельную главу.
Есть и отдельный, очень коварный источник трения: локальный кластер не видит образы из вашего локального Docker-демона. Кажется логичным, что раз вы только что собрали myapp:dev через docker build, кластер сразу его подхватит, но это не так — и образ приходится либо отправлять в доступный кластеру реестр, либо загружать туда специальной командой. Как именно это делается в k3d, подробно разбираем в главе про контейнеризацию. Если шаг доставки пропустить, вы получите под в статусе ImagePullBackOff — kubelet бесконечно пытается стянуть образ, которого в реестре нет.
Грабли с тегом :latest
Если в манифесте указать образ как myapp:latest, Kubernetes по умолчанию выставит imagePullPolicy: Always — kubelet будет тянуть образ из реестра при каждом запуске, даже если локально он уже есть. Используйте конкретные теги (myapp:dev, myapp:abc123) и при необходимости imagePullPolicy: IfNotPresent.
Разрыв «локально работает / в кластере падает»
Медленный цикл — это полбеды. Вторая половина — это баги, которые в принципе невозможно увидеть на ноутбуке без кластера. Почему? Потому что среда, в которой вы обычно проверяете код (просто uvicorn или docker compose), не воспроизводит важные механизмы Kubernetes. Команда Testkube формулирует это прямо: «CI-контейнеры не имеют ограничений ресурсов, сетевых политик, RBAC или правил service mesh, которые есть в проде». Отсюда целый класс багов, видимых только в кластере:
- OOMKilled — под убит за превышение лимита памяти. Локально память «бесконечная», и
myappспокойно ест сколько хочет; в кластере выставлен лимит, и под умирает. - Throttling по CPU. Если задать лимит CPU и приложение его превышает, Kubernetes жёстко притормаживает (throttle) контейнер — отсюда скачки задержек и неожиданные провалы проверок здоровья.
- Блокировки сетевыми политиками (network policy). В проде трафик между подами может быть ограничен; локально его никто не ограничивает, и
myappходит в PostgreSQL без проблем — ровно до выкатки. - Провалы readiness-проб — Kubernetes считает под неготовым и не пускает на него трафик, а вы ломаете голову, почему сервис «есть, но не отвечает».
- RBAC: access denied — локально авторизация обычно разрешает всё, а в проде включён строгий RBAC, и сервис, который локально спокойно ходил в Kubernetes API, получает отказ уже после деплоя.
Суть проблемы одна: чем сильнее ваше окружение для проверки отличается от прода, тем больше багов «просочится» дальше по конвейеру, где их в разы дороже искать и чинить.
Идея shift-left: ловить проблемы окружения как можно раньше
Shift-left («сдвиг влево») — это принцип: ловить проблемы как можно раньше, ближе к началу конвейера разработки, а не в конце. Звучит как банальность, но для Kubernetes у него есть конкретный смысл. Дело не просто в том, чтобы тестировать пораньше — дело в том, в какой среде вы тестируете. Testkube подмечает это очень метко: «Тестировать раньше в CI-контейнере, который не совпадает с вашим кластером, — это не shift-left. Это просто провал быстрее в неправильном окружении».
Ключевое понятие тут — точность воспроизведения окружения (environment fidelity). Настоящий shift-left для Kubernetes означает раннюю проверку против реальных условий кластера: реальные ограничения ресурсов (а не «безлимитный» CI-контейнер), реальные сетевые политики, реальные сервисы в namespace (а не моки), и проверку того, что манифесты вообще дают здоровые поды. Эта серия живёт на уровне лёгкого локального кластера (kind, k3s/k3d) — настоящий Kubernetes, пусть и без полного паритета с продом.
Кстати, о здоровых подах. Официальный блог Kubernetes относит к частым граблям пропуск resource requests/limits и недооценку проб здоровья — без проб «Kubernetes считает, что нагрузка работает, даже если приложение внутри не отвечает». Различие между liveness- и readiness-пробами мы разбираем канонически в главе про приближение к проду; как смотреть статус пробы при отладке — в главе про наблюдаемость.
Что мы построим к концу серии
Сложим всё вместе. У нас две проблемы: цикл разработки в Kubernetes медленный (минуты вместо секунд) и есть разрыв между локалкой и продом (баги, видимые только в кластере). Решение — собрать локальное окружение, которое одновременно быстрое и похожее на прод. К концу у вас будет рабочая связка из четырёх частей:
- Локальный кластер на k3d — настоящий Kubernetes у вас на машине, а не его имитация. Поднимем его в главе про k3d.
- Контейнеризация и реальные манифесты. Dockerfile для
myappи манифесты Kubernetes — те же примитивы (Deployment, Service, пробы, лимиты ресурсов), что и в проде, а неdocker-composeкак замена кластера. - Инструмент быстрого цикла — Tilt с live-update, который синхронизирует код в работающий под и возвращает итерацию к секундам.
- Паритет с продом — зависимости вроде PostgreSQL внутри кластера, правильная работа с конфигами и секретами, сеть и Ingress, а также пробы здоровья, requests/limits ресурсов и прочие приёмы, делающие локалку по-настоящему похожей на прод.
Иными словами, мы реализуем shift-left на практике: будем ловить проблемы окружения локально, ещё до того, как код уедет в общий кластер. А чтобы понять, кто из инструментов за что отвечает и почему мы выбрали именно k3d и Tilt, начнём со следующей главы — про то, что вообще значит «production-like окружение».
Источники
- The developer experience and the inner dev loop — Telepresence docs
- Inner Development Loop Optimization with File Sync to Kubernetes Pods — oneuptime
- Inner Loop and Outer Loop — Stakater KubeStack+ docs
- What Shift-Left Testing Means for Cloud-Native Teams — Testkube Blog
- 7 Common Kubernetes Pitfalls (and How I Learned to Avoid Them) — kubernetes.io blog