Local Kubernetes Dev — Part 8: Tilt — a fast development loop
Tilt оркестрирует ваш inner dev loop: следит за файлами, сам пересобирает образ, деплоит в кластер, а с Live Update синхронизирует код в работающий контейнер за секунды.
К этому моменту у нас уже есть всё необходимое: локальный кластер k3d с именем dev (см. главу про k3d), Dockerfile для нашего сервиса myapp (см. главу про контейнеризацию) и набор манифестов Kubernetes (см. главу про манифесты). Но если попробовать разрабатывать «вручную», цикл получается мучительный: меняешь строчку в Python-коде → docker build → docker push в k3d-registry.localhost:5000 → kubectl rollout restart → ждёшь, пока поднимется новый под → смотришь логи через kubectl logs. Минуту-другую на каждую правку. Десять правок — и полчаса жизни ушло на ожидание.
Tilt решает ровно эту боль. Это инструмент, который оркеструет ваш inner dev loop: следит за файлами, сам пересобирает образ, сам деплоит в кластер, а главное — умеет обновлять код прямо в работающем контейнере за секунды. Плюс даёт web-дашборд, где видны все ваши сервисы, их статусы и логи в одном месте. Давайте разберёмся, как он устроен.
Что такое Tiltfile и из чего он состоит
Конфигурация Tilt живёт в файле с именем Tiltfile (без расширения) в корне проекта. Это не YAML и не JSON — это программа на языке Starlark, диалекте Python. То есть в нём есть функции, циклы, списки, условия — всё как в обычном Python, только урезанном до безопасного детерминированного подмножества. Для новичка это и плюс, и минус: синтаксис знакомый, но придётся привыкнуть, что это код, а не декларативный конфиг.
Когда вы запускаете tilt up, Tilt исполняет Tiltfile сверху вниз. Функции вроде docker_build() и k8s_yaml() ничего не делают «прямо сейчас» — они регистрируют конфигурацию. Из этих регистраций Tilt строит граф ресурсов: что нужно собрать, что задеплоить, что от чего зависит. Дальше Tilt начинает следить за файлами и при изменениях автоматически пересобирает и передеплоивает то, что затронуто. Если вы поменяете сам Tiltfile, Tilt переисполнит его заново — не нужно ничего перезапускать.
Самый минимальный осмысленный Tiltfile состоит буквально из двух строк: «как собрать образ» и «что задеплоить». Всё остальное — донастройка.
docker_build + k8s_yaml + k8s_resource — три кита
Три функции, на которых держится почти любой Tiltfile:
docker_build('тег', 'контекст')— как собрать образ. Первый аргумент — тег образа (например,myapp), второй — путь к build-контексту (обычно.).k8s_yaml('файл')— что задеплоить. Регистрирует ваши манифесты Kubernetes.k8s_resource('имя', ...)— донастройка уже собранного Tilt'ом ресурса: проброс портов, переименование, группировка.
Магия в том, как Tilt связывает эти три вещи. Он сканирует переданный YAML, находит в нём workload'ы (Deployment, StatefulSet и т.п.) и для каждого создаёт ресурс. Затем сопоставляет образы из docker_build с образами, прописанными в манифестах, по совпадению тега. Если совпало — Tilt подменяет в манифесте тег на свежесобранный образ (с уникальным ID, чтобы Kubernetes гарантированно подхватил новую версию) и деплоит. Связанные Service подтягиваются к тому же ресурсу автоматически.
Вот рабочий Tiltfile для myapp:
1# Tiltfile
2
3# Как собрать образ myapp из текущей директории
4docker_build('k3d-registry.localhost:5000/myapp', '.')
5
6# Что задеплоить — наши манифесты из главы про манифесты
7k8s_yaml(['k8s/deployment.yaml', 'k8s/service.yaml'])
8
9# Донастройка: пробросить контейнерный порт 8080 пода на localhost:8080
10k8s_resource('myapp', port_forwards='8080:8080')Тег k3d-registry.localhost:5000/myapp тут должен совпадать с image: в вашем deployment.yaml — иначе Tilt не свяжет сборку с деплоем и просто задеплоит то, что лежит в манифесте без пересборки.
После tilt up сервис будет доступен на localhost:8080, а k8s_resource с port_forwards избавляет вас от ручного kubectl port-forward. Обратите внимание: 8080:8080 здесь форвардит на контейнерный порт пода (8080), на котором слушает uvicorn, а не на порт Service (80 из главы про манифесты) — Tilt пробрасывает напрямую в под, минуя Service. Важное правило: один docker_build на каждый активно разрабатываемый образ. Если у вас монорепа из пяти сервисов, но прямо сейчас вы трогаете только myapp — собирайте Tilt'ом только его, а остальные пусть едут из готовых образов.
Live Update: обновление кода в работающем контейнере за секунды
Это киллер-фича Tilt. Обычный цикл «пересобрать образ → передеплоить под» занимает десятки секунд даже на маленьком сервисе: слои Docker, push в registry, pull в кластере, рестарт пода. Live Update ломает этот цикл: вместо пересборки Tilt копирует изменённые файлы прямо внутрь уже работающего контейнера и при необходимости выполняет там команды. Это занимает секунды.
Настраивается через параметр live_update=[...] внутри docker_build(). Шаги выполняются в строго заданном порядке, и порядок нельзя нарушать:
fall_back_on(['путь'])— только в самом начале. Если изменился указанный файл, Live Update отменяется и запускается полная пересборка. Логика проста: некоторые изменения нельзя «подсунуть» в контейнер на лету.sync('локальный', '/в-контейнере')— основа основ. Копирует файлы из локальной директории в контейнер.run('команда', trigger=[...])— выполняет команду внутри контейнера. Опциональныйtriggerограничивает запуск только определёнными изменениями.restart_container()— последним. В основном для Docker Compose; для Kubernetes используйте подход из раздела про hot reload.
Tilt принимает решение по простому дереву: сработал fall_back_on → полная пересборка; файл попал под sync → быстрый live update; файл в build-контексте, но не покрыт ни одним sync → полный docker build; файл вообще не отслеживается → ничего не происходит.
Два ограничения, на которых спотыкаются все новички:
sync-пути обязаны быть внутри build-контекста того жеdocker_build. Девиз Tilt: «If Tilt is watching it, you can sync it». Если синкаете путь снаружи контекста — live update молча не сработает.run()не может стоять передsync(). Сначала кладём файлы, потом что-то с ними делаем.
И помните: первый деплой всегда полный. Live Update требует уже работающего контейнера, в который можно копировать файлы. Холодный старт он не ускоряет.
Типичный пример для Python-сервиса — синкать код, а тяжёлый pip install запускать только когда поменялись зависимости:
1docker_build(
2 'k3d-registry.localhost:5000/myapp',
3 '.',
4 live_update=[
5 # если поменялись зависимости — проще пересобрать образ целиком
6 fall_back_on(['requirements.txt']),
7 # код синкаем мгновенно (в /code/app — это WORKDIR из Dockerfile главы про контейнеризацию)
8 sync('./app', '/code/app'),
9 # на всякий случай переустановим зависимости, но только если файл изменился
10 run('pip install -r requirements.txt', trigger=['requirements.txt']),
11 ],
12)Тут fall_back_on(['requirements.txt']) и run(..., trigger=['requirements.txt']) отчасти дублируют друг друга по смыслу — в реальном проекте обычно выбирают один подход. fall_back_on надёжнее (полная чистая пересборка), run быстрее (доустановка в живой контейнер). Для новичка я советую fall_back_on: меньше шансов получить «грязное» состояние контейнера.
Hot reload для вашего стека (uvicorn --reload и аналоги)
Live Update доставил новые файлы в контейнер — но как заставить приложение их подхватить? Тут два сценария.
Сценарий А: фреймворк сам умеет hot-reload. Наш myapp на FastAPI запускается через uvicorn, а у uvicorn есть флаг --reload: внутренний watcher следит за .py-файлами и перезапускает процесс при изменениях. Flask dev-сервер умеет то же самое. В этом случае достаточно sync — файлы прилетают в контейнер, watcher внутри их видит и перезагружает приложение. Ничего дополнительно настраивать не нужно.
Команда запуска в контейнере выглядит так:
1uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload --reload-dir appНюанс: uvicorn --reload перезапускает весь процесс на каждое изменение — заново стартует интерпретатор Python и переимпортирует все модули. На большом приложении это заметно медленнее, чем точечная подмена. Флаг --reload-dir ./app сужает зону наблюдения watcher'а до нужной директории, чтобы он не дёргался на каждый временный файл (флаги --reload и --reload-dir описаны в настройках uvicorn). Запуск через --reload — это режим разработки; в главе про приближение к проду мы обсудим, почему в прод-образе reload включать не стоит.
Сценарий Б: hot-reload нет. Допустим, у вас Go-бинарник или Python-сервис, запущенный без --reload — тогда новые файлы лежат в контейнере, но старый процесс про них не знает. Нужно перезапустить процесс после live update. Для Kubernetes-ресурсов это делает расширение restart_process:
1load('ext://restart_process', 'docker_build_with_restart')
2
3docker_build_with_restart(
4 'k3d-registry.localhost:5000/myapp',
5 '.',
6 entrypoint='python -m uvicorn app.main:app --host 0.0.0.0 --port 8080',
7 live_update=[
8 sync('./app', '/code/app'),
9 ],
10)docker_build_with_restart оборачивает обычный docker_build и после каждого live update заново выполняет команду из entrypoint. Это современная замена устаревшему restart_container(), который для Kubernetes больше не рекомендуется (он остаётся актуальным в основном для Docker Compose).
У restart_process есть ограничения: он не работает без shell в образе, не работает если команда запуска задана в манифесте Kubernetes (а не в самом образе), не работает с Docker Compose и с частью сборок через custom_build. Для нашего myapp сценарий А (uvicorn --reload) проще и предпочтительнее — restart_process держите в уме на случай стека без встроенного reload.
Web-дашборд Tilt: логи, статусы, ручной trigger
При запуске tilt up поднимается web-UI на localhost:10350 — это порт Tilt по умолчанию. Дашборд — одна из причин, почему Tilt особенно хорош для новичков: всё состояние вашего dev-окружения видно на одном экране, без жонглирования десятком терминалов.
Что показывает дашборд:
- Список ресурсов, сгруппированных по labels (метки можно задавать в
Tiltfile). У каждого ресурса два статуса: update (как прошла сборка/деплой) и runtime (что с подом сейчас). - Pod ID с кнопкой копирования — удобно, чтобы быстро дёрнуть
kubectlруками. - Эндпоинты в виде кликабельных ссылок — те самые
port_forwards, что мы задали вk8s_resource. - Детальные логи с фильтрацией: по источнику (build или runtime), по уровню (только errors/warnings) и по ключевому слову или regex. Это сильно облегчает разбор того, почему под не стартует — подробнее про отладку в главе про наблюдаемость.
- Кнопка Trigger Update — ручной запуск пересборки и редеплоя конкретного ресурса.
Иногда автоматический режим мешает — например, вы хотите сохранять файлы по ходу, но пересобирать только когда сами решите. Tilt поддерживает ручной режим обновления. Можно перевести в ручной режим всё сразу или отдельные ресурсы:
1# глобально — ручной режим
2trigger_mode(TRIGGER_MODE_MANUAL)
3
4# но конкретный ресурс пусть обновляется автоматически
5k8s_resource('myapp', trigger_mode=TRIGGER_MODE_AUTO)В ручном режиме UI рисует звёздочку рядом с ресурсом, у которого есть неприменённые изменения, и кнопку, чтобы их применить. Удобно, когда правок много, а гонять деплой на каждую не хочется.
Остановить всё и убрать ресурсы из кластера можно командой tilt down — она снесёт то, что Tilt задеплоил.
Чем Tilt отличается от Skaffold и DevSpace
Tilt — не единственный инструмент в этой нише. Чаще всего его сравнивают со Skaffold и DevSpace. Все трое умеют примерно одно: собирать образ, деплоить в кластер и синкать файлы для быстрого цикла. Различия — в подходе.
| Tilt | Skaffold | DevSpace | |
|---|---|---|---|
| Лицензия / автор | Apache-2.0, Tilt | open-source | |
| Конфиг | Tiltfile (Starlark) | YAML | YAML |
| Web-UI | Богатый, из коробки | Нет (CLI) | Нет (CLI) |
| Файл-синк / live | Live Update | file sync | двусторонний sync |
Ключевое различие — UI-first против CLI-first и Tiltfile против YAML.
- Tilt делает ставку на наглядный web-дашборд и Live Update. Конфиг — программа на Starlark: гибко, но кривая обучения круче, чем у привычного YAML. Хорошо заходит новичкам и командам со смешанным опытом — потому что всё состояние видно глазами.
- Skaffold — CLI-only, конфиг на YAML, есть file sync, профили под разные окружения и отдельный режим отладки
skaffold debug. Привычный декларативный формат, но без визуальной панели. - DevSpace — open-source CLI, тоже YAML. Команда
devspace devдаёт двусторонний sync, reverse port-forward и dev-контейнеры; есть мультиклауд-сценарии и лёгкая установка.
Честная оговорка: оценки в духе «что кому лучше подходит» субъективны и во многом взяты из обзорных блогов. Если ваша команда живёт в YAML и любит CLI — Skaffold или DevSpace зайдут отлично. Для целей этой статьи — научить новичка комфортно разрабатывать под Kubernetes локально — Tilt выигрывает за счёт дашборда: меньше «слепой» работы в терминале, больше понимания, что вообще происходит в кластере.
В следующей главе подключим к myapp его зависимость — PostgreSQL — и научим Tilt поднимать базу вместе с сервисом: зависимости: базы данных, очереди, кэши.
Источники
- Tiltfile Concepts — Tilt docs
- Writing Your First Tiltfile — Tilt docs
- Live Update Reference — Tilt docs
- Tiltfile API Reference — Tilt docs
- Smart Rebuilds with Live Update — Tilt tutorial
- Tilt UI — Tilt tutorial
- Play and Pause Resources with Manual Update Control — Tilt docs
- restart_process extension — tilt-dev/tilt-extensions
- Skaffold vs Tilt vs DevSpace — vCluster blog
- Settings — Uvicorn