Эта статья — начало цикла о сборке 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 — по итогам голосования ниже.
Читайте также в нашем блоге:
Комментариев нет:
Отправить комментарий