Сразу оговорюсь, что dapp не задумывался как утилита, упрощающая локальную разработку приложения. Однако в последнее время у нас оформилось видение, как мы могли бы облегчить жизнь обычного разработчика, и об этом обязательно выйдет другая статья. А сейчас — про упрощение процесса CI/CD.
Сборка приложения
Какую часть занимает сборка в процессе CI/CD? Давайте ещё раз взглянем на схему из доклада коллеги distol:
Сборка превращает исходный код программ из Git-репозитория (стадия git) в Docker-образы, готовые к запуску в различных окружениях (стадия build). Встаёт вопрос, что тут упрощать, если уже есть
Dockerfile
и docker build
? Действительно, есть масса статей про сборку Docker-образов для приложений на различных языках. Но в основном это статьи про сборку и запуск приложения из среза исходных текстов.С какими проблемами придётся столкнуться, когда приложений будет несколько десятков? Когда окажется, что у процесса сборки есть много общих частей и нужно копировать кусочки
Dockerfile
? Когда захочется ускорить сборку (ведь в приложении поменялось всего два файла)? Эти вопросы заранее не предскажешь, пока не встретишься с ними на практике. Наши ответы на такие вопросы и опыт их решения воплощён в утилите dapp.Сборка с dapp и Dappfile
Разберём на «живом» примере, что даёт dapp. В качестве первого испытуемого возьмём простое PHP-приложение symfony-demo.
Для самых нетерпеливых в нашем GitHub уже добавлен
Dappfile
и сделан Vagrantfile
, поэтому достаточно запустить vagrant up
, после чего можно собирать приложение без установки Docker и dapp в свою систему.Для тех, кто не спешит и хотел бы «окунуться с головой», нужно установить dapp (см. документацию) и склонировать себе репозиторий приложения:
$ git clone https://github.com/symfony/symfony-demo.git
$ cd symfony-demo
$ vi Dappfile
Описывающий сборку
Dappfile
— файл с синтаксисом DSL на Ruby (в чём-то похоже на Vagrantfile
, но планируем перейти на YAML с сохранением поддержки старого формата). Его содержимое:dimg 'symfony-demo-app' do
docker.from 'ubuntu:16.04'
git do
add '/' do
to '/demo'
end
end
shell do
before_install do
run 'apt-get update',
'apt-get install -y curl php7.0',
# пользователь phpapp
'groupadd -g 242 phpapp',
'useradd -m -d /home/phpapp -g 242 -u 242 phpapp'
end
install do
run 'apt-get install -y php7.0-sqlite3 php7.0-xml php7.0-zip',
# установка composer
'curl -LsS https://getcomposer.org/download/1.4.1/composer.phar -o /usr/local/bin/composer',
'chmod a+x /usr/local/bin/composer'
end
before_setup do
# исходным текстам нужно сменить права и запустить composer install
run 'chown phpapp:phpapp -R /demo && cd /demo',
"su -c 'composer install' phpapp"
end
setup do
# используем текущую дату как версию приложения
run 'echo `date` > /demo/version.txt',
'chown phpapp:phpapp /demo/version.txt'
end
end
# порт совпадает с портом, указанным в start.sh
docker.expose 8000
end
Также нужно добавить
start.sh
и сделать его исполняемым (chmod +x start.sh
):#!/bin/sh
cd /demo
su -c 'php bin/console server:run 0.0.0.0:8000' phpapp
Собрать образ приложения можно командой:
$ dapp dimg build
А запустить так:
$ dapp dimg run -d -p 8000:8000 -- /demo/start.sh
Особенности: кэш, git patch, стадии сборки
Сборка выдаёт длинный лог со всеми совершаемыми действиями. Листинг очень большой, поэтому его часть выложена отдельно на GitHub. Если теперь запустить команду
dapp dimg build
ещё раз, то эти действия выполняться повторно не будут, т.к. результат их работы закэширован. Лог повторного запуска можно увидеть здесь. Его фрагмент:Видны строки с именем стадии и результатом
[USING CACHE]
— это означает, что dapp не стал выполнять описанные действия, создавая новый слой образа, а использовал уже существующий.Теперь притворимся разработчиком и внесём правки — например, изменим текст ссылки в шапке страницы в шаблоне
app/Resources/views/base.html.twig
. Сделаем коммит и попробуем собрать приложение. Видно, что наложился только git patch
, т.е. новый образ был создан на основе закэшированных слоёв, к которым добавились изменения в файлах проекта:...
Setup [USING CACHE]
signature: dimgstage-symfony-demo:3705edf770dd88ac714a7001fd24f395c87b2110005025eff48019d5973846ce
date: 2017-08-16 04:16:46 +0000
difference: 0.0 MB
Git artifacts dependencies [USING CACHE]
signature: dimgstage-symfony-demo:f3f1c3e1ce5f0f5b880b1ec693b194d7e6a841a4166b29982d11b4e4c4cbe360
date: 2017-08-16 04:16:49 +0000
difference: 0.0 MB
Git artifacts: apply patches (after setup) [USING CACHE]
signature: dimgstage-symfony-demo:15e56865dd8b2a1cc55d5381a4e6f2cbcdc3a718509de29b15df02e8279b42c3
date: 2017-08-16 04:16:52 +0000
difference: 0.0 MB
Git artifacts: latest patch ... [OK] 3.22 sec
signature: dimgstage-symfony-demo:a9c21d0e36218563c8fd34b51969ed2f3b6662ca7775acae49488c5ebbbf25e1
Docker instructions ... [OK] 3.16 sec
signature: dimgstage-symfony-demo:2eae4537c4210aaf4a153c7b8d3036343abf98b4ac4a3b99a2eb1967bea61378
instructions:
EXPOSE 8000
Это ускорение сборки хорошо работает для файлов, которым не нужна какая-то особая обработка. Что же делать, если поменяется, например,
composer.json
и нужно при сборке вызывать composer install
?На этот случай dapp поддерживает 4 пользовательских стадии сборки (подробно они описаны в документации). Первая стадия —
before_install
, во время которой недоступны исходники. Обычно это установка редко меняющихся пакетов и настройки ОС. Дальнейшие 3 стадии: install
, before_setup
и setup
— уже имеют доступ к исходным текстам. Как же управлять наложением git patch
?Для того, чтобы указать, что изменения в файлах должны приводить к перезапуску сборки с определённой стадии, нужно указать директиву
stage_dependencies
в директиве git
. В случае нашего приложения изменение файлов composer.json
или composer.lock
должно приводить к пересборке начиная со стадии before_setup
, на которой запускается composer install
. Поэтому директива git
будет выглядеть так: git do
add '/' do
to '/demo'
stage_dependencies.before_setup 'composer.json', 'composer.lock'
end
end
Лог сборки здесь. Видно, что
git patсh
применился после стадии install
и образ был пересобран со стадии before_setup
:...
Install [USING CACHE]
signature: dimgstage-symfony-demo:a112d1abf364602c3595990c3f043d88e041a2a6f3cbcf13b6fc77a9fb3fd190
date: 2017-08-16 04:14:19 +0000
difference: 5.0 MB
Git artifacts dependencies ... [OK] 2.75 sec
signature: dimgstage-symfony-demo:9f0600ab6fb99356110c50454fc31e5fdc6ac3028e4ba8f200e789d140514bf9
Git artifacts: apply patches (after install) ... [OK] 2.18 sec
signature: dimgstage-symfony-demo:f139188f9b0662d8177d41689b57c700e2276d997139673c3384731f6851d72e
Before setup [BUILDING]
...
Такие связи между изменениями файлов в репозитории и стадиями позволяют уменьшить общее время сборки. В большинстве случаев зависимости приложения добавляются или изменяются не очень часто. Например, разработчик реализует новую фичу, из-за которой потребовалось добавить зависимость в
composer.json
. Первым коммитом, в котором будет новая зависимость, образ пересоберётся со стадии before_setup
— это займёт какое-то время. Но последующие коммиты, в которых composer.json
уже не будет изменяться, выполняются быстро. Именно это позволяет в нашей конфигурации CI/CD автоматически запускать сборку для каждого коммита в ветках разработчиков и DevOps-инженеров (см. «GitLab CI для непрерывной интеграции и доставки в production. Часть 1: наш пайплайн»).Особенности: multi-stage или artifact-образ?
Не так давно в
Dockerfile
появилась возможность собирать части итогового образа с помощью других образов (multi-stage builds). Сделано это для того, чтобы не тащить в итоговый образ golang, webpack, gcc или другие инструменты, которые нужны только для сборки и совершенно не нужны во время работы приложения. Dapp поддерживает такую сборку изначально, с помощью секций artifact
.Возьмём для следующего примера веб-приложение на golang. Как и с первым приложением, готовый для экспериментов репозиторий с
Dappfile
и Vagrantfile
можно склонировать из нашего GitHub.По шагам:
$ git clone https://github.com/revel/examples revel-examples
$ cd revel-examples/booking
$ vi Dappfile
Dappfile
будет такой:dimg_group do
artifact do
docker.from 'golang:1.8'
git do
add '/' do
to '/go/src/github.com/revel/examples'
end
end
shell.before_install do
run 'apt-get update',
'apt-get install -y sqlite3 libsqlite3-dev tree'
end
shell.install do
run 'go get -v github.com/revel/revel',
'go get -v github.com/revel/cmd/revel'
end
shell.build_artifact do
run '(go get -v github.com/revel/examples/booking/... ; true)'
run 'revel build github.com/revel/examples/booking /app'
end
export '/app' do
to '/app'
after 'install'
end
end
dimg 'booking-app' do
docker.from 'ubuntu:16.04'
end
end
Собрать и запустить можно всё теми же командами:
$ dapp dimg build
$ dapp dimg run -p 9000:9000 --rm -d -- /app/run.sh
Чтобы не потеряться среди вывода
docker images
, протегируем итоговый образ:$ dapp dimg tag booking-app:v1.0
Теперь можно уведить, что размер итогового образа не зависит от размера образа с инструментами сборки golang:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
booking-app v1.0 57d564633ecb 4 minutes ago 136MB
ubuntu 16.04 ccc7a11d65b1 7 days ago 120MB
golang 1.8 6ce094895555 3 weeks ago 699MB
Теперь подробнее опишу
Dappfile
с применением artifact
(особенности сборки приложения revel сознательно опускаю — если интересует, можно обсудить в комментариях). В общем случае структура будет такая:dimg_group
artifact do
docker.from
git
команды стадий
export
end
dimg 'dimg_name' do
docker.from
команды стадий
end
end
- Секция
artifact
указывает, что часть приложения нужно собрать в отдельном образе, а директивойgit
внутриartifact
можно указать, куда положить исходные тексты. -
export
указывает, что нужно из отдельного образа скопировать в итоговый образ после выполнения команд стадииbuild_artifact
. - В
export
также можно указать, на какой стадии итоговому образу нужны результаты работы артефакта. В нашем случае указаноafter 'install'
, т.е. после команд стадииinstall
в итоговый образ будет скопирована директория/app
из образа артефакта.
По первому впечатлению всё это аналогично
Dockerfile
с применением multi-stage, но преимуществом артефакта является то, что к нему применяются другие возможности dapp — кэширование и зависимости стадий от изменений в Git (эти зависимости можно описать в секции git
). Итоги
Описанные возможности dapp: разбиение на стадии, зависимость стадий от изменений в файлах в Git-репозитории, использование artifact-образов — могут упростить и ускорить сборку практически любого приложения как в процессе CI/CD, так и для локальной разработки.
Но это только начало. В
Dappfile
можно описать сразу несколько образов и dapp dimg build
их соберёт, а dapp dimg push --tag
проставит тег и за'push'ит кэш-образы и итоговый образ в Registry. Эти возможности будут лучше проиллюстрированы в следующих статьях — в связке с описанием недавно появившейся в dapp поддержки деплоя в Kubernetes. Комментарии (9)
Scf
23.08.2017 23:24+1А можно как-то объяснить, зачем всё это нужно вообще?
Ну ок, решили собирать приложения не на хосте дженкинсом, а в контейнере.
Делаем отдельный образ с build environment, содержащий утилиты сборки без кода приложения.
На хосте тем же дженкинсом делаем чекаут и запускам образ сборки, примонтировав ему зачекаутенные исходники и папки с кешами (~/.m2 для мавена, node_modules для npm и т.п.)
Образ с приложением делаем банальным docker build в папке с артефактами, собранными системой сборки.
Хотим, чтобы npm install/composer install вызывался только при изменении соответствующих файлов, а не всегда? пишем простой Makefile, gnu make создавался специально для этой цели.
diafour Автор
24.08.2017 10:00+1Я правда не хотел, но всё-таки получилось много буковок. Этот вопрос вообще достоин своей отдельной статьи.
А можно как-то объяснить, зачем всё это нужно вообще?
Если философски без конкретики, то dapp создавался, чтобы решить проблемы сборки, которые возникают в CI/CD системе с несколькими билд машинами и большим количеством приложений, когда поддерживать развесистые скрипты сборки в каждом репозитории становится совсем накладно и когда одно приложение это не один репозиторий.
Ну ок, решили собирать приложения не на хосте дженкинсом, а в контейнере.
Делаем отдельный образ с build environment, содержащий утилиты сборки без кода приложения.
На хосте тем же дженкинсом делаем чекаут и запускам образ сборки, примонтировав ему зачекаутенные исходники и папки с кешами (~/.m2 для мавена, node_modules для npm и т.п.)
Образ с приложением делаем банальным docker build в папке с артефактами, собранными системой сборки.Вижу первый вопрос про то, зачем нужна multi-stage сборка, если можно все инструменты поставить на билд машину. Тут ответ простой — инструменты в докер-образе обладают всеми плюсами приложений в докер-образе. Так проще обновлять, так проще иметь несколько версий, так проще обеспечить повторяемость сборки, если агентов сборки несколько на разных машинах и так далее.
Второй вопрос про дженкинс. В целом ваш сценарий может быть организован в любой CI-системе как некий начальный, быстрый вариант. В перспективе всплывают такие минусы: сборка приложения будет происходить всегда, даже когда без этого можно обойтись. Взять например, java-приложение, которое надо собрать maven-ом и собрать статику webpack-ом. C dapp это можно разделить на две секции artifact и объявить, что сборка приложения зависит от изменений в /src/main/java/, а статику собирать только при изменениях в /src/main/webapp/.
Другой минус — docker build и все его недостатки, точнее осознанно введённые ограничения при разработке Dockerfile и docker build, которые в случае CI/CD являются недостатками: нельзя пушить слои кэширования во внешний регистри, нет поддержки модульности сборки (общие скрипты), нет зависимостей выполнения команд от изменений файлов в git (есть зависимость на копирование) и прочее.
И это мы пока говорим про сборку из одного репозитория. А если перед сборкой итогового образа нужно пособирать части приложения из других репозиториев? dapp умеет в зависимости стадий сборки от изменений во внешних репозиториях.
Хотим, чтобы npm install/composer install вызывался только при изменении соответствующих файлов, а не всегда? пишем простой Makefile, gnu make создавался специально для этой цели.
Ничего не имею против gnu make — проверенный временем инструмент. Однако агент на билд машине в общем случае не обязан хранить дерево репозитория и файлы от предыдущей сборки, чтобы gnu make смог бы правильно обнаружить изменения файлов после git pull. dapp вычисляет изменившиеся файлы по git истории, что в системе CI/CD надёжнее, чем проверять mtime файлов. Т.е. если агент имеет доступ к предыдущей сборке, то gnu make может и хватить.
Укажу ещё несколько моментов, почему применение dapp и описание сборки в Dappfile интереснее, чем скрипт сборки в дженкинс.
Первое — Dappfile можно разрабатывать и отлаживать на локальной машине, последующая сборка на билд-машине будет предсказуемой.
Второе — сборка разных версий приложения может отличаться и вариант со своим хитрым скриптом в дженкинс сложно поддерживать (а это нужно поддерживать, т.к. новая версия, которая требует новый скрипт сборки, на прод ещё не выкатилась, а хотфикс нужно прямо сейчас собрать старым скриптом).
Третье — стадии сборки и их кэш в виде нормальных образов позволяют попасть в проблемную стадию и посмотреть, что там пошло не так (--introspect* флаги http://flant.github.io/dapp/debug_for_advanced_build.html)Scf
24.08.2017 10:33Я имел в виду сборку приложения типа
git clone myapp ; cd myapp # или git pull docker run -it -v ./:/build my-js-build-tools /build/build.sh docker build -t myapp:42 . docker push myapp:42
Получается, основное преимущество dapp — он умеет сохранять и восстанавливать предыдущее состояние сборки, чтобы делать инкрементальный билд вместо полного?
Прямая сборка приложения из нескольких репозиториев, имхо, странная идея, т.к. усложняет версионность: чтобы воспроизвести сборку, нужно знать нужный коммит в каждом репозитории, участвующим в сборке.
diafour Автор
24.08.2017 13:40Если вам интересна концептуальная сторона этого дела (зачем мы всё это городим, почему тратим свои силы и время) в докладе distol всё по полочкам разложено: видео https://www.youtube.com/watch?v=8R5UDg29Vic, текст https://habrahabr.ru/company/flant/blog/324274/
docker run -it -v ./:/build my-js-build-tools /build/build.sh
Да, упрощенно artifact в Dappfile так и делает. Только для артефакта доступно 5 стадий, то есть 5 скриптов. Можно использовать внешние докер-образы, собранные разработчиками языков или фреймоворков (тот же golang:1.8), дополнив их своими сценариями (добавить пакетов, создать нужных директорий и т.п.) на стадиях before_install, install, которые после первой сборки надолго закэшируются. В случае же my-js-build-tools — получается, что у нас есть где-то отдельный образ с инструментами, сценарий сборки этого образа надо поддерживать, собирать отдельными заданиями, версионировать, при этом как-то указывать нужную версию для сборки конкретного коммита приложения (тут у вас кстати некое противоречие с вашим последним абзацем). Но инструменты они тоже «живые». Иногда оправдано держать сборку образа с инструментами отдельно, в большинстве же случаев сценарий сборки образа с инструментами в Dappfile получается выгоднее, т.к. всегда видно, чем будет собираться конкретный релиз приложения.
Получается, основное преимущество dapp — он умеет сохранять и восстанавливать предыдущее состояние сборки, чтобы делать инкрементальный билд вместо полного?
В каком-то смысле да, можно считать так, dapp умеет что-то похожее на инкрементальный билд в зависимости от изменений файлов в git. Но я бы не сказал, что это прям основное преимущество. Одно из — это да.
Прямая сборка приложения из нескольких репозиториев, имхо, странная идея, т.к. усложняет версионность: чтобы воспроизвести сборку, нужно знать нужный коммит в каждом репозитории, участвующим в сборке.
Разработчики go так не считают (и вот кто их только надоумил). В Dappfile можно указать коммит или ветку или тэг во внешнем репозитории, который нужно склонировать перед сборкой. Если ветка наросла, то запустится пересборка. В целом это не сложнее, чем указывать версию внешних библиотек в pom.xml или Composer.json.
SirEdvin
На данный момент я использую Rocker и в целом там все круто, исключая вот это:
На данный момент приходится писать bash-скрипты и засовывать их в базовый образ, что работает далеко не всегда. В dapp можно использовать библиотеки ruby или как-то по другому работать?
diafour Автор
Тема общих частей была упомянута в обзорном докладе про dapp https://habrahabr.ru/company/flant/blog/324274/. В данный момент dapp поддерживает два способа выполнения команд на пользовательских стадиях: shell (как в этой статье) и chef-рецепты. Chef-рецепты живут в кукбуке приложения и в dimod-ах — кукбуках-модулях, которые выносятся в отдельные репозитории.
Про поддержку chef можно посмотреть в документации
http://flant.github.io/dapp/chef_for_advanced_build.html
http://flant.github.io/dapp/chef_dimod_for_advanced_build.html
chef-server для сборки конечно же не нужен, используется chef-solo, который выполняет рецепты для каждой указанной стадии.
SirEdvin
Печалька. А у вас не планируется, например, поддержка ansible-плейбуков?
diafour Автор
YAML для описания и Ansible для команд сборки — две самые часто спрашиваемые опции ;) Хотим, планируем, т.к. сами решили уходить от chef.