Docker-in-Docker представляет собой виртуализированную среду Docker-демон, запущенную в самом контейнере для сборки образов контейнера. Основной целью создания Docker-in-Docker была помощь в разработке самого Docker. Многие люди используют его для запуска Jenkins CI. Поначалу это кажется нормальным, но затем возникают проблемы, которых можно избежать, установив Docker в контейнер Jenkins CI. В этой статье рассказывается, как это сделать. Если вас интересует итоговое решение без подробностей, просто прочитайте последний раздел статьи «Решение проблемы».
Docker-in-Docker: «Хороший»
Более двух лет назад я вставил в Docker
- hackity hack;
- сборка (build);
- остановка запущенного Docker-демон;
- запуск нового Docker-демон;
- тестирование;
- повтор цикла.
Если же вы хотели сделать красивую, воспроизводимую сборку (то есть в контейнере), то она становилась более замысловатой:
- hackity hack;
- убедиться в том, что запущена работоспособная версия Docker;
- собрать новый Docker со старым Docker;
- остановить Docker-демон;
- запустить новый Docker-демон;
- протестировать;
- остановить новый Docker-демон;
- повторить.
С появлением Docker-in-Docker процесс упростился:
- hackity hack;
- сборка + запуск в один этап;
- повтор цикла.
Не правда ли, так гораздо лучше?
Docker-in-Docker: «Плохой»
Однако, вопреки распространенному мнению, Docker-in-Docker не состоит на 100% из звездочек, пони и единорогов. Я имею в виду, что существует несколько проблем, о которых разработчику нужно знать.
Одна из них касается LSM (модулей безопасности Linux), таких как AppArmor и SELinux: при запуске контейнера «внутренний Docker“ может попытаться применить профили безопасности, которые будут конфликтовать или запутывать „внешний Docker“. Это самая сложная проблема, которую нужно было решить при попытке объединить исходную реализацию флага –privileged. Мои изменения работали, и все тесты тоже бы прошли на моей машине Debian и тестовых виртуальных машинах Ubuntu, но они бы рухнули и сгорели на машине Майкла Кросби (насколько я помню, у него была Fedora). Я не могу вспомнить точную причину проблемы, но возможно, она возникала потому, что Майк — мудрый человек, который работает с SELINUX=enforce (я использовал AppArmor), и мои изменения не учитывали профили SELinux.
Docker-in-Docker: «Злой»
Вторая проблема связана с драйверами хранилища Docker. Когда вы запускаете Docker-in-Docker, внешний Docker работает поверх обычной файловой системы (EXT4, BTRFS или любой другой, которой вы располагаете), а внутренний Docker работает поверх системы копирования при записи (AUFS, BTRFS, Device Mapper и т. д., в зависимости от того, что настроен использовать внешний Docker). При этом возникает множество комбинаций, которые не будут работать. Например, вы не сможете запускать AUFS поверх AUFS.
Если вы запускаете BTRFS поверх BTRFS, сначала это должно работать, но как только появятся вложенные подразделы, удалить родительский подраздел parent subvolume не удастся. Модуль Device Mapper не имеет пространства имен, поэтому, если несколько экземпляров Docker используют его на одной машине, все они смогут видеть (и влиять) на образы друг на друга и на устройства резервного копирования контейнеров. Это плохо.
Есть обходные пути для решения многих из этих проблем. Например, если вы хотите использовать AUFS во внутреннем Docker, просто превратите папку /var/lib/docker в том, и все будет в порядке. Docker добавил некоторые базовые пространства имен к целевым именам Device Mapper, так что если несколько вызовов Docker будут выполняться на одной машине, они не станут «наступать» друг на друга.
Тем не менее, такая настройка совсем не проста, как можно увидеть из этих
Docker-in-Docker: становится еще хуже
А как насчет кэша сборки? Это тоже может быть довольно сложно. Люди часто спрашивают меня “если я запускаю Docker-in-Docker, как я могу использовать образы, расположенные на моем хосте, вместо того, чтобы снова вытаскивать все в моем внутреннем Docker”?
Некоторые предприимчивые люди пытались привязать /var/lib/docker из хоста в контейнер Docker-in-Docker. Иногда они совместно используют /var/lib/docker с несколькими контейнерами.
Хотите повредить данные? Потому что это именно то, что повредит ваши данные!
Docker-демон явно был разработан для того, чтобы иметь эксклюзивный доступ к /var/lib/docker. Ничто другое не должно «касаться, тыкать или щупать» любые файлы Docker, находящиеся в этой папке.
Почему это так? Потому что это результат одного из самых трудных уроков, полученных при разработке dotCloud. Контейнерный движок dotCloud работал, имея несколько процессов, одновременно обращающихся к /var/lib/dotcloud. Хитрые трюки, такие как атомарная замена файлов (вместо редактирования на месте), «перчение» кода рекомендательными и обязательными блокировками и другие эксперименты с безопасными системами, такими как SQLite и BDB, срабатывали не всегда. Когда мы переделывали наш контейнерный движок, который в конечном итоге превратился в Docker, одним из главных дизайнерских решений было собрать все операции с контейнерами под единственным демоном, чтобы покончить со всей этой чепухой одновременного доступа.
Не поймите меня неправильно: вполне возможно сделать что-то хорошее, надежное и быстрое, что будет включать в себя несколько процессов и современное параллельное управление. Но мы думаем, что проще и легче писать и поддерживать код, используя Docker в качестве единственного игрока.
Это означает, что если вы разделяете каталог /var/lib/docker между несколькими экземплярами Docker, у вас будут проблемы. Конечно, это может сработать, особенно на ранних стадиях тестирования. «Слушай, Ма, я могу «докером» запустить ubuntu!» Но попробуйте сделать что-то более сложное, например, вытащить один и тот же образ из двух разных экземпляров, и вы увидите, как пылает мир.
Это означает, что если ваша система CI выполняет сборки и пересборки, то каждый раз при перезапуске контейнера Docker-in-Docker вы рискуете сбросить в его кэш ядерную бомбу. Это совсем не круто!
Решение проблемы
Давайте сделаем шаг назад. Вам действительно нужен Docker-in-Docker или вы просто хотите иметь возможность запускать Docker, а именно собирать и запускать контейнеры и образы из вашей системы CI, в то время как сама эта система CI находится в контейнере?
Держу пари, что большинству людей нужен последний вариант, то есть они хотят, чтобы система CI, такая как Jenkins, могла запускать контейнеры. И самый простой способ сделать это — просто вставить сокет Docker в ваш CI-контейнер, связав его с флагом -v.
Проще говоря, когда вы запускаете свой CI- контейнер (Jenkins или другой), вместо того, чтобы взламывать что-то вместе с Docker-in-Docker, начните его со строки:
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
Теперь этот контейнер будет иметь доступ к сокету Docker и, следовательно, сможет запускать контейнеры. За исключением того, что вместо запуска “дочерних” контейнеров он будет запускать “родственные” контейнеры.
Попробуйте это, используя официальный образ docker (который содержит двоичный файл Docker):
docker run -v /var/run/docker.sock:/var/run/docker.sock
-ti docker
Это выглядит и работает как Docker-in-Docker, но это не Docker-in-Docker: когда этот контейнер будет создавать дополнительные контейнеры, они будут созданы в Docker высшего уровня. Вы не будете испытывать побочных эффектов вложенности, и кэш сборки будет совместно использоваться для нескольких вызовов.
Примечание: предыдущие версии этой статьи советовали привязать двоичный файл Docker от хоста к контейнеру. Теперь это стало ненадежным, так как механизм Docker больше не распространяется на статические или почти статические библиотеки.
Таким образом, если вы хотите использовать Docker из Jenkins CI, у вас есть 2 варианта:
установка Docker CLI с использованием базовой системы упаковки образа (т. е. если ваш образ основан на Debian, используйте пакеты .deb), использование Docker API.
Немного рекламы 🙂
Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым,
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас
Источник: habr.com