
Каждый пул‑реквест хочется не просто проверить тестами, а показать вживую — дизайнеру, менеджеру, соседней команде, чтобы все они смогли потыкаться в UI — порадоваться, понаходить баги — порасстраиваться. Казалось бы, что сложного: поднял окружение и дал ссылку. Но когда пул‑реквесты сыпятся десятками в день, а команд целое множество, «просто поднять окружение» превращается в отдельный инфраструктурный проект.
Привет, Хабр! Меня зовут Михаил Гольбах, я ведущий разработчик интерфейсов Yandex Cloud. Уже несколько лет мы с командой развиваем Ферму — сервис, который поднимает preview‑окружение под каждый пул‑реквест. В этой статье расскажу, как Ферма выросла из простого self‑hosted‑скрипта на одной виртуальной машине в оркестратор на Docker и Kubernetes и почему в итоге мы открыли её код.
Как мы пришли к первой версии Фермы
В Яндексе есть множество команд фронтенд‑разработки, в которых создаются сотни пул‑реквестов в день. Для каждого пул‑реквеста запускается множество тестов и иных проверок в CI. Юнит‑тесты, тайпчеки, линтеры свободно запускаются на изолированных виртуальных машинах.
Но что делать, когда нужно поднять полноценное приложение из ветки? Из этого вопроса вытекает довольно часто встречающаяся у фронтендеров задача поднятия «беток» на каждый пул‑реквест. Первое, что приходит в голову, — поднять определённое dev‑окружение по аналогии с продом и делить его между разработчиками. Так часто и поступают небольшие команды на начальных этапах, но весьма быстро они сталкиваются с проблемой масштабирования. Одно окружение уже не поделишь, да и e2e‑тесты так не попрогоняешь — их стабильность явно будет вызывать вопросы.
Традиционно каждая команда настраивает свой процесс для развертывания dev‑окружений, тратя на это определённые усилия и ресурсы на поддержку. Мы не оказались исключением и выработали собственное решение. Однако проектировали его сразу с целью сделать общую технологию, которую можно будет масштабировать на десятки команд, несмотря на возможные различия в инфраструктуре.
В далеком 2018 году в команде фронтенда Yandex Cloud появилась первая версия Фермы — по сути простого self‑hosted‑сервиса, который скачивал код из ветки репозитория, выполнял команды сборки целевого приложения и запускал его процесс на виртуалке с последующей маршрутизацией трафика с определённого домена. Даже сам набор команд был зашит в коде: Ферма была ориентирована в первую очередь на наши проекты, которые строятся на общих компонентах для веб‑сервисов на Node.js. Получилась своеобразная система упрощенного деплоя с яндексовой спецификой.
На протяжении многих лет сервис Фермы развивался: научился работать с отдельными конфигурациями сборки и запуска под каждый проект, обрел полноценный UI, базу данных и ещё много дополнительного кода, упрощающего работу с инфраструктурой Яндекса. Но суть сервиса не изменилась, он все так же решал следующие задачи:
скачивание кода и сборка проекта из пул‑реквеста;
маршрутизация трафика до запущенного приложения;
унификация решения под одним компонентом инфраструктуры, который легко можно распространять и встраивать в CI процессы Яндекса.
Принцип работы
Ферма представляет из себя небольшое Node.js‑приложение, упрощённая схема работы которого выглядит так:
Ферма получает запрос на генерацию. Он может прийти от таски в CI или напрямую от пользователя через форму в UI.
Новый инстанс приложения попадает в БД со статусом queued, вставая в очередь на сборку.
Ферма разбирает очередь, скачивает код из целевого репозитория и ветки, читает конфигурацию и запускает сборку.
Процесс приложения запускается: от него ожидается, что он запустит сокет по пути <instance_dir>/dist/server.sock.
-
Ферма, а точнее уже nginx со специфичным конфигом, начинает роутить трафик по уникальному домену, например: d008838956420e285cee652262dd5d18b2ed2a2a.farm.example.com.

Из схемы видно, что мы не перекладывали на Ферму ответственность за балансировку, а использовали nginx. Такой подход сохранится и в будущем: поддерживать проксирование трафика — нетривиальная задача, которую не хотелось бы решать. Также была поддержка Git и Arc — собственной системы контроля версий внутри Яндекса.
Сама сборка и запуск представляли из себя незамысловатый запуск команд самой Фермой. Все было максимально брутально и в лоб:
# Сборка npm ci npm run build # Запуск npm run start
Для полноты картины посмотрим ещё на пример конфига farm.json:
{ "preview-generator": { "build": ["npm ci", "npm run build"], "start": { "command": "npm", "args": "run start" }, "env": { "APP_BUILDER_CDN": "false", "IS_FARM": "true" }, "instances": [ { "name": "preprod", "urlTemplate": "https://{hash}.farm.example.com", "env": { "APP_INSTALLATION": "russia", "APP_ENV": "preprod" } }, { "name": "prod", "urlTemplate": "https://{hash}.farm.example.com", "env": { "APP_INSTALLATION": "russia", "APP_ENV": "prod" } } ] } }
По итогу получилась такая «рабочая лошадка». Целевой команде оставалось только подготовить виртуальную машину, описать конфигурацию самой Фермы и проектов, поднять её и настроить процесс в CI для пул‑реквестов. Звучит несложно, не правда ли? Но…
Рост и новые вызовы
В таком состоянии Ферма прожила достаточно долгое время и нормально решала поставленные перед собой задачи. Первоначально у нас даже была коммуналка — одна общая Ферма на несколько команд фронтенд‑разработки, чтобы не тратить лишние силы на поддержку нескольких инсталляций. Но команды росли в размере, их количество увеличивалось, а Ферма могла работать лишь в рамках одной виртуалки. Производительности даже мощных конфигураций со временем перестало хватать.
Выполнение нескольких сборок параллельно занимало все ресурсы машины, а инстанс мог стоять в очереди немалое время. Поэтому запустился органический процесс расселения коммуналки на более мелкие Фермы, иногда даже попроектно.
Очевидно, что расселение не решало проблему, а лишь откладывало её. Вычислительные ресурсы всё равно продолжали заканчиваться, а новым командам приходилось тратить дополнительные силы на поддержку и обслуживание Фермы. Например, довольно часто возникала ситуация заполнения диска на виртуалке, что заставляло запускать ручной процесс очистки ненужных файлов. Первая проблема — это ограниченные ресурсы и отсутствие их менеджмента и масштабирования. Запомним это.
Другая проблема заключалась в том, что Ферма под капотом использовала в качестве БД SQLite без каких‑либо абстракций по типу Knex. Это порождало vendor lock, а также заставляло сносить все данные при изменении их схемы, поскольку не было механизма миграций. С учётом того, что Ферма была разделена на множество инсталляций по командам, это приводило ещё и к тому, что мало кто хотел обновлять её версию: кто знает, что там сломается, да ещё и данные, скорее всего, придётся сносить. Как говорится, «работает — не трожь».
К тому же Ферма умела запускать только Node.js‑приложения, причём все процессы запускались напрямую на хосте без какой‑либо изоляции. А раз изоляции нет, приложения не могли просто использовать свой привычный порт для получения трафика, так как Ферма не занималась менеджментом портов. По этой причине для коммуникации использовали unix‑сокеты, что заставляло приложение соблюдать этот немного странный контракт.
Итак, нам нужно было решить следующие проблемы:
Ограничение ресурсов и отсутствие их менеджмента и масштабирования.
Отсутствие миграций БД и завязка на SQLite.
Отсутствие изоляции инстансов и ограничение в виде Node.js‑приложения и публикации unix‑сокета.
Сложность поддержки и эксплуатации, ведь изначально мы хотели упростить жизнь командам, а не добавлять головной боли (это дополнительная проблема, которую мы пока вынесем за скобки, но вернемся к ней ближе к концу).
Как мы справлялись с этим
Опытный инженер, смотря на данные задачи, скорее всего предложит перевести Ферму на рельсы k8s‑кластера. Так и мы плавно пришли к этой идее: перевести сборку и развертывание приложения на мощности кластера в отдельные поды и использовать его возможности для автоматического масштабирования и менеджмента ресурсов. Сами приложения в свою очередь будут поставляться в виде всем привычного docker‑контейнера — очень понятный и простой контракт для нашей индустрии. Причём нам ещё требовалось сохранить возможность запускать приложения в привычном виде, как процессы, чтобы небольшим командам не пришлось заниматься трудоемкой миграцией на k8s решение.
Просто отказаться от Фермы и перейти на прямой деплой приложения из CI в k8s‑кластер было невозможно по следующим причинам:
Ферма уже глубоко проросла во все наши текущие процессы и CI. Заставлять команды мигрировать на абсолютно новое решение было нецелесообразно.
Если углубляться в детали, то Ферма занимается не только сборкой и запуском приложения, она ещё берёт на себя ответственность за маршрутизацию (пусть и через nginx), а также останавливает и удаляет неиспользуемые инстансы и делает многое другое.
А ещё немаловажно наличие удобного UI, где инстанс определённого приложения могут поднять не только фронтендеры, но и бэкендеры, чтобы потестировать различные фичи.
На деле добавить поддержку k8s оказалось сложнее, чем думалось изначально. С первых версий логика работы с инстансами приложений была очень разбросана по кодовой базе, и за много лет внутри этой логики появилось множество деталей и изящных костылей — добавить рядом ещё одну реализацию было затруднительно.
Основательный рефакторинг
Первым шагом мы затеяли крупный рефакторинг кода Фермы. Сперва нужно было инкапсулировать логику работы с инстансами в отдельный класс провайдера — условного бэкенда нашей Фермы, который бы определял стратегию сборки и развертывания приложения. Это бы позволило в моменте поддержать legacy‑схему и k8s вместе, а в дальнейшем добавлять новые реализации через единый интерфейс.
Подербанив код, получилось выделить все специфичные для провайдера методы и подготовить абстрактный класс для его описания. Немного упрощая, всё свелось к такому коду:
class BaseFarmProvider { startup(): Promise<void>; buildInstance( generateData: GenerateInstanceData, observer: SubscriptionObserver<InstanceObservableEmitValue>, ): Promise<void>; stopBuilder(hash: string): Promise<void>; startInstance(instance: Instance): Promise<void>; stopInstance(hash: string): Promise<void>; restartInstance(instance: Instance): Promise<void>; deleteInstance(hash: string): Promise<void>; getInstanceStatus(instance: Instance): Promise<InstanceProviderStatus>; getInstances(): Promise<Array<InstanceProviderInfo>>; getInstanceLogs(params: { hash: string; stdout?: LogParams; stderr?: LogParams; }): Promise<{stdout?: string; stderr?: string}>; }
Примерно в этот момент мы абстрагировали и всю работу с БД, и перевели её на Knex, чтобы иметь возможность в будущем использовать другую СУБД, более мощную и персистентную. Заодно и получить дешёвый механизм миграций для упрощения внесений изменений в схему базы данных.
Новая схема с Kubernetes
В новой схеме Ферма должна быть развернута в кластере, работать в виде контроллера, подключаться к k8s API и начинать управлять ресурсами кластера: запускать поды для сборки, для приложения, создавать и удалять другие ресурсы.
Чтобы понять её, достаточно покрыть два основных процесса: сборку и развертывание инстанса приложения.
Сборка состоит из нескольких этапов:
Пользователь или CI запускает сборку (метод buildInstance).
Из ветки получается конфигурация Фермы для целевого приложения (файл farm.json из корня репозитория).
Запускается под билдера, который скачивает код из ветки, собирает docker‑образ (по умолчанию Dockerfile.farm) и пушит его в заранее определённый реестр.
Все логи из пода билдера стримятся в реальном времени и отображаются в UI для отладки.
-
После завершения сборки Ферма удаляет под билдера.

На выходе сборки получается артефакт в виде запушенного в реестр образа приложения, который готов к развёртыванию и запуску. Плюсом мы получили фичу кеширования сборок при помощи Docker, что очень полезно для ускорения CI.
Развертывание запускается сразу после сборки, в том же методе buildInstance, так что продолжаем разбирать процесс с момента пуша образа:
Создается Deployment в кластере, где используется образ, полученный на прошлом этапе.
Создаются Service с портом, который мы указали в конфигурации Фермы, и Ingress с доменом под ID инстанса.
-
Дальше в игру вступает новый игрок — Ingress NGINX Controller, он занимается всем роутингом и делает приложение доступным извне. Тут Ферма остается верной себе, перекладывая ответственность за проксирование трафика на другой компонент системы.

Технически, для роутинга можно использовать любой другой Ingress‑контроллер, вплоть до какого‑нибудь облачного ALB, если потребуется. Выбор пал на Ingress NGINX Controller из‑за простоты работы и скорости обновления конфигов.
Примерно так мы и получаем рабочий инстанс приложения, готовый к тому, чтобы на него можно было заходить и тестировать функционал. Дополнительно Ферма следит за тем, чтобы ресурсы не дублировались и все операции были идемпотентными, — это очень важно для стабильной работы.
Упрощенная схема чистым Docker
После появления идеи с поддержкой k8s в Ферме родилось и достаточно очевидное предложение — почему бы не поддержать вариант запуска инстансов просто на виртуальной машине в Docker? Это бы позволило решить проблему изоляции инстансов и снять ограничение на Node.js‑приложение и использование портов, а заодно дало бы Ферме ещё один режим работы, но уже для небольших команд без k8s‑кластера. В дальнейшем можно было бы полностью избавиться от старого способа запуска в виде обычных процессов Node.js, что сильно упростило бы поддержку, позволив удалить кучу легаси.
Как это в итоге работает? Ферма подключается к Docker Engine по сокету и сама же выступает в роли билдера — никаких отдельных подов под сборку тут нет. И вот первое важное отличие от k8s‑схемы: образ инстанса собирается локально, на том же демоне, и там же остаётся жить. Пушить его в реестр не нужно — реестр пригождается разве что для того, чтобы подтянуть базовые образы во время сборки. Получается заметно проще, а это ровно то, что нужно небольшим командам без кластера.
При этом мы заложили два режима работы:
docker_container — сама Ферма запущена как контейнер с примонтированным сокетом хоста и дирижирует «соседними» контейнерами на том же демоне;
vm — Ферма живёт прямо на виртуалке со своим Docker Engine.
И Ферма, и все инстансы подключаются к общей Docker‑сети (по умолчанию farm), а каждый инстанс приложения получает контейнер с говорящим именем farm‑docker‑<hash>. Именно по этому имени мы потом и находим его, когда дело дойдёт до трафика.
Как и в k8s‑схеме, сборка и запуск уживаются в одном методе buildInstance, только без разделения на отдельные поды:
Пользователь или CI запускает сборку (метод buildInstance).
Из ветки выкачивается код и читается конфигурация Фермы из farm.json — оттуда берётся путь до Dockerfile, а также переменные для сборки и запуска.
Ферма локально собирает docker‑образ и вешает на него тег farm‑docker‑ <hash>, попутно стримя все логи сборки в UI для отладки.
-
Из готового образа создаётся и запускается контейнер в сети farm — с проброшенными переменными окружения и, при желании, переопределённой командой запуска.

Маршрутизация трафика
Здесь Ферма снова перекладывает проксирование на nginx. Контейнер инстанса не публикует никаких портов на хост — достучаться до него можно по имени farm‑docker‑<hash> внутри Docker‑сети farm, где это имя резолвит встроенный DNS Docker.
Вопрос лишь в том, где сидит тот самый nginx, который это имя умеет разрешать, — а это уже зависит от режима:
В режиме docker_container запрос с хоста уходит в контейнер Фермы, и её внутренний nginx, и в сети farm доводит трафик до инстанса напрямую.
-
В режиме vm nginx Фермы крутится прямо на хосте и имён контейнеров не видит, поэтому проксирует запрос в отдельный контейнер Docker Proxy (тот же nginx, но поднятый внутри сети farm), а уже он отдаёт трафик нужному инстансу.

Так мы и получаем рабочий инстанс на обычной виртуальной машине с Docker, без всякого кластера: код собирается в образ, образ превращается в контейнер, а трафик до него доводит nginx.
Заодно мы решили и проблему изоляции с тем самым странным контрактом про unix‑сокеты — теперь приложение поставляется привычным docker‑образом и спокойно слушает свой порт, ничего не зная о внутренней кухне Фермы.
Эволюция Фермы и вынос в опенсорс
После всех улучшений, добавления абстракций над провайдерами, поддержки разных схем сборки и запуска приложений и перехода на Knex Ферма выглядела уже как полноценный и зрелый продукт, в котором специфика Яндекса не играет большой роли. Таким образом Ферма постепенно стала оркестратором без жесткой привязки к технологиям, которые запускала под собой.
Она уже напоминала упрощенную систему деплоя по типу Heroku, только в виде self‑hosted‑сервиса. Поэтому появились и более амбициозные планы — опубликовать код Фермы в рамках нашего опенсорс‑проекта Gravity UI, чтобы она приносила пользу не только внутри компании, но и всему сообществу.
Но у красивой идеи с открытием кода была одна загвоздка. Ферма хоть и стала более абстрактной и чистой, но много яндексовой специфики все ещё оставалось: внутренняя аутентификация, работа с Arc, постинг статусов в наш трекер, отправка телеметрии и прочие мелочи, завязанные на внутреннюю инфраструктуру. Выкладывать всё это наружу не стоит, да и сообществу оно без надобности. Но и просто вырезать тоже не вариант — на этой специфике живут наши собственные инсталляции, которыми пользуются десятки команд каждый день.
Поэтому задача сформулировалась так: вынести всё яндексовое за скобки и выделить из Фермы конфигурируемое ядро, в которое специфику можно было бы вернуть снаружи, не трогая сам код ядра. По сути нам нужно было провести ещё одну границу между тем, что мы готовы открыть всему миру, и тем, что остаётся жить только внутри.
В итоге мы физически разделили Ферму на две кодовые базы.
Открытое ядро (core): вся оркестрация, провайдеры Docker и k8s, работа с Git, база данных на Knex, API, UI, очередь сборок, healthcheck — словом, всё, что и составляет Ферму как продукт. Ядро ничего не знает про Яндекс и спокойно может быть опубликовано как есть.
Тонкий слой расширений, который досыпает в ядро всё проприетарное.
Связующим звеном стал реестр плагинов: ядро поднимает себя и предоставляет единую точку входа, через которую слой расширений регистрирует свои реализации по понятным интерфейсам. В итоге вся яндексовая часть теперь живёт в одном небольшом модуле и подключается декларативно:
initCoreExtension(async () => { // свой способ запуска инстансов coreRegistry.farmProviders.plugIn('process', { constructor: ({internalApi, config}) => new ProcessFarmProvider(internalApi, config), }); // своя система контроля версий coreRegistry.vcs.plugIn('arc', {constructor: () => new ArcVcs()}); // свой экшен на вебхук — например, постинг статуса в трекер coreRegistry.webhookActions.plugIn('tracker', new TrackerWebhookAction()); // своя аутентификация coreRegistry.authProviders.plugIn('internal', {constructor: () => new InternalAuthProvider()}); // ...и далее телеметрия, CSP-домены, пункты меню в UI });
Реестр устроен как набор отдельных точек расширения со своими интерфейсами — через них наружу вынесено всё, что может меняться:
провайдеры (farmProviders) — как и куда деплоить;
системы контроля версий (vcs) — открытый git, внутренний arc;
аутентификация (authProviders) — как пускать пользователей в UI и API;
экшены на вебхук (webhookActions) — что сделать в ответ на событие из CI, например отписаться статусом в трекер;
схема farm.json (farmJsonConfig) — свои поля в конфигурации приложения под нужды конкретного провайдера;
UI и безопасность (uiConfiguration, cspDirectives) — пункты меню и список доверенных доменов в CSP.
Ключевой момент в том, что для ядра все эти реализации равноправны: arc для него ничем не отличается от git, а process — от docker или k8s. Поэтому ядро можно открывать, не оглядываясь на внутреннюю кухню, а наша инсталляция продолжает работать как раньше — просто собирая ядро вместе со слоем расширений.
Внедрение Фермы на k8s
Пройдя долгий путь, мы наконец подошли к тому, чтобы переводить наши инсталляции Фермы на k8s. Выбрали такую стратегию: переводим самую крупную инсталляцию на рельсы Kubernetes, а дальше, если всё будет хорошо работать, возрождаем коммуналку, переводя остальные проекты с локальных Ферм на одну общую. Такой подход как раз и позволяет решить проблему сложности поддержки, которую мы обозначали в начале. Командам теперь снова не нужно будет думать о том, чтобы содержать свою собственную инсталляцию и следить за её ресурсами, ведь общий кластер автоматически масштабируется при нагрузке.
Мы можем свободно использовать свои же сервисы и облачные услуги для построения инфраструктуры. Так мы подняли кластер при помощи Yandex Managed Service for Kubernetes® и описали абсолютно все ресурсы через Terraform. К тому же настроили раздельное управление секретами для каждого проекта с атомарным назначением прав доступа на каждый ресурс. В итоге получился переиспользуемый Terraform‑модуль, с помощью которого можно весьма просто развернуть k8s‑инсталляцию Фермы.
Одним днем мы успешно перетащили все текущие проекты на k8s инсталляцию Фермы. Это было не сильно сложно, так как изначальный контракт и конфиги сохранялись. Новая Ферма показывала себя весьма неплохо, и другие проекты тоже начали переезжать на неё. Достаточно быстро всплыла новая проблема — хоть кластер и масштабируется по RAM и CPU, но память на диске всё равно продолжала забиваться, так как каждый день собиралось немало образов, что занимало место на дисковом пространстве.
Тут мы решили проблему «в лоб» — добавили механизм клинеров:
для Docker он работает так: задается определённое время в виде cron‑выражения, когда нужно удалить неиспользуемые образы;
а для k8s этот механизм построен на базе нативного ресурса CronJob, по которому на каждом узле запускаются поды, тоже удаляющие старые образы.
Мы признали эксперимент с Фермой на k8s успешным, основываясь на отклике команд: отзывам, количеству проблем и обращений к мейнтейнерам.
Резюме по возможностям Фермы
За этот путь Ферма из «рабочей лошадки» под один стек выросла в зрелый оркестратор preview‑окружений. Коротко резюмирую, что она умеет сейчас:
Сборка и запуск из ветки. По вебхуку или из UI Ферма забирает код, собирает приложение и поднимает изолированный инстанс с собственным URL.
Несколько провайдеров. k8s — для крупных инсталляций с горизонтальным масштабированием, docker — для одной виртуальной машины без кластера. Контракт приложения общий для всех провайдеров.
Конфигурируемое ядро с реестром плагинов. Провайдеры, системы контроля версий, аутентификация и экшены на вебхук подключаются через единый реестр, что позволяет расширять Ферму, не меняя её ядро.
Планировщик и очередь сборок. Запросы на генерацию встают в очередь, а планировщик разбирает её с учётом лимитов на количество одновременных сборок и запущенных инстансов.
Управление жизненным циклом. Автоматический старт инстанса из UI, а также остановка и удаление простаивающих инстансов по таймауту.
Healthcheck. Ферма следит за состоянием инстансов и отражает их актуальный статус в UI и API: в Docker и при запуске процессами — собственными проверками доступности, в Kubernetes — через нативные liveness/readiness‑пробы.
Проброс env‑переменных. Гибкая настройка окружения: переменные сборки и рантайма, защищённые переменные, которые нельзя переопределить при генерации, а для Docker — ещё и наследование переменных хоста или контейнера.
Персистентная БД с миграциями. Knex поверх SQLite, PostgreSQL или другой СУБД.
UI и API. Веб‑интерфейс для запуска и отладки инстансов и HTTP API для интеграции в CI.
Клинеры дискового пространства. Регулярная уборка неиспользуемых образов в Docker и k8s.
Что дальше
Теперь о планах. Есть ещё много вещей, которые хотелось бы улучшить в Ферме. И вот основные направления на данный момент:
Переход на Gateway API на замену Ingress для k8s провайдера.
Поддержка алиасов, чтобы на инстанс можно было заходить не по хешу, а по человекочитаемому названию.
Полноценная поддержка авторизации — система пользователей, ролей и прав доступа.
Раздельные квоты для разных проектов.
Как и с любым другим опенсорс‑проектом, мы будем развивать документацию и примеры, чтобы Фермой было пользоваться проще и удобнее и количество внешних пользователей стремительно росло. Хотелось бы, чтобы со временем Ферма стала одним из привычных инструментов для preview‑окружений в индустрии — но тут уже как пойдёт.
Как попробовать
Вам на проекте тоже нужна система для поднятия preview‑окружений? Смело приходите к нам в репозиторий, изучайте README и соответствующую документацию. Для теста можете свободно попробовать поднять Ферму на локалке с установленным Docker Engine. Мы будем благодарны любой обратной связи, так что заводите issue, присылайте ПРы, задавайте вопросы и приносите feature requests, если вам нужен какой‑то дополнительный функционал!
Спасибо за внимание! Если вам пришелся по душе наш проект, то будем рады заёздочкам! Если вам интересно узнавать все самые актуальные новости жизни команды Yandex Cloud, то присоединяйтесь к нашему каналу.