JavaRush /Java блог /Java-проекты /Реализуем развертывание приложения - "Java-проект от А до...
Roman Beekeeper
35 уровень

Реализуем развертывание приложения - "Java-проект от А до Я"

Статья из группы Java-проекты
Всем привет. Продолжаем цикл статей по написанию своего проекта. “Java-проект от А до Я”: Реализуем развертывание приложения - 1

Сортируем ветки

Из важного, чтобы не потеряться в ветках и их порядке в репозитории, я решил их переименовать, добавив приставку STEP_{number}. Например, у нас есть три ветки помимо главной:
  • JRTB-0
  • JRTB-2
  • JRTB-3
Сразу не поймешь, какая за какой должна идти. Поэтому я их переименую следующим образом:
  • STEP_1_JRTB-0 — первый шаг
  • STEP_2_JRTB-2 — второй шаг
  • STEP_3_JRTB-3 — третий шаг
И так далее для следующих статей. Чтобы переименовать ветки, заходим на страницу репозитория, находим плашку branches, переходим по ней:“Java-проект от А до Я”: Реализуем развертывание приложения - 2Под каждой веткой нажимаем на карандашик и переименовываем ветку:“Java-проект от А до Я”: Реализуем развертывание приложения - 3И в результате получим:“Java-проект от А до Я”: Реализуем развертывание приложения - 4Кстати, все, кто подписан на мой телеграм-канал, узнали сразу же, что я переименовал ветки.

Немного о докере

Что такое Docker? Вкратце — это инструмент, с помощью которого можно быстро и безопасно развертывать (деплоить) приложения, создавая для них закрытую инфраструктуру, необходимую только для них. Пока что сложно, я понимаю. В общем и целом докер можно понимать как платформу для разработки, где можно быстро и эффективно работать. Докер можно понимать как программу, которая работает на сервере. Эта программа имеет возможность хранить контейнеры с приложениями. Что такое контейнер? Это отдельная инфраструктура, в которую можно добавить все, что нужно. Например для Java-приложения нам нужна JRE, чтобы запустить приложение, вот контейнер будет иметь это, нужно будет еще какое-то программное обеспечение — можно добавить это. А может быть, нам нужен Линукс и Tomcat сервлет контейнер. Такое тоже можно будет сделать. Контейнеры создаются на основе image (образа): то есть, это определенный шаблон в котором написано все необходимое для создания докер контейнера. Как создать этот образ? В нашем случае нам нужно будет создать файл Dockerfile в корне проекта с описанием того, что должно быть в контейнере. Так как мы не хотим где-то показывать токен бота, придется извернуться и передавать его каждый раз, когда мы захотим развертывать приложение. Более детально об этой теме почитать можно здесь и здесь.

Пишем JRTB-13

Нужно настроить быстрый и легкий процесс развертывания (деплоя) нашего приложения на сервер. То есть на машину, которая работает 24/7. За основу возьмем докер. Но задачи в нашем списке, которая бы отвечала за добавление этой функциональности, нет. Как-то я его пропустил при создании. Ничего страшного, сейчас создадим. Заходим на вкладку создания issue на гитхаб и выбираем Feature Request:“Java-проект от А до Я”: Реализуем развертывание приложения - 5Добавляем описание задачи, критерии его приемки, устанавливаем, к какому проекту этот issue относится и можно создавать новое issue:“Java-проект от А до Я”: Реализуем развертывание приложения - 6Теперь чтобы показать, что задача взята в работу, сменим статус задачи с To do на In Progress:“Java-проект от А до Я”: Реализуем развертывание приложения - 7Это будет сложная статья. Если будут проблемы — пишите в комментариях: я буду следить и отвечать на них в меру сил. Такой будет небольшой Customer Support :D

Создаем Dockerfile

Что такое докерфайл? Для докера это скрипт (пошаговая инструкция), как создавать образ для докер контейнера. Для работы нашего приложения нужна JDK, причем 11-й версии. То есть, нам нужно найти докер-образ JDK 11 и добавить его в наш образ. Это что-то сродни с тем, как мы добавляем зависимость в помник. Для этого дела у докера есть DockerHub. Чтобы локально загружать образы, нужно там зарегистрироваться. После регистрации идем искать нам JDK11. Из того, что получилось найти — вот этот контейнер: adoptopenjdk/openjdk11. В описании этого контейнера есть то, что нужно для докерфайла:

FROM adoptopenjdk/openjdk11:ubi
RUN mkdir /opt/app
COPY japp.jar /opt/app
CMD ["java", "-jar", "/opt/app/japp.jar"]
Поправим папку, из которой мы берем jar файл. У нас он находится в target папке после того, как мы запускаем mvn package задачу мавена. Перед тем, как все это делать, на основе обновленной main ветки создаем новую, для нашей задачи: STEP_4_JRTB-13. Теперь можно работать. В корне проекта создаем файл без расширения Dockerfile и добавим внутрь следующее:

FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Первая строка — на основе чего будет состоять образ — adoptopenjdk/openjdk11. Вторая строчка — добавляем аргумент в образ с именем JAR_FILE, который находится в папке target. Причем нынешняя папка определяется по месту Dockerfile. Третья строка — копируем в докер-образ jar нашего проекта. Последняя строка по сути содержит массив, созданный из команды в терминале, которую разделили по пробелу. То есть, в итоге будет выполнено следующее: “java -jar /app.jar” Чтобы держать в тайне токен бота, при запуске контейнера нам нужно будет передавать две переменные — имя бота и его токен. Для этого напишем запрос, который должен запустить наш проект с переменными. А как это сделать? Нужно загуглить: вот первая ссылка с нормальным описанием. А что мы хотим сделать? У нас в application.properties файле есть две переменные, которые мы там определяем:
  • bot.username
  • bot.token
Я хочу запускать докер контейнер и каждый раз передавать туда свое значение, чтобы никто не видел эти значения. Я знаю, что в SpringBoot переменные окружения, которые задаются в момент запуска jar проекта, будут более приоритетнее чем те, которые находятся в файле application.properties. Чтобы передать переменную в запросе, нужно добавить следующую конструкцию: -D{имя переменной}=”{значение переменной}”. Фигурные скобки не дописываем ;) Получим запрос, при котором будет запущено наше приложение с предопределенными значениями — имя и токена бота: java -jar -Dbot.username=”test.javarush.community_bot” -Dbot.token=”dfgkdjfglkdjfglkdjfgk” *.jar Теперь нужно передать эти переменные внутрь докер контейнера. Это environment variable. Чтобы в будущем у нас база данных работала четко и без проблем с нашим приложением, будем использовать docker-compose. Это отдельный инструмент, в котором можно упорядочить работу, запуск и зависимости между контейнерами. Иными словами, это надстройка над докером, чтобы управлять контейнерами одной инфраструктуры. Плюс перед тем, как запустить docker-compose, нужно быть уверенным, что мы стянули все изменения кода с сервера, собрали приложение и остановили старую версию. Для этого будем использовать баш скрипт. Ух… Звучит все непросто, согласен. Но работа с настройкой развертывания приложений — это всегда муторный и сложный процесс. Поэтому у нас вырисовывается нехилая схема:
  1. Запускаем баш скрипт.
  2. Баш скрипт запускает docker-compose.
  3. Docker-compose запускает docker контейнер с нашим приложением.
  4. Docker контейнер запускает наше приложение.
И вот нужно сделать так, чтобы две переменные — имя бота и его токен — прошли из 1 пункта в 4. Причем так, чтобы эти две переменные использовались при запуске нашего java-приложения. Пойдем с конца в начало. Мы уже знаем, какую команду нужно выполнить, чтобы запустить джарник. Поэтому будем настраивать Dockerfile, чтобы он научился принимать две переменные и передавать их в запрос. Для этого приведем Dockerfile к следующему виду:

FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}", "-jar", "/app.jar"]
Видно, что мы добавили две строки и обновил ENTRYPOINT. Строки:

ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
объявляют переменные внутри кодер файла. По умолчанию у них значение указано. Если при создании образа из этого докерфайла будут переданы переменные окружения с такими именами, значения будут другие. А в ENTRYPOINT мы добавили еще несколько элементов, которые будут считывать эти переменные среды:

"-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}"
Здесь видно, что внутри строки при помощи ${} конструкции будут переданы значения BOT_NAME и BOT_TOKEN. Далее нам нужно научить получать и передавать эти переменные в docker-compose.

Создаем docker-compose.yml

Хорошо бы вам про YAML формат почитать отдельно, а то статья и так уже растет, как на дрожжах. Для нас это просто еще одно описание переменных по типу .properties. Только в пропертях записывается через точку, а в YAML это делается немного красивее. Например, так. Две переменные в .properties: javarush.telegram.bot.name=ivan javarush.telegram.bot.token=pupkin А вот в .yaml (тоже самое что и .yml) будет это так:

javarush:
	telegram:
		bot:
		  name: ivan
		  token: pupkin
Второй вариант более красивый и понятный. Пробелы должны быть именно такие, как указаны выше. Как-нибудь переведем наши application.properties и application.yml. Для начала нужно его создать. В корне проекта создаем файл docker-compose.yml и записываем туда следующее:

version: '3.1'

services:
 jrtb:
   build:
     context: .
   environment:
     - BOT_NAME=${BOT_NAME}
     - BOT_TOKEN=${BOT_TOKEN}
   restart: always
Первая строка — это версия docker-compose. services: говорит о том, что все следующие строки после этого (будут сдвинуты) — относятся к сервисам, которые мы настраиваем. У нас такой пока только один — java-приложение под названием jrtb. И уже под ним будут все его настройки. Например, build: context: . говорит о том, что мы будем искать Dockerfile в той же директории, что и docker-compose.yml. А вот секция environment: будет отвечать за то, чтобы мы передали в Dockerfile необходимые переменные среды (environment variables). Как раз то, что нам и нужно. Поэтому ниже мы переменные и передаем. Их docker-compose будет искать в переменных операционной среды сервера. Добавим их в баш скрипте.

Создаем баш скрипты

И последний шаг — создать баш скрипт. Создаем в корне проекта файл с именем start.sh и пишем туда следующее:

#!/bin/bash

# Pull new changes
git pull

# Prepare Jar
mvn clean
mvn package

# Ensure, that docker-compose stopped
docker-compose stop

# Add environment variables
export BOT_NAME=$1
export BOT_TOKEN=$2

# Start new deployment
docker-compose up --build -d
Первая строка нужна для всех баш скриптов: без нее работать не будет. А далее — просто набор команд в терминале, которые нужно выполнить. Я добавил комментарии в каждой команде, поэтому должно быть понятно. Единственное, что хочется объяснить — это то, что значит $1 и $2. Это две переменные, которые будут переданы в запуске баш скрипта. При помощи команды export они будут добавлены в переменные сервера и считаны уже в docker-compose. Это работает для убунты, для виндоуса, наверно, нет, но я не уверен. Теперь нужно добавить скрипт stop.sh, который будет останавливать работу. В нем будет несколько строк:

#!/bin/bash

# Ensure, that docker-compose stopped
docker-compose stop

# Ensure, that the old application won't be deployed again.
mvn clean
Здесь мы останавливаем docker-compose и зачищаем джарник проекта, который лежит еще с прошлой сборки. Делаем мы это для того, чтобы наш проект точно пересобирался. Были прецеденты, поэтому и добавляю) В итоге у на получается 4 новых файла:
  • Dockerfile — файл для создания образа нашего приложения;
  • docker-compose.yml — файл с настройкой того, как мы будем запускать наши контейнеры;
  • start.sh — баш скрипт для развертывания нашего приложения;
  • stop.sh — баш скрипт для остановки нашего приложения.
Также обновим версию нашего приложения с 0.2.0-SNAPSHOT на 0.3.0-SNAPSHOT. Добавим в RELEASE_NOTES описание к новой версии и немного отрефакторим то, что было:
# Release Notes ## 0.3.0-SNAPSHOT * JRTB-13: added deployment process to the project ## 0.2.0-SNAPSHOT * JRTB-3: implemented Command pattern for handling Telegram Bot commands ## 0.1.0-SNAPSHOT * JRTB-2: added stub telegram bot * JRTB-0: added SpringBoot skeleton project
И в README добавим новый параграф с описанием того, как деплоить наше приложение:
## Deployment Deployment process as easy as possible: Required software: - terminal for running bash scripts - docker - docker-compose to deploy application, switch to needed branch and run bash script: $ bash start.sh ${bot_username} ${bot_token} That's all.
Разумеется, все пишет на английском. Уже как обычно, в нашей новосозданной ветке STEP_4_JRTB-13 создаем новый коммит с именем: JRTB-13: implement deployment process via docker и делаем пуш. Я перестаю подробно останавливаться на вещах, которые я уже описывал в прошлых статьях. Не вижу смысла повторять одно и тоже. К тому же, кто разобрался и сделал у себя, у того вопросов не возникнет. Это я о том, как создать новую ветку, как создать коммит, как запушить коммит в репозиторий.

Итог

За сегодня я показал тьму новой информации, которую нужно хорошо обдумать и расширить дополнительным чтением. Самое главное: при помощи ОДНОЙ(!!!) команды все необходимое для развертывания нашего приложения будет сделано. Это настолько классно, что я даже передать не могу вам. Да, пришлось потратить приличное количество времени в документации докера, чтобы понять, как правильно пробрасывать переменные. С этого момента телеграм-бот всегда будет работать на последней версии main ветки. Ссылка на телеграм-бота. Сегодня не будут ссылок на материалы, которые хорошо бы прочесть: ответственность лежит на вас. Нужно учиться искать информацию. Все, кто подписан на мой телеграм-канал, узнали о деплое бота почти сразу же. Друзья, нравится проект? Ставьте ему звезду! Так он станет более популярным и больше людей смогут узнать о нем и поучиться. Традиционно предлагаю зарегистрироваться на GitHub и подписаться на мой аккаунт, чтобы следить за этой серией и другими моими проектами, которые я веду там. Теперь мы готовы подключать базу данных. Следующая статья будет бооольшая и в ней мы сделаем все необходимое для работы с БД. Все описание — в JRTB-1.

Список всех материалов серии в начале этой статьи.

Комментарии (37)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ivanius Уровень 34
31 октября 2023
Народ, может задам глупый вопрос , но как все это запустить и проверить работает ли ?
Peace The Ball Уровень 32
31 октября 2023
Приветствую! Вижу, что последний коммент был больше года назад, не уверен, что кто-то увидит мою просьбу о помощи, тем не менее попытаюсь: Столкнулся с проблемой переменных сервера $1 $2. Если я ввожу истинные значения переменных в докерфайле (ENV BOT_NAME= ... и ENV BOT_TOKEN=...) или же прямо в команде (bash start.sh имяБота токенБота) - то все работает на ура. Если же я попытаюсь скрыть значения переменных и делаю так как описал глубоко уважаемый мною Роман, то получаю ошибку Error removing old webhook. Пытался и на Винде 10 и на Убунте, результат один и тот же. Два дня гугления и общения с чатом ДжиПиТи не дали желаемого результата. Если меня кто-нибудь слышит/видит мой коммент - пожалуйста помогите) Заранее благодарен)
Anonymous #2996742 Уровень 37
17 октября 2022
На windows10 столкнулся с проблемой переменных сервера $1 $2. На самом деле много осталось не понятным, поэтому не могу утверждать что мое решение верное. Для начала убедился что проект заработает с докером. Руководствовался комментарием Сергея где он советует видео https://www.youtube.com/watch?v=QF4ZF857m44. Проделал те же шаги что и в примере с Hello World. На этом этапе проблем не было, телеграм бот запустился. Но со скриптом выяснил что необходимо изменить файл docker-compose.yml // все то же самое, но в environment я убрал =${BOT_NAME} и =${BOT_TOKEN} Т.е ... environment: - BOT_NAME - BOT_TOKEN ... Но надо иметь ввиду что у меня стоит git (с git bash). Есть вероятность что без него не будет работать команда export в файле start.sh. Надеюсь кому-нибудь поможет. Роман, огромное спасибо за цикл статей!
Serhio Gonsales Уровень 35
5 июля 2022
Короче, (видимо на винде) если запускать сборку через bash скрипт, то docker стабильно выдает => ERROR [internal] load metadata for docker.io/adoptopenjdk/openjdk11:ubi и следом ERROR: Service 'jrtb' failed to build : Build failed ... типа какая-то проблема с образом, но это не так, ибо если деплоить руками через docker-compose, то все нормально работает. Все что гуглится на первых двух страницах по этой ошибке не помогло... скорее всего вся чехарда из-за винды, но это не точно. Если кто столкнулся с подобной проблемой, а главное решил ее, или знает как решить, буду признателен за разъяснения. Ну а пока, видимо, придется прописывать докер команды вместо скрипта
Iryna Уровень 23
16 мая 2022
Непонятно почему в Dockerfile мы оставляем

ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
если в запускаемом скрипте в терминале передаем значения этих переменных:

$ bash start.sh ${bot_username} ${bot_token}
Константин Уровень 34
29 марта 2022
Кто-нибудь, пожалуйста, объясните, что мы тут конкретно должны сделать. Куда скачать, какой файл и прочее. На самом деле ничего не понятно, и я бы даже забил, не будь Докера в последующих частях. Спасибо больше заранее!
29 января 2022
Как проблема решается на Windows? Застрял на этом месте
Игорь Уровень 22
28 октября 2021
Правильно ли я понимаю, что в данном случае мы создали докер-контейнер локально, сервер - личный пк. Сильно ли отличается процесс запуска приложения на уделенном сервере?
Ivan Уровень 41
27 июля 2021
Роман, спасибо за серию:) В своей версии сделал сборку в контейнер докера через файл с переменными окружения, может кому то интересно будет, ветки для истории я тоже оставляю. Если кому то интересно могу подробнее объяснить как это работает.
Vlad Уровень 33
6 июля 2021
Установил DockerDesktop. Подключил его с горем пополам, прописал Dockerfile и docker-compose.yml. Запустил в терминале - все успешно, контейнер создался и работает, но бот не отвечает. В чем может быть проблема?