На Хабре уже есть материалы про то, как настроить docker-контейнер для компиляции проекта. Например, Использование Docker для сборки и запуска проекта на C++. В этой статье, как и в предыдущей будет рассмотрен вопрос сборки проекта, но здесь я бы хотел выйти за рамки туториала и рассмотреть глубже вопросы использования контейнеров в таких задачах, а так же построения инфраструктуры сборки с docker.
Немного о docker
Для наглядности дальнейшего изложения необходимо привести описание некоторых компонент docker.
Image
Docker image это шаблон только для чтения с инструкциями по созданию контейнера. Для того, чтобы собрать image необходимо создать Dockerfile, в котором описываются все шаги сборки. Каждый такой шаг создает отдельный слой внутри image. Каждый последующий слой накладывается поверх всех предыдущих и содержит лишь изменения, которые необходимо внести в предшествующий слой.
Например, для Dockerfile:
FROM ubuntu:18.04
ADD app.sh /app
ENTRYPOINT /bin/bash /app/app.sh
docker-образ будет иметь следующую структуру:
Слои внутри image кешируются и могут быть переиспользованы, если никаких изменений не обнаружено. Если слой меняется(добавляется/удаляется), то все последующие создаются с нуля. Для внесения изменений в образ контейнера (и соответственно в окружение запускаемого процесса) достаточно поправить Dockerfile и запустить сборку образа.
Контейнер
Docker контейнер — это запускаемый экземпляр image. Его можно создать, запустить, остановить, удалить и пр. По умолчанию, контейнеры изолированы друг от друга и хост-системы. При старте контейнер запускает только команду, которая указана в ENTRYPOINT или CMD, и останавливается при ее завершении.
При создании каждого контейнера добавляется новый слой поверх всех существующих. Он доступен для записи в текущем контейнере, и уничтожается вместе с контейнером. Все операции записи, создания новых файлов при работе контейнера применяются к этому слою, image всегда остается неизменным. Таким образом структура слоев созданного контейнера будет иметь вид:
При использовании команды docker run
каждый раз будет создаваться новый контейнер, со своим слоем для записи. В задачах сборки это означает, что при каждом запуске будет создавать новое чистое окружение, которое никак не связано с предыдущими выполнениями. Список созданных контейнеров можно посмотреть, выполнив команду: docker container ls -a
.
Собираем проект в контейнере
Для наглядности кратко опишем процесс сборки приложения в контейнере, более подробно этот процесс описан в статье 1 и статье 2.
Схематично возможные шаги по сборке приложения в docker можно представить следующим образом:
Разберем показанные этапы:
- Используем Dockerfile, который описывает окружение, команды для сборки и копирования результатов, и на его основе создаем образ контейнера.
- Применяем полученный образ для создания и запуска контейнера командой
docker run
. Монтируем в контейнер папку с исходниками и папку, куда будет скопирован результат сборки. - После завершения работы контейнера артефакты сборки будут помещены в смонтрованную директорию.
Пример приведен в статье.
Так как здесь используется команда docker run
, то для каждого запуска будет создаваться отдельный контейнер со своим слоем для записи, поэтому временные файлы из предыдущих сборок не попадут в текущую. Необходимо не забывать чистить остановленные контейнеры.
Монтирование директории с исходниками облегчает отладку сборки. Но несет риски — можно собрать релиз из кода, который не прошел проверку на качество, или вообще не добавлен в систему контроля версий. Чтобы этого избежать, можно при каждой сборке клонировать git-репозиторий внутрь контейнера, как, например, в файле:
FROM ubuntu:bionic
RUN apt-get update \
&& apt-get install -y apt-utils
RUN apt-get update \
&& apt-get install -y make gcc g++ qt5-default git
RUN mkdir -p /app/src
WORKDIR /app/build
# Собираем проект и копируем артефакты сборки
ENTRYPOINT git -C /app/src clone https://github.com/sqglobe/SimpleQtProject.git \
&& qmake /app/src/SimpleQtProject/SimpleQtProject.pro \
&& make \
&& cp SimpleQtProject /app/res/SimpleQtProject-ubuntu-bionic
Здесь клонирование выполняется в ENTRYPOINT
, а не в инструкции RUN
, по причине кеширования. ENTRYPOINT
выполняется всегда при запуске контейнера, а результат выполнения команды RUN
может быть взят из кеша.
Инфраструктура для сборки
Для сборки проекта под разные операционные системы или дистрибутивы Linux может применяться некая конфигурация серверов (машин сборки, серверов с системой контроля версий и пр.). На практике мне приходилось сталкиваться со следующей инфраструктурой:
Здесь пользователь обращается к web-серверу, через который запускается сборка проекта на машинах с Ubuntu и Red Hat. Далее, на каждой машине выполняется клонирование git-репозитория с проектом во временную директорию и запускается сама сборка. Пользователь может скачать результирующие файлы с той же страницы, с которой и запускал весь процесс.
Такая сборка является повторяемой, потому что разработчики используют одно и то же окружение.
Из минусов — необходимо поддерживать целую инфраструктуру, администрировать несколько серверов, устранять баги в скриптах и web-приложении и пр.
Упрощаем с docker
Поддержка показанной выше инфраструктуры требует определенных затрат, как денежных, так и людских. В случае, если Ваша команда трудится над небольшим стартапом, или Вы являетесь единственным разработчиком, то можно воспользоваться контейнерами docker для реализации своей инфраструктуры сборки.
Рассмотрим тривиальный Qt проект, который собирается с помощью qmake — SimpleQtProject. В папке docker указанного проекта находится ряд файлов:
- centos7.docker — описывает контейнер для сборки проекта под CentOS 7;
- ubuntu-bionic.docker — контейнер для сборки под Ubuntu 18.04;
- ubuntu-xenial.docker — описывает контейнер для сборки под Ubuntu 16.04.
Данные файлы реализуют идею клонирования исходного кода внутрь контейнера.
Запускается вся сборка с помощью Makefile. Он очень короткий и содержит достаточно комментариев. Его основа — это создание образа и запуск контейнера:
%: %.docker
docker build -t simple-qt-$(strip $(subst .docker,, $< )) --file $< .
docker run --mount type=bind,source=$(RELEASE_DIR),target=/app/res simple-qt-$(strip $(subst .docker,, $< ))
В этом этапе сборки создается образ контейнера с именем, состоящим из префикса simple-qt- и названия системы (для centos 7 это будет simple-qt-centos7). В качестве Dockerfile используется соответствующий файл с разрешением .docker. Далее запускается контейнер на основе созданного образа, и к нему монтируется папка для копирования артефактов сборки.
После запуска команды make
в директории docker, в папке docker/releases будут находится результаты сборки под несколько платформ.
Таким образом наша инфраструктура для сборки SimpleQtProject будет выглядеть следующим образом:
Достоинства данной конфигурации:
- Локальность. Разработчик собирает проект для нескольких платформ на своей локальной машине, это исключает необходимость содержать парк серверов, настраивать копирование артефактов между серверами по сети, отправку и обработку сетевых команд.
- Изоляция окружения. Контейнер обеспечивает полностью изолированную среду для сборки конкретного приложения. Есть возможность обеспечить сборку проектов с несовместимыми окружениями на одной машине (например таких, которые требуют различных версий одной и той же библиотеки).
- Версионирование. Поместив Dockerfile в git-репозиторий, можно отслеживать изменения в среде сборки с выходом новых релизов, откатываться к предыдущим версиям среды сборки и пр.
- Мобильность. При необходимости данная инфраструктура без особых проблем разворачивается на другом компьютере. Технология создания образа контейнера позволяет вносить изменения в сам образ очень легко — достаточно обновить Dockerfile и запустить сборку образа.
- Самодокументируемость. По сути, Dockerfile содержит шаги для развертывания окружения сборки. Поэтому, при необходимости развернуть такое окружение, но уже в обычной системе, можно воспользоваться командами из него же.
- Легковесность. Контейнер запускается в момент начала сборки и останавливается по ее завершению автоматически. Он не тратит процессорное время и оперативную память впустую.
Однако есть и существенный минус — сборка проекта потребует и сборки образа контейнера. При первом запуске это может занять продолжительное время. Но при повторных, особенно если Dockerfile не менялся, образ собирается с использованием кеша в разы быстрее.
Так же необходимо не забывать очищать остановленные контейнеры.
Заключение
В заключении хотелось бы отметить, что docker является не единственной технологией контейнеризации. Но есть некоторые особенности, которые его выгодно отличают для задач сборки от того же LXC:
- Создать контейнер можно используя текстовый Dockerfile. Это файл с простым синтаксисом, его можно добавить в репозиторий с проектом (как я всегда делаю) и держать постоянно под рукой.
- Каждый раз, запуская контейнер docker командой
docker run
мы получаем чистую среду, как если бы выполняли все в первый раз. Временные файлы между сборками не сохраняются. - Контейнер запускет не целую операционную систему, а только необходимый процесс сборки.