Сборка docker контейнеров с помощью docker контейнеров

image

С момента начала работы с Docker, мы столкнулись с немалым количеством проблем. Одна из них — организация сборки приложений и упаковки их в контейнера. Мы решили эту задачу с помощью введения концепции сборочного контейнера. О том, что это такое, зачем нужно и как мы до этого дошли и пойдет речь в этом посте.

Что такое сборочный контейнер (builder container)?


Сборочный контейнер — это специально собранный docker image, задача которого производить некую работу над исходниками программы, например установить зависимости, скомпилировать исходники, статический анализ кода и т.д. Определение довольно пространное, поэтому рассмотрим на примере.

У нас есть некий проект. Чтобы его собрать и запустить нужно:
  1. установить все необходимые npm пакеты
  2. с помощью bower установить все клиентские зависимости
  3. затем запустить grunt, чтобы он собрал нам все это

Мы пользуемся для этого npm-builder — сборочный контейнер с nodejs, npm, g++ и make внутри, для наглядности выполним все шаги по отдельности:

$ docker run --rm -v `pwd`:/data -w /data leanlabs/npm-builder npm install
$ docker run --rm -v `pwd`:/data -w /data leanlabs/npm-builder bower install --allow-root
$ docker run --rm -v `pwd`:/data -w /data leanlabs/npm-builder grunt build

Итак, в несколько команд мы получили собранный проект.

Разберем по шагам, что же происходит. Мы запускаем docker контейнер npm-builder в корневой директории проекта и монтируем в /data текущую директорию (корневую директорию проекта со всеми исходниками), /data мы указываем как рабочую директорию контейнера, чтобы все команды в контейнере выполнялись в ней.
Первой командой, npm install, в директорию node_modules установятся все nodejs модули перечисленные в package.json, включая bower.
Следующей командой, bower install --allow-root, мы устанавливаем в директорию bower_components все зависимости, перечисленные в bower.json.
И последней командой мы запускаем сборку проекта grunt’ом. Дополнительно при запуске контейнеров указываем опцию --rm, чтобы контейнера удалялись после остановки.

На примере проекта на php, мы можем использовать сборочный контейнер composer, для установки зависимостей:

$ docker run --rm -v `pwd`:/data imega/composer:1.1.0 install --no-dev

Зачем все это?


Порог вхождения в проект. Использование сборочных контейнеров позволяет избавиться от необходимости установки разнообразного программного обеспечения на машине разработчика. Все что нужно для начала работы над проектом — склонировать репозиторий проекта, установить docker, docker-compose и выполнить “docker-compose up -d”, все необходимые для сборки и запуска проекта контейнеры подтянутся с docker hub. Больше не нужно устанавливать на машину разработчика разнообразный необходимый для сборки и запуска софт.

Время сборки. Если все инструкции для сборки указывать в Dockerfile, то время сборки контейнера начинает возрастать, пропорционально количеству шагов сборки.

Единообразие исполняемой среды. Сборка приложения и его запуск теперь вещи независимые, упаковка в контейнер — всего лишь COPY собранного приложения. Значит, независимо от окружения, контейнеры в которых приложения исполняются всегда одни и те же, например, любое приложение на php исполняется в контейнере с php. Значит, если что-то вдруг ломается, то ломается оно везде и одинаково.

Повторное использование этапов сборки. Если сборка описана в Dockerfile, то все этапы сборки каждый раз придется описывать заново, решается использованием инструкции ONBUILD, но в этом случае мы лишаемся определенного уровня гибкости, например, если понадобятся дополнительные шаги сборки их все равно придется описывать отдельно, также частично можно лишиться прозрачности сборки, чтобы понять что же произойдет при сборке нужно смотреть исходник родительского контейнера.

Предсказуемость сборки. Когда мы начали использовать docker, то, по-началу некоторые шаги сборки проектов мы производили на хостовой машине, например запуск grunt. Не делайте так ;)

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

FROM leanlabs/nginx:1.0.1

COPY . /var/www/client/
COPY ./build/sites-enabled/client.conf /etc/nginx/sites-enabled/client.conf

CMD ["nginx", "-g", "daemon off;"]


Использование уже существующих инструментов. В экосистеме докера уже существует масса инструментов, хватает и полезных и не очень, изобретать велосипед не хотелось. Например, docker-compose, сборочные контейнера подходят и здесь.

Конвенции при использовании сборочных контейнеров


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

Общий “интерфейс”. Из общего мы выделили volume’ы. В сборочных контейнерах есть volume /data — для монтирования исходников приложения и /cache — для монтирования директории с кэшами сборочного контейнера, это могут быть, например, сторонние зависимости (кэш composer, npm, bower). Для сборочных контейнеров мы используем один базовый образ, который и выполняет роль интерфейса.

Сборочный контейнер — внешняя относительно приложения зависимость. Например, мы не используем отдельные сборочные контейнеры с phpunit, потому что phpunit подключается через composer и как его версия, так и его наличие определяется непосредственно самим приложением.

Способ запуска. По способу запуска мы разделяем контейнера на:
  • One shot контейнеры, запускаются один раз, выполняют команду и завершают свою работу. Удобно использовать для установки зависимостей, клонирования исходников, генерации документации к апи.
  • Long running контейнеры, долгоживущие, отслеживают изменения в исходниках и пересобирают проект — пересборка SCSS при изменениях стилей, сборка JS, прогоняют тесты при изменениях исходников, прогоняют статический анализатор и т.д.

Именование контейнеров. К названию сборочного контейнера мы добавляем суффикс “-builder”, например, npm-builder, erlnag-builder и т.д.

Использование с docker-compose


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

С docker-compose все просто, но есть нюансы. Использовать one shot контейнеры возможно только, если docker-compose up выполняется с опцией “-d”, в противном случае по завершении работы такого контейнера, compose останавливает все остальные контейнеры.

Вот так выглядит установка зависимостей php приложения с использованием composer:

phpbuilder:
   image: imega/composer:1.1.0
   volumes:
    - "./:/data"
    - "$HOME/.composer:/cache"
   command: ["update"]

Long running контейнеры работают без проблем. Вот так выглядит запуск установки nodejs, bower зависимостей и запуск grunt’a:

clientbuilder:
  image: leanlabs/npm-builder
  volumes:
    - "./:/data"
    - "$HOME/.node_cache:/cache"
  command: ["/bin/sh", "-c", "npm install && bower install --allow-root && grunt"]

Использование с make


Make в связке с докером и сборочными контейнерами мы используем для сборки релизов, местами мы им заменяем docker-compose.

Вот пример makefile’а, использующего подход со сборочными контейнерами:

IMAGE = leanlabs/client
TAG   = 1.1.9

build:
	@docker run --rm -v $(CURDIR):/data -v $$HOME/node_cache:/cache leanlabs/npm-builder npm install
	@docker run --rm -v $(CURDIR):/data -v $$HOME/node_cache:/cache leanlabs/npm-builder bower install --allow-root
	@docker run --rm -v $(CURDIR):/data -v $$HOME/node_cache:/cache leanlabs/npm-builder grunt build

release:
	@docker build -t $(IMAGE) .
	@docker tag $(IMAGE):latest $(IMAGE):$(TAG)
	@docker push $(IMAGE):latest
	@docker push $(IMAGE):$(TAG)

.PHONY: build release

Итог


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

На этом все. Спасибо за внимание.

Благодарю своего коллегу, cnam812, за помощь в написании статьи.
Share post

Comments 11

    +8
      +1
      Что угодно, лишь бы не LXC.
        +1
        В каком контексте возражение?
        Если в контексте «полноценной виртуальной машины» vs идеологии docker'а один контейнер — одно приложение, то спорить смысла не вижу, разные задачи и цели
          0
          В контексте высосанности из пальца докера как такового. Да, у меня весьма резкое и радикальное к нему отношение. Да, я работал (и работаю, увы) с ним.
          man lxc-execute
            +1
            А для более продуктивной беседы не хотите поделиться своим мнением, основанным на реальном использовании его в продакшене?
              +1
              Так, действительно, Докер ничего особо нового в контейнеризацию не принес (кроме дельт контейнеров и докерфайлов), а ограничений у него — вагон: примером, дубовый сетевой стек, невозможность что-то изменить после создания контейнера и т.п.

              Я достаточно активно юзал OpenVZ, и он крут. Просто что не в мейнстриме. LXC, как я понимаю, вполне ему замена.

              Вердикт в том что Докер — это что-то типа для «быстренько проверить разработчику».
        0
        Собственно, а где-же попытка собрать докер в докере? Вот интересно было бы, чтобы докер-сборщик собирал бы собственно имедж другого контейнера как артефакт.
          0
          Статья, мне кажется, получилась довольно объемная, про сборку докера в докере напишу в следующей.
            0
            Не сложилось?
            0
            У нас в проекте именно так и делается. Сначала собираем сборщик, потом он собирает нам все необходимые образы. На статью это не тянет. Все банально и просто.
              0
              Вот тут github.com/jpetazzo/dind бери и собирай на здоровье )

            Only users with full accounts can post comments. Log in, please.