company_banner

Сборка проектов с 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 — по итогам голосования ниже.

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

    Only registered users can participate in poll. Log in, please.

    Про сборку какого приложения вам будет интереснее прочитать в следующей части?

    • 44%PHP11
    • 56%Node.js14
    • +19
    • 4.7k
    • 2
    Флант
    238.38
    Специалисты по DevOps и Kubernetes
    Share post

    Comments 2

      0

      На mount.from есть какие-то ограничения? Можно указать произвольный системный или пользовательский каталог, который бы расшаривался бы между всеми билдами всех проектов. Например, кеш пакетов apt, npm, composer? В классическом docker build такой возможности нет и сильно раздражает постоянное скачивание. В обсуждениях, например, https://github.com/moby/moby/issues/14080, видно, что фича запрашиваемая, но реализация вряд ли будет из-за соображений безопасности.

    Only users with full accounts can post comments. Log in, please.