ADG Оставить заявку
Блог DevOps 5 мин чтения

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 держит нас от полного перехода до обновления хостовых ОС.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.