Хабрахабр

Сборка проектов с dapp. Часть 1: Java

Эта статья — начало цикла о сборке dapp'ом приложений на различных языках, платформах, технологических стеках. Предыдущие статьи про dapp (см. ссылки в конце материала) были больше обзорными, описывали возможности dapp. Теперь же пора поговорить более предметно и поделиться конкретным опытом работы с проектами. В связи с недавним релизом dapp 0.26.2 я заодно покажу, как описывать сборку в YAML-файле.

Описывать сборку буду на примере приложения из репозитория dockersamples — atsea-sample-shop-app. Это прототип небольшого магазина, построенный на React (фронт) и Java Spring Boot (бэкенд). В качестве БД используется PostgreSQL. Для большей похожести на рабочий проект добавлены реверсивный прокси на nginx и шлюз платежей в виде простого скрипта.

В статье опишу сборку только приложения — образы с nginx, PostgresSQL и шлюзом можно найти в нашем форке в ветке dappfile.

Сборка приложения «как есть»

После клонирования репозитория готовый Dockerfile для Java- и React-приложений можно найти по пути /app/Dockerfile. В этом файле определено два образа-стэйджа (в dapp это артефакт) и один финальный образ. В стэйджах собирается Java-приложение в jar и React-приложение в директорию /static.

FROM node:latest AS storefront
WORKDIR /usr/src/atsea/app/react-app
COPY react-app .
RUN npm install
RUN npm run build FROM maven:latest AS appserver
WORKDIR /usr/src/atsea
COPY pom.xml .
RUN mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve
COPY . .
RUN mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests FROM java:8-jdk-alpine
RUN adduser -Dh /home/gordon gordon
WORKDIR /static
COPY --from=storefront /usr/src/atsea/app/react-app/build/ .
WORKDIR /app
COPY --from=appserver /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]

Для начала переделаю этот файл «как есть» в «классический» Dappfile, а затем — в dappfile.yml.

Dappfile получается более многословным за счёт Ruby-блоков:

dimg_group do artifact do # артефакт для сборки Java-приложения docker.from 'maven:latest' git do add '/app' do to '/usr/src/atsea' end end shell do install do run 'cd /usr/src/atsea' run 'mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve' run 'mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests' end end export '/usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar' do to '/app/AtSea-0.0.1-SNAPSHOT.jar' after :install end end artifact do # артефакт для сборки React-приложения docker.from 'node:latest' git do add '/app/react-app' do to '/usr/src/atsea/app/react-app' end end shell do install do run 'cd /usr/src/atsea/app/react-app' run 'npm install' run 'npm run build' end end export '/usr/src/atsea/app/react-app/build' do to '/static' after :install end end dimg 'app' do docker.from 'java:8-jdk-alpine' shell do before_install do run 'mkdir /app' run 'adduser -Dh /home/gordon gordon' end end docker do entrypoint "java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar" cmd "--spring.profiles.active=postgres" end end
end

«Классический» Dappfile — это вариант с export в artifact, который был доступен в dapp до февральских релизов. Он отличается от директивы COPY --from в Dockerfile тем, что именно в артефакте указывается, что и куда нужно скопировать, а не в описании финального образа. Так проще описывать примерно одинаковые образы, в которые нужно скопировать один результат сборки чего-либо. Теперь же, с версии 0.26.2, dapp поддерживает механизм import, который даже предпочтительней использовать (пример его использования см. ниже).

И ещё один комментарий к файлу. При сборке через docker build в Docker Engine отправляется контекст. Обычно это директория, где лежит Dockerfile и исходные тексты приложения. В случае с dapp контекст — это Git-репозиторий, по истории которого dapp вычисляет изменения, произошедшие с последней сборки, и меняет в финальном образе только то, что изменилось. То есть аналог директивы COPY без --from в Dockerfile ­— это директива git, в которой описывается, какие директории или файлы из репозитория нужно скопировать в финальный образ, куда положить, какого владельца назначить. Также здесь описывается, от каких изменений зависит пересборка, но об этом чуть позже. Пока что давайте посмотрим, как выглядит та же сборка в новом синтаксисе YAML:

artifact: appserver
from: maven:latest
git: - add: '/app' to: '/usr/src/atsea'
shell: install: - cd /usr/src/atsea - mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve - mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests
---
artifact: storefront
from: node:latest
git: - add: /app/react-app to: /usr/src/atsea/app/react-app
shell: install: - cd /usr/src/atsea/app/react-app - npm install - npm run build
---
dimg: app
from: java:8-jdk-alpine
shell: beforeInstall: - mkdir /app - adduser -Dh /home/gordon gordon
import: - artifact: appserver add: '/usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar' to: '/app/AtSea-0.0.1-SNAPSHOT.jar' after: install - artifact: storefront add: /usr/src/atsea/app/react-app/build to: /static after: install
docker: ENTRYPOINT: ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"] CMD: ["--spring.profiles.active=postgres"]

Всё довольно похоже на «классический» Dappfile, но есть несколько отличий. Во-первых, разрабатывая YAML-синтаксис, мы решили отказаться от наследования и вложенности. Как показала практика, наследование было слишком сложной фичей и время от времени приводило к непониманию. Линейный файл — такой, как Dockerfile — гораздо понятнее: он больше похож на скрипт, а уж скрипты понимают все.

Во-вторых, для копирования результатов артефактов теперь используется import в том dimg, куда нужно поместить файлы. Добавилось небольшое улучшение: если не указать to, то путь назначения будет таким же, как указано в add.

На что обратить внимание при написании Dappfile? Распространённой практикой в проектах с Dockerfile является раскладывание разных Dockerfile по директориям и поэтому пути в директивах COPY указываются относительно этих директорий. Dappfile же один на проект и пути в директиве git указываются относительно корня репозитория. Второй момент — директива WORKDIR. В Dappfile директивы из семейства docker выполняются на последнем шаге, поэтому для перехода в нужную директорию на стадиях используется вызов cd.

Улучшенная сборка

Сборку Java-приложения можно разбить как минимум на два шага: скачать зависимости и собрать приложение. Первый шаг зависит от изменений в pom.xml, второй — от изменений в java-файлах, описателях, ресурсах— в общем можно сказать, что изменение в директории src должно приводить к пересборке jar’а. Dapp предлагает 4 стадии: before_install (где нет исходников) и install, before_setup, setup (где исходники уже доступны по путям, указанным в директивах git).

Скачивание зависимостей можно сделать более агрессивным, указав для maven цель dependency:go-offline вместо dependency:resolve. Это может быть оправданным решением, т.к. pom.xml меняется не очень часто, а dependency:resolve не скачивает всё и на этапе сборки приложения будут обращения в Maven-репозиторий (central или в ваш nexus/artifactory/…).

Итого, шаг скачивания зависимостей можно вынести в стадию install, которая останется в кэше до момента изменений в pom.xml, а сборку приложения — вынести в стадию setup, прописав зависимости от изменений в директории src.

artifact: appserver
from: maven:latest
git: - add: /app to: /usr/src/atsea stageDependencies: install: ['pom.xml'] setup: ['src']
shell: install: - cd /usr/src/atsea - mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:go-offline setup: - cd /usr/src/atsea - mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests

Сборка React-приложения также может быть разбита на два шага: скачивание зависимостей на стадии install и сборка приложения на стадии setup. Зависимости описываются в /app/react-app/package.json.

artifact: storefront
from: node:latest
git: - add: /app/react-app to: /usr/src/atsea/app/react-app stageDependencies: install: ['package.json'] setup: ['src', 'public']
shell: install: - cd /usr/src/atsea/app/react-app - npm install setup: - cd /usr/src/atsea/app/react-app - npm run build

Обращаю внимание, что пути в stageDependencies указываются относительно пути, указанного в add.

Коммиты и кэш

Теперь посмотрим, как работают stageDependencies. Для этого нужно сделать коммит с изменением в java-файле и запустить сборку dapp dimg build. В логе будет видно, что собирается только стадия setup:

Setup group Git artifacts: apply patches (before setup) ... [OK] 1.7 sec signature: dimgstage-atsea-sample-shop-app:e543a0f90ba39f198b9ae70a6268acfe05c6b3a6e25ca69b1b4bd7414a6c1067 Setup [BUILDING]
[INFO] Scanning for projects...
[INFO] [INFO] ------------------------------------------------------------------------
[INFO] Building atsea 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------ ...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 39.283 s
[INFO] Finished at: 2018-02-05T13:18:47Z
[INFO] Final Memory: 42M/355M
[INFO] ------------------------------------------------------------------------ Setup [OK] 46.71 sec signature: dimgstage-atsea-sample-shop-app:264aeb0287bbe501798a0bb19e7330917f3ec62b3a08e79a6c57804995e93137 commands: cd /usr/src/atsea mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests building artifact `appserver` [OK] 49.12 sec

Если изменить pom.xml, сделать коммит и запустить сборку, то будет пересобрана стадия install со скачиванием зависимостей и затем стадия setup.

Зависимости

Разделение сборки на два шага для Java-приложения закэшировало зависимости и теперь образ стадии install выполняет роль хранилища зависимостей. Однако dapp предоставляет возможность подмонтировать директорию для такого рода хранилищ. Монтировать можно из временной директории tmp_dir, время жизни которой — одна сборка, можно из build_dir — это постоянная директория, уникальная для каждого проекта. В документации приведены директивы для Dappfile, а в случае нашего приложения покажу, как добавить монтирование поддиректории из build_dir в dappfile.yml:

 artifact: appserver from: maven:latest
> mount:
> - from: build_dir
> to: /usr/share/maven/ref/repository git: ... shell: install: ...

Если не указать флаг --build-dir, то dapp в качестве build_dir создаёт директорию ~/.dapp/builds/<имя проекта dapp>. В build_dir после сборки появляется директория mount, в которой будет дерево монтируемых директорий. Имя проекта вычисляется как имя директории, в которой содержится Git-репозиторий. Если собираются проекты из одноимённых директорий, то имя проекта можно указать флагом --name, либо явно указывать разные директории с помощью флага --build-dir. В нашем случае имя dapp будет вычислено из директории, где хранится Git-репозиторий проекта и потому будет создан ~/.dapp/builds/atsea-sample-shop-app/mount/usr/share/maven/ref/repository/.

Запуск через compose

Ранее об этом не упоминалось, но можно использовать dapp для сборки, а запускать проект для проверки с помощью docker-compose. Для запуска понадобится сделать теги для образов и поправить docker-compose.yml, чтобы использовались образы, собранные dapp'ом.

Самый простой способ протегировать образы — запустить команду dapp dimg tag без флагов (другие способы и схемы именования образов есть в документации). Команда выведет на экран имена образов с тегом latest. Теперь нужно поправить docker-compose.yml: убрать директивы build и добавить директивы image с именами образов из вывода dapp dimg tag.

Например:

 payment_gateway: image: atsea-sample-shop-app/payment-gateway

Теперь проект можно запустить командой docker-compose up (если build по какой-либо причине остались, то поможет флаг --no-build):

Сайт доступен по адресу localhost:8080:

P.S.

В следующей части статьи мы расскажем о сборке приложения на… PHP или Node.js — по итогам голосования ниже.

Читайте также в нашем блоге:

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть