Docker 19.03: переводим CI на BuildKit и rootless-режим
Переходим на Docker 19.03 с BuildKit и rootless-сборкой на shared build-серверах. BuildKit ускоряет параллельные сборки, rootless снижает риски при компрометации агента.
Docker 19.03 включил BuildKit по умолчанию и вывел rootless-режим в экспериментальный статус: сборка образов без root-привилегий через user namespace
На одном из клиентских проектов в рамках managed-сопровождения у нас несколько shared build-серверов под GitLab CI. Конфигурация стандартная: runner с docker-executor, агенты крутятся на одних и тех же машинах, на которых параллельно собирается код нескольких команд. Docker 19.03 там стоял давно, но BuildKit и rootless мы держали выключенными - не было времени разбираться, что поменяется. В мае наконец дошли руки, и разбор занял несколько дней.
Зачем трогать то, что работает
Поводов два, и они независимые.
Первый - скорость. BuildKit переписывает движок сборки: параллельное выполнение независимых слоёв, умное кеширование, lazy-pull (тянет только нужные слои базового образа). На нашей кодовой базе с многоступенчатыми Dockerfile-ами время сборки упало заметно - примерно вдвое-втрое на образах, где несколько FROM-стадий не зависят друг от друга. Точную цифру не даём, потому что она сильно зависит от конкретного Dockerfile и наполнения кеша, но на hollow-сборке (кеш холодный) выигрыш самый очевидный.
Второй - безопасность. Стандартный docker daemon работает от root. Это значит, что контейнер сборки имеет доступ к unix-сокету /var/run/docker.sock, и если что-то пойдёт не так - образ-агрессор может монтировать хостовую файловую систему, запускать привилегированные контейнеры, делать всё что угодно. На shared build-серверах, где одновременно собирается код нескольких проектов, это неудобная реальность. Rootless Docker убирает демон из-под root: и демон, и контейнеры работают в user namespace текущего пользователя. Компрометация сборочного агента не даёт root на хосте.
BuildKit: что меняется на практике
Включить BuildKit в Docker 19.03 можно двумя способами. На уровне переменной среды для конкретного вызова:
DOCKER_BUILDKIT=1 docker build .
Или глобально через /etc/docker/daemon.json:
{
"features": {
"buildkit": true
}
}
Мы включили глобально - нет смысла держать два режима на одной машине.
Что реально изменилось в поведении:
- Вывод в терминал. BuildKit рисует прогресс по-другому - табличный вид с параллельными стадиями вместо линейного потока. CI-логи выглядят иначе, к этому нужно привыкнуть. Если есть парсинг docker output-а в скриптах - проверьте.
- Синтаксис Dockerfile. BuildKit поддерживает директиву
# syntax=docker/dockerfile:1.xв начале файла - это подключает расширенный синтаксис:RUN --mount=type=cache,RUN --mount=type=secret, heredoc вCOPY. Без этой директивы Dockerfile работает как раньше, ничего не ломается. - Кеш mount. Самое полезное новшество для наших сборок:
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt. Кеш pip (или npm, или cargo) живёт между сборками на уровне BuildKit, не запекается в слой образа. Итоговый образ не раздувается, повторные сборки проходят быстро даже когда зависимости не менялись.
Один нюанс, который стоит знать: BuildKit иначе обрабатывает аргументы --build-arg. Если ARG определён до первого FROM в многоступенчатом Dockerfile - он влияет на все стадии. Это соответствует спецификации, но старый движок мог вести себя по-другому. У нас один Dockerfile сломался именно на этом - ARG с именем окружения передавался в глобальной секции, и BuildKit применял его ко всем стадиям, включая те, где это не ожидалось. Починили за час.
Rootless: ставим и проверяем
Rootless Docker в 19.03 - экспериментальная фича. Это важно: она не включается через daemon.json, это отдельная установка через скрипт:
curl -fsSL https://get.docker.com/rootless | sh
Скрипт настраивает отдельный экземпляр dockerd в пространстве имён пользователя. Запускается через systemd --user, сокет лежит в /run/user/<uid>/docker.sock.
Что работает по-другому по сравнению с обычным Docker:
- Networking. Rootless Docker использует slirp4netns для сетевой изоляции - это user-space реализация сетевого стека. Производительность сети внутри контейнеров ниже, чем с обычным bridge. Для сборочных агентов, которые тянут пакеты из интернета или внутреннего registry, это ощутимо - pull занимает дольше.
- Проброс портов. Пробрасывать порты ниже 1024 нельзя - нет root, нет привилегированных портов. Для CI-сборки это обычно не проблема.
- Нет cgroup v1 ограничений. Без root нельзя ограничить CPU и memory для контейнера через cgroup v1. На хостах с cgroup v2 (Fedora 31+, некоторые rolling-дистрибутивы) это работает, на старых - нет. У нашего клиента Ubuntu 18.04, там cgroup v1, и ограничения по ресурсам для сборочных контейнеров не применяются. Это означает, что жадная сборка теоретически может съесть всю память машины.
Из-за последнего пункта мы не перевели все агенты на rootless немедленно. Оставили rootless на части агентов для проектов с предсказуемым потреблением памяти, остальные пока на обычном режиме.
Как выглядит схема сейчас
На shared build-серверах работает два пула GitLab Runner:
- Обычный docker-executor, BuildKit включён, rootless выключен. Сюда идут сборки с неизвестным или большим потреблением памяти.
- Rootless docker-executor, BuildKit включён. Сюда назначаем проекты, где нужна изоляция и потребление предсказуемо.
Разбивка по тегам раннеров: в .gitlab-ci.yml проставляем tags: [docker-rootless] там где это нужно.
Один открытый вопрос - docker-in-docker в rootless-режиме. Часть сборок использует dind для интеграционных тестов. В rootless это работает, но требует --privileged флага для внешнего контейнера, что убивает смысл rootless для этих конкретных job-ов. Пока там оставили обычный режим, исследуем альтернативы через kaniko и buildah - это уже отдельная задача.
Итого
BuildKit включили глобально, он работает стабильно уже три недели на всех агентах. Прирост скорости на параллельных многоступенчатых сборках - реальный и заметный. Rootless - в боевой эксплуатации на части агентов, ограничение с cgroup v1 держит нас от полного перехода до обновления хостовых ОС.