Практика с dapp. Часть 1: Сборка простых приложений
Эта статья — ознакомительное руководство по сборке Docker-образов приложений с помощью нашей Open Source-утилиты dapp (подробнее о ней читайте в анонсе. На примере двух простых приложений (с одним образом) рассмотрим, как могут быть задействованы некоторые из основных возможностей dapp и какой результат они дают.
Обновлено 13 августа 2019 г.: в настоящее время проект dapp переименован в werf, его код переписан на Go, а документация значительно улучшена.
Сразу оговорюсь, что 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 (см. документацию по установке werf) и склонировать себе репозиторий приложения:
Описывающий сборку Dappfile — файл с синтаксисом DSL на Ruby (в чём-то похоже на Vagrantfile , но планируем перейти на YAML с сохранением поддержки старого формата). Его содержимое:
Также нужно добавить start.sh и сделать его исполняемым ( chmod +x start.sh ):
Собрать образ приложения можно командой:
А запустить так:
Особенности: кэш, git patch, стадии сборки
Сборка выдаёт длинный лог со всеми совершаемыми действиями. Листинг очень большой, поэтому его часть выложена отдельно на GitHub. Если теперь запустить команду dapp dimg build ещё раз, то эти действия выполняться повторно не будут, т.к. результат их работы закэширован. Лог повторного запуска можно увидеть здесь. Его фрагмент:
Видны строки с именем стадии и результатом [USING CACHE] — это означает, что dapp не стал выполнять описанные действия, создавая новый слой образа, а использовал уже существующий.
Теперь притворимся разработчиком и внесём правки — например, изменим текст ссылки в шапке страницы в шаблоне app/Resources/views/base.html.twig . Сделаем коммит и попробуем собрать приложение. Видно, что наложился только git patch , т.е. новый образ был создан на основе закэшированных слоёв, к которым добавились изменения в файлах проекта:
Это ускорение сборки хорошо работает для файлов, которым не нужна какая-то особая обработка. Что же делать, если поменяется, например, 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 patсh применился после стадии install и образ был пересобран со стадии before_setup :
Такие связи между изменениями файлов в репозитории и стадиями позволяют уменьшить общее время сборки. В большинстве случаев зависимости приложения добавляются или изменяются не очень часто. Например, разработчик реализует новую фичу, из-за которой потребовалось добавить зависимость в 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 (обновлено 13 августа 2019 г.: воспользуйтесь примером сборки с помощью werf).
Dappfile будет такой:
Собрать и запустить можно всё теми же командами:
Чтобы не потеряться среди вывода docker images , протегируем итоговый образ:
Теперь можно уведить, что размер итогового образа не зависит от размера образа с инструментами сборки golang:
Теперь подробнее опишу Dappfile с применением artifact (особенности сборки приложения revel сознательно опускаю — если интересует, можно обсудить в комментариях). В общем случае структура будет такая:
- Секция artifact указывает, что часть приложения нужно собрать в отдельном образе, а директивой git внутри artifact можно указать, куда положить исходные тексты.
- export указывает, что нужно из отдельного образа скопировать в итоговый образ после выполнения команд стадии build_artifact .
- В export также можно указать, на какой стадии итоговому образу нужны результаты работы артефакта. В нашем случае указано after 'install' , т.е. после команд стадии install в итоговый образ будет скопирована директория /app из образа артефакта.
Итоги
Описанные возможности dapp: разбиение на стадии, зависимость стадий от изменений в файлах в Git-репозитории, использование artifact-образов — могут упростить и ускорить сборку практически любого приложения как в процессе CI/CD, так и для локальной разработки.
Но это только начало. В Dappfile можно описать сразу несколько образов и dapp dimg build их соберёт, а dapp dimg push --tag проставит тег и за'push'ит кэш-образы и итоговый образ в Registry. Эти возможности будут лучше проиллюстрированы в следующих статьях — в связке с описанием недавно появившейся в dapp поддержки деплоя в Kubernetes.