Всем привет. Продолжаем цикл статей по написанию своего проекта. “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.

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