Инфраструктура сборки проекта с docker

На Хабре уже есть материалы про то, как настроить 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-образ будет иметь следующую структуру:

vapvrhrjuhirk1fw2up-_h-osau.png

Слои внутри image кешируются и могут быть переиспользованы, если никаких изменений не обнаружено. Если слой меняется (добавляется/удаляется), то все последующие создаются с нуля. Для внесения изменений в образ контейнера (и соответственно в окружение запускаемого процесса) достаточно поправить Dockerfile и запустить сборку образа.


Контейнер

Docker контейнер — это запускаемый экземпляр image. Его можно создать, запустить, остановить, удалить и пр. По умолчанию, контейнеры изолированы друг от друга и хост-системы. При старте контейнер запускает только команду, которая указана в ENTRYPOINT или CMD, и останавливается при ее завершении.

При создании каждого контейнера добавляется новый слой поверх всех существующих. Он доступен для записи в текущем контейнере, и уничтожается вместе с контейнером. Все операции записи, создания новых файлов при работе контейнера применяются к этому слою, image всегда остается неизменным. Таким образом структура слоев созданного контейнера будет иметь вид:

yjebrku_12g3cuu3qnl6xfdtdbo.png

При использовании команды docker run каждый раз будет создаваться новый контейнер, со своим слоем для записи. В задачах сборки это означает, что при каждом запуске будет создавать новое чистое окружение, которое никак не связано с предыдущими выполнениями. Список созданных контейнеров можно посмотреть, выполнив команду: docker container ls -a.


Собираем проект в контейнере

Для наглядности кратко опишем процесс сборки приложения в контейнере, более подробно этот процесс описан в статье 1 и статье 2.

Схематично возможные шаги по сборке приложения в docker можно представить следующим образом:
g_ruktfloq-0loozoxjo9aafxga.png

Разберем показанные этапы:


  1. Используем Dockerfile, который описывает окружение, команды для сборки и копирования результатов, и на его основе создаем образ контейнера.
  2. Применяем полученный образ для создания и запуска контейнера командой docker run. Монтируем в контейнер папку с исходниками и папку, куда будет скопирован результат сборки.
  3. После завершения работы контейнера артефакты сборки будут помещены в смонтрованную директорию.

Пример приведен в статье.

Так как здесь используется команда 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 может применяться некая конфигурация серверов (машин сборки, серверов с системой контроля версий и пр.). На практике мне приходилось сталкиваться со следующей инфраструктурой:

ue0i1wmgapl-8x9h9-zw-bsrbi8.png

Здесь пользователь обращается к 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 будет выглядеть следующим образом:

bsbgru5mo2bcrqb4z_31sdreyt8.png

Достоинства данной конфигурации:


  1. Локальность. Разработчик собирает проект для нескольких платформ на своей локальной машине, это исключает необходимость содержать парк серверов, настраивать копирование артефактов между серверами по сети, отправку и обработку сетевых команд.
  2. Изоляция окружения. Контейнер обеспечивает полностью изолированную среду для сборки конкретного приложения. Есть возможность обеспечить сборку проектов с несовместимыми окружениями на одной машине (например таких, которые требуют различных версий одной и той же библиотеки).
  3. Версионирование. Поместив Dockerfile в git-репозиторий, можно отслеживать изменения в среде сборки с выходом новых релизов, откатываться к предыдущим версиям среды сборки и пр.
  4. Мобильность. При необходимости данная инфраструктура без особых проблем разворачивается на другом компьютере. Технология создания образа контейнера позволяет вносить изменения в сам образ очень легко — достаточно обновить Dockerfile и запустить сборку образа.
  5. Самодокументируемость. По сути, Dockerfile содержит шаги для развертывания окружения сборки. Поэтому, при необходимости развернуть такое окружение, но уже в обычной системе, можно воспользоваться командами из него же.
  6. Легковесность. Контейнер запускается в момент начала сборки и останавливается по ее завершению автоматически. Он не тратит процессорное время и оперативную память впустую.

Однако есть и существенный минус — сборка проекта потребует и сборки образа контейнера. При первом запуске это может занять продолжительное время. Но при повторных, особенно если Dockerfile не менялся, образ собирается с использованием кеша в разы быстрее.

Так же необходимо не забывать очищать остановленные контейнеры.


Заключение

В заключении хотелось бы отметить, что docker является не единственной технологией контейнеризации. Но есть некоторые особенности, которые его выгодно отличают для задач сборки от того же LXC:


  1. Создать контейнер можно используя текстовый Dockerfile. Это файл с простым синтаксисом, его можно добавить в репозиторий с проектом (как я всегда делаю) и держать постоянно под рукой.
  2. Каждый раз, запуская контейнер docker командой docker run мы получаем чистую среду, как если бы выполняли все в первый раз. Временные файлы между сборками не сохраняются.
  3. Контейнер запускет не целую операционную систему, а только необходимый процесс сборки.

© Habrahabr.ru