Альтернативная версия упаковки Java приложения в Docker

Прочитав статью об упаковке JVM приложения в Docker, я остался в смятении. Я, как и любой разработчик, хочу чтоб мои волосы развевались на ветру контейнер собирался за одну команду, сразу, без лишних телодвижений. Чтобы конфигурация проекта и сборки была собрана в одном файле, и чтобы не было зависимостей от внешних систем.

Java приложения, как мы их видим

Далее последует альтернативный способ сборки Java-приложений в Docker контейнер используя Maven.

Для сборки нам понадобится Java проект, Maven и немного терпения, чтоб все это завести.

Дисклеймер
Я предполагаю, что читатель уже знаком Java, Maven и Docker. Представляет, что написано в Docker-файле, зачем все ему это надо и вообще ниндзя


В это раз, мы будем использовать Maven-плагин от fabric8io.

Предположим, что у нас микро-сервис, который упаковывается в jar-файл, содержащий main метод, который и надо запустить для старта приложения. Добавляем плагин в build секцию:

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.16.4</version>
    <executions>
        <execution>
            <id>Build docker container</id>
            <phase>package</phase>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
        <execution>
            <id>Push docker container</id>
            <phase>deploy</phase>
            <goals>
                <goal>push</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <images>
            <image>
                <name>${project.artifactId}</name>
                <registry>registry.io</registry>
                <build>
                    <from>java:8</from>
                    <tags>
                        <tag>${project.version}</tag>
                    </tags>
                    <ports>
                        <port>8080</port>
                    </ports>
                    <cmd>
                        <shell>java -cp 'jars/*' com.myapp.MainClass '/config.file'</shell>
                    </cmd>
                    <assembly>
                        <basedir>/</basedir>
                        <inline>
                            <dependencySet>
                                <outputDirectory>jars</outputDirectory>
                            </dependencySet>
                            <files>
                                <file>
                                    <source>${project.build.directory}/${project.build.finalName}.jar</source>
                                    <outputDirectory>jars</outputDirectory>
                                </file>
                                <file>
                                    <source>${project.basedir}/src/main/config/config.yml</source>
                                    <outputDirectory>/</outputDirectory>
                                </file>
                            </files>
                        </inline>
                    </assembly>
                </build>
            </image>
        </images>
    </configuration>
</plugin>

И, в общем то, все. Разберем поподробнее, что у нас там написано.

В начале идёт типичное определение плагина с версией. Далее идёт определение в какие фазы луны сборки мы хотим собирать и пушить наш контейнер. В моем случае сборка контейнера (build) будет происходить во время упаковки проекта (phase: package).

Пуш (push) контейнера в репозиторий будет происходить во время фазы деплой (deploy).

<executions>
    <execution>
        <id>Build docker container</id>
        <phase>package</phase>
        <goals>
            <goal>build</goal>
        </goals>
    </execution>
    <execution>
        <id>Push docker container</id>
        <phase>deploy</phase>
        <goals>
            <goal>push</goal>
        </goals>
    </execution>
</executions>

Внимание Maven!
Maven имеет скотскую привычку запускать различные плагины одной фазы в прядке их упоминания в effective pom. Построить effective pom можно следующей командой:
mvn help:effective-pom

Если Вам никакими ухищрениями не удается получить правильный порядок, то перенесите построение контейнера в следующую фазу: install, deploy.
Например в моем случае перенесение в фазу deploy выглядит так:

<executions>
    <execution>
        <id>Build & Push docker container</id>
        <phase>deploy</phase>
        <goals>
            <goal>build</goal>
            <goal>push</goal>
        </goals>
    </execution>
</executions>


Определяем имя образа и репозиторий:

<name>${project.artifactId}</name>
<registry>registry.io</registry>

Если репозиторий не указан, то будет использоваться репозиторий Docker-а. Документация для дальнейшего чтения.

Описание сборки образа осуществляется в секции <build>. По сути это отражение Docker файла в Maven.

from — базовый образ
tags — теги с которыми будет собран образ
ports — открытые порты
cmd — строка запуска, так же можно указать entryPoint, о все доступных опциях можно почитать в документации.

В моем примере:

<shell>java -cp 'jars/*' com.myapp.MainClass '/config.file'</shell>

Это типичная строка запуска Java приложения, завернутая в shell форму Docker-а, в представлении Maven-а.

assembly — самая интересная часть плагина. Она определяет, что будет упаковано в образ контейнера и как. Есть несколько вариантов как можно описать упаковку, я выбрал inline, но есть и другие варианты.

В примере, я хочу взять все зависимости проекта и сложить в папку «jars»:

<dependencySet>
    <outputDirectory>jars</outputDirectory>
</dependencySet>

А так же, артефакт проекта, собственно наше приложение, туда же, а конфигурационный файл в корень контейнера.

<files>
    <file>
        <source>${project.build.directory}/${project.build.finalName}.jar</source>
        <outputDirectory>jars</outputDirectory>
    </file>
    <file>
        <source>${project.basedir}/src/main/config/config.file</source>
        <outputDirectory>/</outputDirectory>
    </file>
</files>

Подробнее о возможностях ассемблирования в документации, на этот раз Maven-а.

И все! В результате:

  • Вся конфигурация сборки Docker контейнера у нас сосредоточенна в файле проекта.
  • Мы можем использовать все прелести Maven-а, его переменные и плагины.
  • Сборка контейнера осуществляется прозрачно, в момент сборки проекта, результат доступен локально.
  • Версионирование контейнера идет нога в ногу с нашим проектом.
  • Мы можем продолжать использовать все тот же CI/CD инструмент, что и раньше (надо установить Docker на сервере).

Несколько слов о плагине: разработка идет активно, мейнтейнер оперативно отвечает и охотно мерджит пул-реквесты.

» Сам плагин может ещё много чего: возможности
» Пример проекта выложен на github-е
Поделиться публикацией

Комментарии 18

    0

    Не пробовали на openjdk:8-jre-alpine запускать то же самое?

      0
      Руки пока, к сожалению, не дошли… по отзывам должно работать.
      +4
      Никогда так не делайте, пожалуйста.
      Смысл Dockerfile'a в том, что я могу взять и сделать такой же билд. В том, что система оркестрации (начиная от обычного docker-compose заканчивая Rancher) может взять и сбилдить контейнер. В том, что Jenkins + Docker plugin может билдить проект, в котором есть Dockerfile, пушить его в ваш/общедоступный Registry.

      А здесь Вы зачем-то делаете тоже самое, но в maven-конфигурации. Смысла в этом крайне мало, а может добавить гемора при деплое этого сервиса куда-то.
        0
        Эм, Вы и так можете взять проект и сделать такой же билд… ну и плюсы решения перечисленны в конце статьи.
          0
          Пример для Spring Boot в этом плане выглядит удачнее?
          +2
          в свое время юзал вот этот очень удобный плагин от sptofy — https://github.com/spotify/docker-maven-plugin
            +1
            Также можно перенести сбоку на фазу install. Может кто знает, как заставить Maven исполнять плагины в нужном порядке?

            Мавен не руководствуется порядком написания, у него есть lifecycle в котором определены фазы сборки, для плагинов дефолтные задаются через аннотации, например:
            io.fabric8.maven.docker.PushMojo
            ....
            @Mojo(name = "push", defaultPhase = LifecyclePhase.DEPLOY)
            @Execute(phase = LifecyclePhase.INITIALIZE)
            public class PushMojo extends AbstractDockerMojo {
            ...
            


            подробнее описано на https://maven.apache.org/developers/mojo-api-specification.html
            Так что здесь необходимо посмотреть на какие фазы прибит гоал плагина, ну и переопределить при необходимости разбросав на разные фазы.

            К тому же пуш контейнера в регистри вполне себе вписывается в фазу деплой, как мне кажется.
              0
              Проблема не совсем в этом. Проблемы начинаются когда два плагина должны исполняться в одной фазе, например: сборка контейнера и сборка jar-ников проекта. Можно столкнуться с ситуацией когда контейнер будет собираться до того как собраны jar-ники и все упадет с ошибкой. Железно работает перенос сборки контейнера в более позднюю фазу (install, deploy), но это как-то не аккуратненько.
                +2
                Если плагины запускаются в одной фазе, то они исполняются в порядке описания в pom.xml, если смотреть на твой пример, у тебя там сборка jar в принципе не задана, потому как она используется по дефолту, однако, в этом случае порядок исполнения плагинов стоит проверить через генерацию эффективного пома.
                  0
                  конкретно на примере указанного репозитория github-е:
                      <plugin>
                          <groupId>io.fabric8</groupId>
                          <artifactId>docker-maven-plugin</artifactId>
                          <version>0.16.4</version>
                          <executions>
                            <execution>
                              <id>Build docker container</id>
                              <phase>package</phase>
                              <goals>
                                <goal>build</goal>
                              </goals>
                  ....
                        <plugin>
                          <artifactId>maven-jar-plugin</artifactId>
                          <version>2.3.2</version>
                          <executions>
                            <execution>
                              <id>default-jar</id>
                              <phase>package</phase>
                              <goals>
                                <goal>jar</goal>
                              </goals>
                            </execution>
                          </executions>
                  ....
                  

                  build из докер плагина будет исполнен раньше запуска jar плагина
                    0

                    если быть точнее, то даже не в порядке объявления в pom.xml, а в порядке объявления всех Execution блоков в effective-pom. Просто допускается опускать блок execution если он один

                      0
                      оу, точно! спасибо!
                        0
                        я это и имел ввиду=)
                          0
                          спасибо вам обоим :-)
                  0
                  Не люблю большие pom файлы. Предпочитаю Dockerfile + CI (drone). Локально докер образ мне не нужен, докер это все же инструмент для поставки приложения (редко нужно запустить проверить). Еще раз написать версию в другом файле не сложно, хотя может быть источником ошибок, которые легко исправимы. Версионирование — единственный плюс. Ради этого плюс еще один плагин — нееет.
                    0
                    Мне кажется отличный способ расширить типы поставки приложения.
                      0
                      Версионированние может быть источником частых ошибок, на выяснение которых будет тратится много времени.
                      К примеру у нас в команде, сборка контейнера определена одни раз в родительском pom-e, куда заглядываешь не часто, а вот dokerfile прийдется хранить в каждом приложении. Так что файл проекта выходит не такой уж и большой.
                      Бонусом идет обновление сборки для всех проектов одновременно! А их много…

                      Более того возможность использовать maven большой плюс.
                      Например:
                      — Мы теггируем контейнер еще и ревизией гита, которая опять же легко поставляется мавеном.
                      — Каждый разработчик имеет свой собственный спейс в докер репозитории, так что мы не мешаем друг другу собирая и отлаживая рабочие версии, в том числе и на тест окружениях. Имя пользователя поставляется мавеном.
                      — Ну и может вам и ненужен контейнер локально, но очень часто для того чтоб запустить нечто, нужно поставить много разных пакетов, что не всегда хочется делать на локальной машине (мавеном ведь можно собирать не только ява приложения). И вот тогда локальный контейнер становится поистине спасением, не надо ничего качать и зависеть от подключения к сети/впн.
                      0
                      Вполне юзабельный плагин и спасибо за статью. Удалось прикрутить и использовать в рабочем проекте.

                      Из преимуществ — пересборка и запуск грозди контейнеров в нужном порядке до запуска интеграционных тестов, остановке и удаление всей этой кухни — после. Всё это — по одной команде mvn verify без внешних инструментов оркестрации и шелловских скриптов.

                      Из недостатков — разработчики плагина не всегда успевают догнать быстро развивающийся докер. Нужно потратить порядочно времени, чтобы сконфигурить плагин. Ну и сам факт использования лишнего звена в системе.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое