Привет! Меня зовут Слава Ишутин, я инженер в Авито, занимаюсь внутренней инфраструктурой. Мы проводим много UI- и unit-тестов. Чтобы ускорить их прогон, написали тест-раннер Emcee. Он сам организует очередь из тестов, которые распределяет между машинами так, чтобы ни одна из них не простаивала. В конце даёт аналитику и показывает, сколько было успешных и проваленных тестов.
Emcee можно скачать с репозитория GitHub и развернуть на своих серверах. В этой статье я хочу рассказать, как мы запустили Emcee Cloud на мощностях Авито для тестирования приложений других компаний. Запуск стороннего кода на наших серверах ставит много вопросов по безопасности. Очевидный способ решить эти проблемы — сендбоксинг с помощь виртуализации. Реализовать это на macOS оказалось нетривиально и нам пришлось поломать голову. Рассказываю, что мы сделали, чтобы обезопасить внутреннюю сеть и данные пользователей.
Попробовали самый простой инструмент
Для запуска виртуализации есть масса решений, в том числе и для macOS. Компания Apple предоставляет фреймворк Virtualization с высокоуровневым API для создания и конфигурации виртуальных машин. Чтобы его использовать, нужно разработать свой формат виртуальной машины, где нужно сохранять конфигурацию, диск виртуальной машины, nvram. Такие подготовленные виртуальные машины мы называем образами и используем для клонирования. Надо озаботиться хранением этих образов и доставкой их на машины тестовой фермы. Каждый подготовленный образ с установленным Xcode и Simulator рантаймами для наших целей занимает около 60 Гб. Для прототипа запуска Emcee в облаке мы стали рассматривать готовые решения.
В коммерческих продуктах Parallels, VMWare, UTM основной упор сделан на то, чтобы пользоваться виртуальными машинами из оконного интерфейса, как ОС внутри ОС. Например, чтобы виртуализировать Windows или Linux на macOS. Виртуальные машины там можно бекапить и откатывать состояния, приостанавливать и возобновлять работу, подключаться к ним по vnc. В задаче автоматизации тестирования мы этим не пользуемся, мы будем клонировать подготовленную виртуальную машину и запускать один раз на время выполнения одной задачи, а после этого её удалять. Зато нам важна возможность управлять виртуальными машинами через rest api или cli. Популярные инструменты нам не подходили и по другим причинам:
Сложно настроить автоматизированную установку приложения на macOS-хост (мы используем Puppet). Эти приложения дистрибутируются как dmg-образы или установщики, их нельзя установить с помощью homebrew или macports.
Все они имеют пользовательский интерфейс и несут с собой много ресурсов для его отображения. Хоть в некоторых решениях есть возможность запускать виртуальную машину в безоконном режиме, всё равно само приложение занимает много места. Например, UTM занимает ~1 Гб.
Нет удобного функционала для хранения и распространения образов. У нас несколько образов под разные окружения, которые мы регулярно обновляем. Каждое решение использует свой уникальный формат образа, и чтобы их применить в задаче автоматизации, надо дополнительно реализовать хранилище образов с версионированием.
Поэтому для быстрого старта мы стали искать другие варианты и остановились на утилите Tart. По сути, это простая cli-обёртка для фреймворка виртуализации на macOS. Проект с открытым исходным кодом, есть заранее подготовленные образы, в том числе с предустановленным Xcode. Есть образы с бета-версиями macOS и Xcode. Например, можно проверить работу своего CI на новой версии macOS/iOS в виртуальной машине без изменения окружения хоста. Запустить виртуальную машину очень легко:
tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
tart run ventura-base
Один из значимых плюсов для нас — возможность конвертации образа виртуальной машины в oci-совместимый образ. Это позволяет использовать любой репозиторий docker-образов вроде docker hub или GitHub packages. Мы просто загружаем и скачиваем образы в наш внутренний репозиторий, который уже используется в Авито Linux-сообществом. Недостаток такого подхода в том, что образ виртуальной машины — это всегда один уникальный слой в oci-образе. Получается, что слои не шарятся между образами и каждый новый образ, даже с минимальными изменениями, будет загружаться целиком и занимать полный объём в хранилище.
Запустили виртуальную машину и провели тесты
Мы изолируем запуск тестов не только от внутренней инфраструктуры Авито, но и клиентов облака друг от друга. У фреймворка на macOS есть ограничение: одновременно можно запустить не более двух виртуальных машин. Конечно, хотелось бы больше, но лимит зашит внутри операционной системы. В пограничных случаях мы можем не до конца использовать ресурсы хоста из-за наличия такого ограничения.
Запуск проходит в четыре этапа:
Сначала клонируем образ с нужным окружением из нашего репозитория образов. В нём уже всё настроено: скопирован публичный ключ для доступа по SSH, установлен Xcode, необходимые рантаймы симулятора и сам Emcee binary.
tart clone registry.avito.ru/emcee/ios/emcee:latest emcee-test-runner
Готовим временную директорию, где размещаем бандлы xctest, target, runner и другие ресурсы, которые нужны для тестирования. Затем запускаем виртуальную машину, в которую монтируем созданную директорию. Возможность монтировать директорию появилась только в macOS 13.0 Ventura.
tart run emcee-test-runner \ --dir="834efdb6-6044-4b44-8fcb-560710936f37:/path/to/tmp/dir"
Получаем IP-адрес запущенной виртуальной машины и проверяем её доступность. Иногда на это требуется несколько секунд, пока dhcp-демон выделит сетевой адрес.
tart ip emcee-test-runner --wait 15
Запускаем наш тест-раннер Emcee внутри виртуальной машины по SSH от пользователя _emcee. При запуске передаём идентификатор работы — так раннер сможет найти временную директорию, в которой мы уже разместили все ресурсы. После этого Emcee запускает тесты так, как будто работает на обычной машине.
ssh -i ~/.ssh/emcee _emcee@192.168.64.2 \ 'JOB_ID=834efdb6-6044-4b44-8fcb-560710936f37 bin/Emcee runTests ...'
Изолировали трафик из виртуальной сети
Тестовые бандлы — это упакованные binary с ресурсами, переданные клиентами в emcee cloud. Запускать приложения, которые передала третья сторона, небезопасно. Поэтому нам нужно изолировать ещё и трафик из сети виртуальных машин. В фреймворке Virtualization есть три способа, как это сделать:
Подключить сетевое устройство виртуальной машины напрямую к физическому интерфейсу host OS, например, en0.
Создать виртуальный коммутатор, который объединит все машины вместе с хостом в виртуальную сеть.
Так называемая software network, когда весь трафик из виртуальной машины приходит на socket в host OS. Как будет обрабатываться трафик, решает разработчик, который встраивает фреймворк в своё приложение.
Для изоляции трафика первый способ не подходит, это наименее безопасный способ подключения виртуальной машины к сети. В третьем надо реализовать обработку низкоуровневых сетевых пакетов, это трудоёмко и не очень производительно. Самый подходящий вариант — создать виртуальный коммутатор. Так мы сможем контролировать трафик через настройки packet filter на macOS.
Что есть на схеме:
VM1 и VM2 — виртуальные машины;
vmenet0 и vmenet1 — интерфейсы виртуального коммутатора, куда "подключены" VM1 и VM2;
bridge100 — это виртуальный интерфейс, к которому подключён host OS;
en0 — внешний сетевой интерфейс.
Фактически host OS видит и внешний интерфейс, и виртуальный, который подключён к виртуальному коммутатору. При этом host OS выступает в роли роутера, а трафик во внешнюю сеть транслируется через en0 с помощью NAT.
Ограничили виртуальные машины от внутренней сети
Если говорить о Linux, то здесь всё просто. В семействе этих ОС есть фаервол, настройки которого известны любому сетевому инженеру. Достаточно использовать команды к iptables, например:
iptables -A FORWARD -d 10.0.0.0/8 -j DROP
Подобным образом мы ограничиваем контейнеры для Linux-хостов, когда запускаем тесты на Android.
В macOS всё устроено по-другому. Здесь фильтрация пакетов идёт через packet filter, который сложнее в настройке. В него можно добавлять собственные правила и таким образом ограничивать исходящий трафик, чтобы он не шёл во внутренние приватные сети. В нашей конфигурации настройка packet filter выглядит так:
Создаём таблицу с приватными сетями:
pfctl -t rfc1918 -T add 192.168.0.0/26 172.16.0.0/12 10.0.0.0/8
Создаём отдельную группу правил в /etc/pf.anchors/ru.avito, где блокируем доступ из виртуальной сети в любую приватную сеть. Чтобы доступ к самой виртуальной машине работал по SSH, надо разрешить ответы внутри установленного соединения:
block drop in on bridge100 from any to <rfc1918>
pass out on bridge100 all flags S/SA keep state
Загружаем в /etc/pf.conf отдельную группу правил:
anchor "ru.avito"
load anchor "ru.avito" from "/etc/pf.anchors/ru.avito"
Применяем изменения:
pfctl -f /etc/pf.conf
Теперь о том, как мы изолируем трафик в Emcee cloud. Работа системы разбита на микросервисы:
Очередь EmceeQueue — центральный сервис системы, который делит тесты на бакеты, раздает их агентам и принимает результаты.
Агенты Emcee Agent — набор хостов macOS, которые получают бакеты с тестами для исполнения. Именно агент запустит виртуальную машину и тесты в ней. Есть ещё Linux-хосты, которые работают по аналогичной схеме.
Бэкенд сервиса, который обслуживает запросы фронтенда на создание нового запуска тестов или получение статуса уже существующего.
Вспомогательные сервисы — например, Graphite для аналитики, инстанс S3 для хранения тестовых бандлов и артефактов.
Сам Emcee Agent должен иметь доступ ко внутренней сети, а виртуальная машина, запущенная на этом же агенте, должна иметь доступ только в интернет. Для этого мы создали правило, которое ограничивает трафик ко всем приватным IP-сетям, описанным в rfc1918. В то же время процесс Emcee Agent имеет доступ к очереди Emcee Queue, находящейся во внутренней сети, может получить бакет в работу, скачать тестовые бандлы с S3 и отправить данные аналитики в Graphite.
В интернете мало информации по настройке packet filter, мануал сложный для понимания, поэтому решить эту задачу непросто. Мы потратили много времени, прежде чем найти рабочее решение, и теперь рады поделиться им с вами. Надеюсь, что статья была полезна.
Предыдущая статья: Построение платформенного продукта в Авито