Удивительно, но факт — дистрибьюция Java приложений в 21 веке по прежнему огромный костыль. Разработчики до сих пор придумывают способы вроде rsync/copy-paste/wget для установки java приложений на сервер. И только монструозные enterprise production ready платформы иногда позволяют сделать чуть больше — откатить приложение на предыдущую версию. В этой статье я хотел бы рассказать о доступном и простом способе организации дистрибьюции.

deb и apt


В мире существует множество действительно гигантских репозиториев приложений и инструментов по их дистрибьюции. Самые большие, по ощущениям, — это AppStore, Google Play, Debian/Ubuntu репозитории и CentOS/Fedora YUM репозитории. Например в Ubuntu репозитории для версии 15.04 содержится около 90000 приложений (без учета различных версий). Почему бы не воспользоваться провернной временем системой и для дистрибьюции Java приложений? Тем более, что:
большинство серверов и так используют Debian/Ubuntu
— проверенный временем инструмент: первый релиз был 16 лет назад
— нативная поддержка в операционной системе

Для начала немного о системе дистрибьюции приложений в Debian/Ubuntu. Она состоит из двух основных частей:
— deb пакеты
— apt (Advanced Package Tool) инструменты

deb пакеты


deb это бинарный дистрибутив приложения. Он состоит из 3 основных частей:
— метаинформация. Производитель, версия, зависимости на другие пакеты (очень похоже на maven), описание и пр.
— непосредственно приложение. .tar.gz архив
— (опционально) скрипты, которые будут выполняться во время установки

Структура .tar.gz архива может быть абсолютно произвольной. Однако, чтобы Ваше приложение было похоже на все остальные приложения системы, оно должно следовать структуре каталогов Debian/Ubuntu:
— /etc — конфиги
— /etc/init.d/ — скрипты запуска демонов
— /usr/bin — исполняемые файлы
— /usr/lib — библиотеки
— /var/log — логи

В зависимости от Вашего приложения каталоги могут немного отличаться, но общая структура надеюсь понятна.

Еще одной важной особенностью deb пакетов является возможность запускать скрипты во время установки. Эти скрипты тоже хранятся в deb пакете и имеют стандартное именование. Каждый скрипт может выполняться в определенную фазу установки. Установка пакета делится на несколько фаз:
— preinst
— inst
— postinst
— prerm
— rm
— postrm

Существует множество различных промежуточных фаз и различные комбинации состояния инсталляции. Нас они мало интересуют, но тем кто хочет разобраться можно почитать официальную документацию. Обычно эти скрипты настраивают ротацию и архивирование логов, задают начальные значения конфигураций (например, root-пароль для mysql). Если же у вас конечное бизнес-приложение, то лучше взять какое-нибудь нормальное средство автоматизации вроде Ansible, Chief, Puppet.

apt


apt — это набор инструментов для работы с deb пакетами. Он позволяет:
— конфигурировать репозитории и работать с ними: добавлять, удалять, менять, обновлять индекс
— управлять пакетами: устанавливать, удалять, обновлять, искать

apt репозиторий в упрощенном виде — это HTTP сервер, который раздает deb пакеты. У него есть индекс (файл), который доступен по стандартному пути и непосредственно бинарники, путь к которым находится в индексе.

Связывая все воедино


После того, как стала понятна примерная схема работы связки deb + apt, можно попробовать их съинтегрировать. Примерная схема такая:
— создания deb пакета в фазе package
— публикация получившегося пакета в фазе deploy

Для этого есть несколько maven плагинов.

jdeb


Схема его работы достаточно проста:
— перечислить файлы и директории, которые попадут в результирующий .tar.gz архив
— указать пермиссии

Более полная документация о возможностях плагина можно узнать на официальной странице.

apt-maven-plugin


Работает с репозиторием заданным в distributionManagement как с apt репозиторием, а не maven репозиторием. Хотя ничего не мешает их использовать одновременно под одним url. Их layout'ы совместимы между собой.

Пример конфигурации выглядит следующим образом:

<plugin>
	<groupId>com.aerse.maven</groupId>
	<artifactId>apt-maven-plugin</artifactId>
	<version>1.5</version>
	<executions>
		<execution>
			<id>deploy</id>
			<goals>
				<goal>deploy</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<component>main</component>
		<codename>strepo</codename>
	</configuration>
</plugin>

И секция distributionManagement (ничего необычного):

<distributionManagement>
	<repository>
		<id>maven-release-repo</id>
		<url>http://example.com/maven</url>
	</repository>
</distributionManagement>

После выполнения фазы deploy, example.com/maven станет еще и apt репозиторием. И можно смело писать:

sudo add-apt-repository "deb http://example.com/maven strepo main"
sudo apt-get update
sudo apt-get install <artifactId>

Немного любимого enterprise


Манца любого java-enterprise разработчика звучит следующим образом:
— security
— stability
— high availability production ready platform

Все это отлично решается, если устроить apt репозиторий из самого популярного хостинга для разработчиков: s3. Вкупе с cloudfront, он даёт гарантию 99.9% надёжности и географическую распределённость.

Делается это опять же достаточно просто. Надо подключить плагин для работы с s3:

<build>
	<extensions>
		<extension>
			<groupId>org.springframework.build</groupId>
			<artifactId>aws-maven</artifactId>
			<version>5.0.0.RELEASE</version>
		</extension>
	</extensions>
	...
</build>

Поменять url в секции distributionManagement на имя bucket'a:

<distributionManagement>
	<repository>
		<id>maven-release-repo</id>
		<url>s3://example.bucket</url>
	</repository>
</distributionManagement>

И настроить доступ к вашему bucket'у:

<servers>
	<server>  
		<id>maven-release-repo</id>  
		<username>apikey</username>  
		<password>apisecret</password>  
	</server>
	...
</servers>

На конечных серверах для доступа к такому репозиторию существует специальный плагин: apt-transport-s3. К сожалению его еще нет в официальных репозиториях, поэтому необходимо добавить вручную один из репозиториев, где он содержится:

sudo add-apt-repository ppa:leonard-ehrenfried/apt-transport-s3
sudo apt-get install apt-transport-s3

После чего можно уже указывать наш s3 репозиторий:

sudo add-apt-repository "deb s3://apikey:apisecret@s3.amazonaws.com/example.bucket strepo main"

Итого


В результате всех манипуляций установка приложения:
— mvn clean deploy

На любом Debian/Ubuntu сервере в любой точке мира:
— apt-get update
— apt-get install artifactId

Комментарии (43)


  1. eaa
    18.08.2015 14:57

    Хм, по-моему так вообще не важно, java или бинари или еще что-то: в линукс-подобных системах все ставится через пакеты.
    Всегда собирал java в пакеты RPM через связку ant + redline, все пучком.
    Про костыли «rsync/copy-paste/wget» слышу от Вас впервые.


    1. zloddey
      18.08.2015 15:31
      +1

      Проблема в следующем: для linux-программиста эти вещи действительно очевидны. Но далеко не все Java-программисты являются linux-программистами. Мой личный опыт как раз включает в себя весёлые приключения с уговорами (к счастью, успешными) завернуть дистрибуцию продуктов в нормальные пакеты под используемый дистрибутив.

      Причём, стоит добавить, что rsync/wget ещё продвинутые товарищи используют. В запущенных случаях приходилось видеть и ручное перекладывание jar-ников на сервер. Вот уж где настоящий угар!


      1. guai
        18.08.2015 19:52
        -2

        Ява-программисты привыкли избегать платформозависимых инструментов. Инсталляторов и на яве хватает.


        1. eaa
          18.08.2015 20:03
          +1

          Ага, Eclipse идет на убунте почему-то в виде .deb
          Android studio для винды — в виде .exe


          1. guai
            18.08.2015 20:19

            Захотели покрыть все платформы родными инсталлерами — покрыли. Но если делается 1 инсталлер, то чаще явавский, или вообще архивчик, JAVA_HOME прописан — больше ничего и не надо.


            1. eaa
              18.08.2015 20:30
              +2

              Ох, я помню, когда совсем новичком пробовал поставить что-то явовское на комп… сколько я матом крыл то, что надо кучу всего прописывать в переменных, часто одним JAVA_HOME дело не ограничивается.

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


              1. guai
                18.08.2015 20:38

                нормальный обычный пользователь линукса, умеющий apt/yum, я думаю, яву тоже осилит


                1. eaa
                  18.08.2015 20:40
                  +4

                  Я, как нормальный пользователь, не хочу ничего осиливать, я хочу пользоваться инструментами.


                  1. guai
                    18.08.2015 20:54
                    +1

                    И поэтому все должны писать софт под те инструменты, которые вы знаете, так что ли?
                    Для человека, не знающего ни яву, ни yum, разбираться придется одинаково, что с архивчиком, что с юмом. С юмом кстати косяков может быть больше, если нет сетки, если за прокси, если нет пряв на yum и т.п.
                    Вы наверн никогда не сидели с жестко зарезанными правами под линуксом. Я сидел, с тех пор даже блокнот явавский привык язать, потому что возможность просто укачать архивчик сложнее зарезать.


                    1. eaa
                      18.08.2015 20:59

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

                      И обычному пользователю вообще не надо знать yum/apt-get, есть вполне адекватные графические оболочки под все это дело, любое приложение ставится за пару кликов мышкой. Если оно конечно адекватно написано.


          1. nehaev
            18.08.2015 21:44
            +1

            О, эклипс это богатая тема.

            1. В моей убунте (14.04 LTS) из репозитория доступен только Eclipse 3.8, в то время как актуальная версия — 4.5. Сидеть на версии трехлетней давности? Даже PPA для 4.5 что-то не нашелся сходу.

            2. На сайте эклипса для линукс можно скачать только tar.gz. Похоже, как верно отметил автор предыдущего коммента, разработчики избегают платформозависимых инструментов.

            3. Для разных проектов мне нужны разные версии эклипса. Что я делаю сейчас? Скачиваю несколько архивов и распаковываю в папки, которые выбираю сам. Больше ничего делать не надо, разные эклипсы с разными конфигурациями запускаются из разных папок без проблем. Насколько просто достичь того же эффекта deb-пакетами?

            4. Страшная вещь, но внутри эклипса есть своя система дистрибуции пакетов — Eclipse Marketplace. И работает она не на deb-пакетах. Потому что с точки зрения джава, jar — это более универсальный формат распространения программ.


            1. eaa
              18.08.2015 22:38

              Все становится намного сложнее, когда в проекте еще есть с/с++ код, который, конечно, можно вынести отдельно, но его тоже надо как-то ставить на ОС.
              Можно городить зоопарк, что бинари ставим через .deb, java — через .zip, при этом как в .zip указать правильную зависимость от нужного бинарного .deb — это никак. Т.е. надо изобретать какую-то свою систему зависимостей, либо паковать java опять же в .deb и использовать готовую. Ну или наоборот через maven таскать сишные скомпиленные .so.

              В общем, подходов масса, если проект чисто на java — там одно, если он смешанный — то все уже идет по другому сценарию.
              Все пакеты (jar, war, zip, deb, rpm) на самом деле работают на разных уровнях, юзеру надо одно, админу на сервере может быть проще другое.


            1. Lure_of_Chaos
              19.08.2015 09:57

              есть новый www.eclipse.org/downloads/installer.php, который упрощает жизнь по 3ему, немного по 2 и 1 пунктам, ну и частично 4


  1. allnightlong
    18.08.2015 16:30
    -2

    chief?


  1. nehaev
    18.08.2015 16:37
    +1

    А насколько легко предложенный способ позволяет иметь несколько версий приложения на одном сервере?


    1. eaa
      18.08.2015 16:56

      Вообще при установке .deb можно задать каталог, куда ставить

      --instdir=dir
          Change default installation directory which refers to the directory where packages are to be installed. instdir
          is also the directory passed to chroot(2) before running package's installation scripts, which means  that  the
          scripts see instdir as a root directory.  (Defaults to /)
      


      Вообще, это вопрос архитектуры, должно ли вообще быть несколько версий, как они будут взаимодействовать между собой, и вопрос отнюдь не ограничивается только системой упаковки.
      Например, несколько версий апача на одной машине… как они порты делить будут между собой и вообще зачем такой изврат? Может вообще контейнерами делать?


      1. nehaev
        18.08.2015 18:19

        > Вообще при установке .deb можно задать каталог, куда ставить

        Т.е. можно одновременно иметь в системе две версии одного пакета в разных instdir, правильно?

        > Например, несколько версий апача на одной машине…

        Что за апач? Мы вроде про джаву говорим…

        > Вообще, это вопрос архитектуры, должно ли вообще быть несколько версий, как они будут взаимодействовать между собой, и вопрос отнюдь не ограничивается только системой упаковки.

        Ну, например, в случае с rsync/wget или деплоем с дженкинса по SSH, вопрос с системой упаковки вообще не стоит.


        1. eaa
          18.08.2015 19:51
          -1

          1. Да, можно
          2. Не важно, апач или нет, вообще без разницы, но если Вы настаиваете, возьмите два сервиса на java, которые слушают один и тот же порт и подумайте, как быть с разными версиями одного сервсиса при их одновременной установке. И таких проблем надо решать множество и сборка — ото очень малая часть этих проблем. Как собирать — вытекает из архитектуры приложения в целом.
          3. Вы еще предложите конечному пользователю из исходников собирать по несколько часов, да еще вручную, запуская javac, тогда не только упаковка не нужна, тогда и maven/ant не нужны вообще.


          1. shuron
            20.08.2015 23:24

            Это гемор. Яву дистрибьютить в пакетных мэнеджерах было круто лет 5 назад.
            Сейчас, в пору микросервисов, пакеты начинают упираться в новые границы… Контейнеры уже мэйнстрим… там одинаковый порт никого не пугает, даже захардкоденый. ;)


            1. dernasherbrezon
              21.08.2015 15:56

              И как же докер решает проблему одного и того же порта? Проблема просто переносится уровнем выше. В каждом из двух контейнерах можно держать 80 порт, но как их bind'ить в хост системе?


              1. shuron
                07.09.2015 19:20

                Да конечно, но вам не надо думать он этом при разработке а при деплойменте…
                А если вы деплоите в нормальный клауд то и там не надо… ;)


    1. shuron
      20.08.2015 23:21

      Отличный вопрос…
      Это фундаментальное отличие пакетных менеджеров от контейнеров…
      Если у вас столь динамичные требования то сразу смотрите например в сторону Докера…


  1. voidnugget
    18.08.2015 16:44
    +1

    Эм… ну вообщем-то Ынтырпрайс это конечно хорошо, но фраза

    Разработчики до сих пор придумывают способы вроде rsync/copy-paste/wget для установки java приложений на сервер.
    очень смутила.

    С чего вы взяли что сейчас Разработчики для деплоя используют Java инфраструктуру?
    Обычно это готовые системы оркестрации типа Puppet / Ansible / Saltstack / Chef.
    На худой конец можно спокойно всё заскриптовать на groovy и покрыть интеграционными тестами.

    Для того что бы откатить версию или сделать hot reload какого-то сервиса без даунтайма: нужно создать дочерний и сделать dup() syscall для текущих открытых сокетов (это позволит обработать флаг FD_CLOEXEC) и передать в SIGHUP родителю afaik, но могу ошибаться с сигналом. Непосредственно к языку или платформе фича hot reload'a и отката в продакшене не имеет прямого отношения — росли бы руки с нужного места, и было бы желание разобраться как оно работает…

    Имхо, Ынтырпрайсовские java бутерброды ничего общего с понятием производительности не имеют.


    1. dernasherbrezon
      18.08.2015 17:28
      +1

      • Puppet/Ansible/Sailstack/Chief ничего не знают о зависимостях и транзитивных зависимостях проекта. Maven знает.
      • откуда эти системы будут загружать дистрибутив приложения?
      • в настоящее время java не умеет ловить SIGHUP


      1. eaa
        18.08.2015 17:33

        Одной из задач процесса билда в случае deb/rpm является выявление транзитивных зависимостей и прописывание их в зависимости deb/rpm пакетов.


      1. voidnugget
        18.08.2015 18:32
        +1

        1. Им и не нужно знать — они подгружают эту информацию с maven / gradle.
        2. Системы деплоя и оркестрации имеют встроенные средства дистрибуции: ведения репозиториев и сборки пакетов под различные дистрибутивы, не deb'ом единым.
        3. Есть sun.misc.SignalHandler и можно делать так

        Signal.handle(new Signal("HUP"), new SignalHandler() {
            public void handle(Signal signal) {
                reloadService();
            }
        });
        


        1. dernasherbrezon
          19.08.2015 13:46

          • Ansible не умеет подтягивать транзитивные зависимости. Chef не умеет складывать все транзитивные зависимости в одно место. Puppet скачивает весь Maven, чтобы потом скачать нужные артефакты.
          • Ansible, Chef, Puppet — это в простейшем случае скрипты. Они не хранят внутри себя бинарников приложений. Где это репозитории будут находиться?
          • sun.misc.* — это проприетарное API, которое не рекомендуется использовать и которое в будущем может быть удалено


  1. vsb
    18.08.2015 20:53
    +2

    Для 99% проектов это ненужные лишние действия. Остановил сервер, удалил webapps/ROOT, создал его, распаковал туда jar-ник, опционально скопировал поверх конфиги (а лучше их хранить в другом месте), запустил сервер. Скрипт пишется за 10 минут, работает железобетонно.


  1. amarao
    18.08.2015 21:46
    +4

    Описаны не настоящие deb'ы. Настоящие должны иметь сырцы (source deb) и воспроизводиться при сборке из сырцов с помощью dpkg-buildpackage или им подобным. Должны быть правильные changelog, секции установки и помеченные конфиги.

    Всё это можно не делать — и будет халтура обыкновенная.


    1. dernasherbrezon
      18.08.2015 23:26

      К сожалению, да. Классического debian-way здесь нет. Но:

      1. Большинство проектов на java — проприетарные системы. Все остальные уже имеют deb пакеты и собственных maintainer'ов
      2. deb начинался разрабатываться 16 лет назад. Это одновременно и плюс: множество необходимого функционала уже есть, и минус — множество устаревшего функционала есть. Если Вам нужны исходники в apt репозитории то это не проблема — можно создать pull request в maven-apt-plugin и поработать над улучшением интеграции


  1. Mistx
    19.08.2015 09:50

    Это с каких это пор в Debian\Ubuntu логи стали хранить в /usr/log? Всегда хранились в /var/log


    1. dernasherbrezon
      19.08.2015 13:32

      Обновил


  1. Lure_of_Chaos
    19.08.2015 10:03

    А если надо собрать rpm для других линуксов, не дебиановских?


    1. eaa
      19.08.2015 10:59

      Под это дело есть соответствующие плагины, типа redline для ant (может он есть и под maven, я не искал за ненадобностью).


  1. grossws
    19.08.2015 20:24

    У меня возникают некоторые сомнения в качестве работы maven-plugin'а, не соблюдающего даже базовые рекомендации об именовании:

    Important Notice: Plugin Naming Convention and Apache Maven Trademark

    You will typically name your plugin <yourplugin>-maven-plugin.

    Calling it maven-<yourplugin>-plugin (note «Maven» is at the beginning of the plugin name) is strongly discouraged since it's a reserved naming pattern for official Apache Maven plugins maintained by the Apache Maven team with groupId org.apache.maven.plugins. Using this naming pattern is an infringement of the Apache Maven Trademark.


    1. grossws
      21.08.2015 20:05

      Дополнение: автор быстро пофиксил проблему (см. issue#1).

      Предыдущий комментарий получился довольно резким, но, надеюсь, автор меня простит. Да, dernasherbrezon?)


      1. dernasherbrezon
        21.08.2015 20:37

        Без проблем. Всё было по существу.


  1. artspb
    10.09.2015 21:43

    В одном из проектов использовали схему war-в-deb. Мне она показалась достаточно удобной, хотя со скриптами пришлось изрядно повозиться.


  1. shuron
    13.09.2015 16:02

    Вот тут хорошо описано почему пакеты уже морально устарели по сравенению с Докером


    1. dernasherbrezon
      16.09.2015 00:00

      Отличная статья для дискуссии!
      Насколько я понял сравнивается только одна сторона: dependency hell. Мне же кажется что с докером ситуация будет просто еще хуже.

      Допустим есть 3 контейнера с руби. Руби разных версий внутри. И вот однажды выходит CVE-2015-3900. Каким образом обновить руби на всех трех контейнерах?

      Или вот еще пример про зависимости: есть все те же 3 контейнера с руби разных версий внутри. И внезапно происходит что то магическое связанное с ipv6 и резолвингом хостнейма. Вопрос: сколько приложений в контейнерах упадут? И что потом делать с контейнерами в которых настолько старый руби в котором этой баги еще нет? Добавить в календарь после каждого релиза «проверить тот древнющий контейнер»?

      Вывод такой: если хочется разных версий одного и того же продукта, то значит в консерватории что то не то.


      1. shuron
        20.09.2015 17:43
        +1

        Не знаю насколько это серьезная проблема… с руби.
        Но да если эта проблема открывет брешь в контейнер придется обновить базовый контейнер и перестроить все что на него опираются…
        Ну это как-бы не то чего я боюсь с рабочим CI/CD.

        Вывод такой: если хочется разных версий одного и того же продукта, то значит в консерватории что то не то

        Ерунда. Да и разговор не оразных версиях, руби или явы…
        А о разных версиях вашего приложения. Например в рамках «canary testing»

        П.С. Не только dependency hell
        иметь разделение Export services via port binding
        Тоже правильная вешь…
        Дело в том что она контролируемая, например можно повесить хуки и оповестить кого нужно автоматически (Прокси, конзьюмнеров, мониторинг). Тоесь все то что делают возможным клауд.


        1. dernasherbrezon
          22.09.2015 00:09

          Секунду, секунду. Давайте обо всем по-порядку:

          • Перестройка контейнера и CI/CD никак не помогут против security обновлений. Как вы узнаете, что bash в вашем контейнере нужно обновить? А заодно обновить все, что на него линкуется.
          • «canary testing». Есть два варианта:
            1. Вы делаете его в разных контейнерах на одном физическом хосте и имеете проблему пересечения портов. Ее можно решать двумя конфигурациями: «real prod» и «canary testing» с непересекающимися портами, дисками и путями. Поправьте меня, если это делается как-то по-другому.
            2. Вы делаете его на двух физически разных хостах. Тогда это ничем не отличается от:

              sudo apt-get install exampleapp=1.3.2-canary1
              

              на определенном хосте. Или даже больше — можно определить experimental репозиторий, добавить его на определенные хосты и на этих хостах обновлять всегда до последней версии. Делается это примерно в 3 команды:

              sudo add-apt-repository "deb s3://apikey:apisecret@s3.amazonaws.com/example.bucket strepo experimental"
              sudo apt-get update
              sudo apt-get install exampleapp
              
          • По поводу Export services via port binding. Не могу найти ничего противоречещее идеи пакетной сборки. Все то же самое можно делать и с пакетами.


          1. shuron
            24.09.2015 00:20

            Перестройка контейнера и CI/CD никак не помогут против security обновлений. Как вы узнаете, что bash в вашем контейнере нужно обновить? А заодно обновить все, что на него линкуется.

            В контенере вашь меньше всего интересует версиай башь и всего что на него линкуется… с наружи видно порт и сервис на него повешен…
            Проблема с башем — если сервис на баше написан ;) А это решемо ;)

            Вы делаете его в разных контейнерах на одном физическом хосте и имеете проблему пересечения портов. Ее можно решать двумя конфигурациями: «real prod» и «canary testing» с непересекающимися портами, дисками и путями. Поправьте меня, если это делается как-то по-другому.

            Вы не думаете о портах впринципе особенно в build-time
            И в идеале при деплойменте контейнра инфраструктура сама линкует новый контейнер к лоадбэленсеру…
            И вообще вы не думаете о том на каком хосте он запустится и вообще вы можете перестать думать хостами

            По поводу Export services via port binding. Не могу найти ничего противоречещее идеи пакетной сборки. Все то же самое можно делать и с пакетами.

            нет пакетами вы не можете стартануть два пакета с одним портом не конфигурируя что-то руками на каком-то хосте и препадавать это апликации (например пермеными окружения)
            Хотя даже инсталлировать один и тот-зех пакет разных версий уже не сможете ;)