Побег из облака: как пять репозиториев перестали делить один лимит минут

 · 3 min read

Поскольку проект изначально строился как экосистема независимых компонентов, каждый из пяти репозиториев получил свою независимую систему непрерывной интеграции. Архитектурно мы добились идеальной изоляции. Проблема крылась в жестких лимитах бесплатных минут GitHub Actions. Пять отдельных пайплайнов начали прожигать стартовые две тысячи минут со скоростью лесного пожара, запуская независимые джобы на каждый мелкий коммит.

Одна физическая машина для пяти клиентов

Одна VM, пять репозиториев

Мы наотрез отказались докупать облачные минуты и развернули собственный self-hosted раннер прямо на офисном ноутбуке под Windows. Внутри крутилась виртуальная машина Ubuntu под управлением VirtualBox. Эта единственная виртуалка принялась методично обслуживать задачи всех пяти репозиториев по очереди.

Официальная документация GitHub строго не рекомендует привязывать локальные раннеры к публичным проектам из-за риска удаленного исполнения кода через вредоносные пулл-реквесты. Наши репозитории полностью закрыты, поэтому атаки снаружи нам не грозили. Тем не менее мы внедрили параноидальную гигиену для гарантии воспроизводимости сборок. Один раннер пропускает через себя код пяти разных проектов, и файловый мусор от предыдущей сборки легко мог бы сломать следующую.

Мы изолировали гостевую систему через классический NAT (nic1=nat), закрыв ей доступ к локальной сети хоста. Перед каждым новым джобом скрипт безжалостно откатывал виртуальную машину к чистому снапшоту. Сам раннер запускался с флагом --ephemeral для одноразовой регистрации, а дополнительный скрипт Unregister-OrphanedRunner.ps1 вычищал зависшие записи через API.

Цена настоящей изоляции с кастомным супервизором

Снапшот чистой ВМ

Многие задаются вопросом, зачем было писать собственный скрипт-опрашиватель, если GitHub предлагает встроенные Organization Runners. Штатный механизм действительно позволяет раздавать доступ пулу машин для нескольких репозиториев. Он совершенно бесполезен для управления жизненным циклом самих виртуальных сред.

Флаг --ephemeral успешно удаляет раннер из пула после выполнения задачи. Он никак не восстанавливает файловую систему до исходного состояния. Чтобы обернуть каждую задачу в строгий цикл отката снапшота и запуска ВМ, нам все равно требовался внешний контроллер. Кастомный скрипт опроса REST API стал обязательной платой за чистую среду для каждой сборки.

Сторож мониторит самого себя

Сторож смотрит в зеркало

Единственный сервер стал критической точкой отказа для всего проекта. Любое зависание супервизора мгновенно парализовало проверки кода и публикацию документации. Первые пару недель скрипт работал в одиночестве и регулярно требовал ручного перезапуска, когда VirtualBox плодил зомби-процессы. Мы написали отдельный скрипт-сторож, обязанный автоматически перезапускать зависшего контроллера.

Логика была примитивной: если файл supervisor.log переставал обновляться, а процесс VBoxHeadless не висел в памяти, сторож дергал рубильник. Мы сами выстрелили себе в ногу в первый же день. Сторож усердно писал свои диагностические логи в тот же самый файл supervisor.log. Он постоянно обновлял время модификации файла, надежно блокируя условие собственной проверки.

Мы быстро разнесли логи по разным файлам, и система вроде бы заработала стабильно. На следующее утро сторож сошел с ума и принялся методично убивать абсолютно здоровый супервизор без видимых причин. Это привело нас к двухдневному детективному расследованию сетевого полтергейста, о котором мы расскажем в следующей части.