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



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


    Контейнер


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


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



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


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


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


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


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


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



    Здесь пользователь обращается к web-серверу, через который запускается сборка проекта на машинах с Ubuntu и Red Hat. Далее, на каждой машине выполняется клонирование git-репозитория с проектом во временную директорию и запускается сама сборка. Пользователь может скачать результирующие файлы с той же страницы, с которой и запускал весь процесс.


    Такая сборка является повторяемой, потому что разработчики используют одно и то же окружение.


    Из минусов — необходимо поддерживать целую инфраструктуру, администрировать несколько серверов, устранять баги в скриптах и web-приложении и пр.


    Упрощаем с docker


    Поддержка показанной выше инфраструктуры требует определенных затрат, как денежных, так и людских. В случае, если Ваша команда трудится над небольшим стартапом, или Вы являетесь единственным разработчиком, то можно воспользоваться контейнерами docker для реализации своей инфраструктуры сборки.


    Рассмотрим тривиальный Qt проект, который собирается с помощью qmakeSimpleQtProject. В папке 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 будет выглядеть следующим образом:



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


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

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


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


    Заключение


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


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

    Похожие публикации

    Средняя зарплата в IT

    113 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 10 037 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 12

      +1
      Jenkins ещё сюда добавить надо, чтоб сборки и тесты автоматом делались при коммите в гит и т.д.
        0

        Да, это хороший вариант дальнейшего развития

          +3
          Или Gitlab. Он и проект хранит, и реестр докер образов и пайплайны в нём.
          +3
          Отличная статья для тех, кто вообще ничего про Докер не слышал. Все на пальцах объяснено, понятно, что происходит. Я помню себя несколько лет назад, когда пытался понять, что это и зачем — ни в одной статье человеческим языком не было объяснено.
            0
            Тех, кто вообще ничего про Докер не слышал, подобные несогласованные определения только запутают:
            Docker image это шаблон только для чтения с инструкциями по созданию контейнера.

            Docker контейнер — это запускаемый экземпляр image.

              0

              Возможно кого-то это может запутать, именно на такой случай статья содержит большое количество ссылок на документацию к docker. Эти определения взяты оттуда же.

            0

            Если проект большой, то можно в докере подмонтировать глобальный volume для промежуточных файлов сборки. Чтобы не пересобирать каждый раз с нуля.

              +1
              При старте контейнер запускает только команду, которая указана в ENTRYPOINT или CMD, и останавливается при ее завершении.

              Это неверно. Команда, которая выполняется при запуске контейнера получается, условно, путем склеивания ENTRYPOINT и CMD. Причем оба параметры могут переопределены при docker run


              Чтобы этого избежать, можно при каждой сборке клонировать git-репозиторий внутрь контейнера, как, например, в файле:

              Это худшая из идей. Нужно для начала определиться — для чего мы используем образ. Если для дистрибуции нашего софта, то git clone в докерфайле быть не должно. Как минимум из соображений того, что нам на приватные гит репозитории придется в докерфайле ещё как-то прописать ключи доступа. Ну, и, очевидно, что в образе хранить их не очень.
              Второй момент, что если мы образ докера используем для фиксации версии сборочного тулинга, то это другая ситуация и исходники можно передавать как volume (опять же нет необходимости делать git clone внутри докерфайла)
              Третий момент, который хочется подсветить — касается кэширования. Действительно, что докер пытается все кэшировать. И, соответственно, если в докерфайле написан "git clone", то докер может подумать, что изменений нет. Именно поэтому рекомендуется исходники инжектировать в образ через команду COPY, которая, как следует из названия, копирует файлы с машины разработчика внутрь образа. И в случае изменения любого файла в каталоге, передаваемом внутрь, докер увидит изменения. Либо есть ещё интересный воркераунд с передачей build-args и директивой ARG. Если внутрь образа подкидывать, например, текущую дату, то все после соответстввющей инструкции ARG будет инвалидировано и, как следствие, пересобрано. В принципе, это нормальная, описанная практика, хотя и выглядит как небольшой костыль


              Ещё можно использовать docker multistage.

                0
                Команда, которая выполняется при запуске контейнера получается, условно, путем склеивания ENTRYPOINT и CMD.

                Не совсем так, в документации написано, что, кроме склеивания, ENTRYPOINT может полностью затирать CMDhttps://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact.


                Это важное замечание, и в статью добавлена информация о том, что ENTRYPOINT и CMD могут быть указаны вместе.


                По поводу клонирования git-репозитория внутрь контейнера.


                Такое поведение полезно, когда комиты попадают в мастер только через pull request (это позволяет выполнить, как минимум, сode review ). При таком подходе происходит разделение ролей — одни пишут код, другие выполняют сode review, а третьи выпускают версии (создают тег с новой версией в репозитрии и выполняют сборку релиза из добавленного тега). Клонирование репозитория с нужным тэгом внутрь контейнера, как минимум, упрощает сборку новой версии (не придется руками выполнять команду git checkout ...). Так же, такое поведение исключает вероятность того, что папка с исходниками может содержать незакомиченный код, ветку разработки и пр.


                Использование сборки с volume так же описана в статье.


                Нужно для начала определиться — для чего мы используем образ.

                В данной статье образ используется только для сборки. Если есть приватный репозиторий, то данные для аутентификации могут быть проброшены в контейнер, например через параметры -e, --env, --env-file команды docker run.


                Третий момент, который хочется подсветить — касается кэширования.

                Это полезное добавление. Я упустил из виду возможность использовать COPY для копирования исходников внутрь контейнера.

                  0
                  Не совсем так, в документации написано, что, кроме склеивания, ENTRYPOINT может полностью затирать CMD —

                  Ну, там написано, что если меняете ENTRYPOINT, то CMD обнуляется.
                  Плюс есть немного магии с sh -c
                  А, в целом, это не противоречит написанному мной.


                  Так же, такое поведение исключает вероятность того, что папка с исходниками может содержать незакомиченный код, ветку разработки и пр.

                  Небольшая ремарка. У вас все равно должна быть система сборки и хранения кода. Например, гитлаб. И она гарантирует, что сборка не сможет быть произведена для кода, который не в репозитории. А разработчику МОЖЕТ быть нужно проверить код, который в его локальную копию репозитория ещё не попал. Это тонкости и, наверное, не предмет для спора. Но именно по совокупности — я за COPY исходников, а лучше — за подключение их в виде volume/bind-mount в сборочный контейнер (т.к. тот же гитлаб/jenkins etc. сами делают git clone в известное место известного коммита/ветки/тэга).


                  Если есть приватный репозиторий, то данные для аутентификации могут быть проброшены в контейнер, например через параметры -e, --env, --env-file команды docker run.

                  Принимается, но опять же — этой проблемы можно избежать, если исходники есть. Сюда же можно приплести гит-субмодули, если требуется сборка проекта из нескольких репозиториев (тогда создаётся убер-репозиторий, в который все остальные включены как субмодули с правильными версиями, и можно выкачать определенный срез-версию кодовой базы одним чохом, как говорится)

                0
                А знающие могут дать пример dockerfile для билдинга используя виндовый докер? Какой контейнер брать за основу, чтобы собирать проект на Windows (msbuild + cmake)?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое