К этому моменту у нас уже есть всё необходимое: локальный кластер k3d с именем dev (см. главу про k3d), Dockerfile для нашего сервиса myapp (см. главу про контейнеризацию) и набор манифестов Kubernetes (см. главу про манифесты). Но если попробовать разрабатывать «вручную», цикл получается мучительный: меняешь строчку в Python-коде → docker builddocker push в k3d-registry.localhost:5000kubectl 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:

python
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(). Шаги выполняются в строго заданном порядке, и порядок нельзя нарушать:

  1. fall_back_on(['путь']) — только в самом начале. Если изменился указанный файл, Live Update отменяется и запускается полная пересборка. Логика проста: некоторые изменения нельзя «подсунуть» в контейнер на лету.
  2. sync('локальный', '/в-контейнере') — основа основ. Копирует файлы из локальной директории в контейнер.
  3. run('команда', trigger=[...]) — выполняет команду внутри контейнера. Опциональный trigger ограничивает запуск только определёнными изменениями.
  4. 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 запускать только когда поменялись зависимости:

python
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 внутри их видит и перезагружает приложение. Ничего дополнительно настраивать не нужно.

Команда запуска в контейнере выглядит так:

bash
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:

python
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 поддерживает ручной режим обновления. Можно перевести в ручной режим всё сразу или отдельные ресурсы:

python
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. Все трое умеют примерно одно: собирать образ, деплоить в кластер и синкать файлы для быстрого цикла. Различия — в подходе.

TiltSkaffoldDevSpace
Лицензия / авторApache-2.0, TiltGoogleopen-source
КонфигTiltfile (Starlark)YAMLYAML
Web-UIБогатый, из коробкиНет (CLI)Нет (CLI)
Файл-синк / liveLive Updatefile 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 поднимать базу вместе с сервисом: зависимости: базы данных, очереди, кэши.

Источники

Нужна помощь с настройкой Tilt для вашей команды?
Хотите быстрый inner dev loop на Kubernetes с Tilt и Live Update? Помогу настроить Tiltfile и удобный локальный рабочий процесс для вашей команды.