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

o8movbrurpkgf2xtkyoihfukyqy.png

Эта статья — начало цикла о сборке 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):

kx3bxlnilzfag1tsgg1etirzhi8.png

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

3dulvsrrbj3hs3k7fhmhdiqdwdi.png

P.S.


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

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

  • «Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI»;
  • «Практика с dapp. Часть 1: Сборка простых приложений»;
  • «Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm»;
  • «Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp (обзор и видео доклада)»;
  • «Официально представляем dapp — DevOps-утилиту для сопровождения CI/CD».

© Habrahabr.ru