Docker для начинающих: простое развертывание приложения за несколько шагов

в 12:15, , рубрики: docker, docker-compose, dockerfile, java, контейнеризация, контейнеры docker, приложения, развертывание приложений

Всем привет! Для своей первой статьи я решил выбрать проблему, с которой сам столкнулся при изучении Java и попытке упаковки приложения в докер-контейнер. К сожалению не нашел ни одной исчерпывающей статьи, как это делать, поэтому решил написать свою.

Начну, пожалуй, с самого сервиса. Я написал достаточно простое веб-приложение на стеке - Java, Spring, Maven, REST, HTTP, Hibernate, Postgresql, JSP/JSTL. Пока приложение представлено достаточно в сыром виде, но для понимания, как оно упаковывается в контейнер, вполне подойдет. Если вкратце, то это сервис для голосования за лучший ресторан, где можно зарегистрироваться, добавить ресторан, его описание, оставить отзыв и проставить рейтинг. Также, в зависимости от роли, можно посмотреть информацию о пользователях и редактировать ее. 

Ссылки на проект:

Приложение состоит из бэкэнд части, написанной на стеке, который указан выше и UI части, которая очень сырая и написана на JSP/JSTL, так как других языков для написания UI пока не знаю 🙂. Структура проекта выглядит следующим образом

Структура проект

Структура проект
  1. Бэкэнд часть

  2. UI часть - JSP + JSTL

  3. Конфигурационные файлы для создания образов и контейнеров - Dockerfile и docker-compose.yml

Почему контейнеризация и Docker?

Мы хотим, чтобы наше приложение, которое теоретически может состоять из нескольких сервисов (например, в случае микросервисной архитектуры или распределенного монолита), запускалось в изолированных контейнерах со своими настройками и подключенными библиотеками. Также мы хотим чтобы эти контейнеры могли между собой общаться. Если упрощенно, то есть целевая ОС, в которой запускаются изолированные контейнеры. Если по простому, то можно представить это на схеме:

Структура контейнеризации

Структура контейнеризации

Общение между контейнерами может происходить через брокеры, очереди, http+REST и другие технологии. Забегая вперед, скажу что в нашем примере будет всего 2 контейнера - один с приложением и второй с базой данных (БД).

НО! Возникает резонный вопрос - а почему бы не использовать виртуальные машины для запуска приложений. По факту это программа, которая запускает на одном физическом компьютере несколько операционных систем одновременно. Теоретически в каждой из них мы можем изолированно развернуть сервисы и можно спокойно настроить общение между ними.

Так в чем же отличие VM от контейнеров? 

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

Если говорить именно о Docker, то это программа, которая по сути и управляет созданием, запуском и удалением контейнеров. А именно, при запуске Docker создается виртуальная машина с Linux. Это и есть целевая система, в которой будут запускаться контейнеры. Целевую ОС еще называют ядром Докера.

Как настроить проект для упаковки в контейнеры?

Итак, у нас есть проект, и первое, что надо сделать - это настроить сборку, чтобы генерировался именно war-файл, потому что по умолчанию при компиляции будет генерится jar-файл. Эту настройку необходимо указать в pom файле

Настройки компиляции для генерации war-файла

Настройки компиляции для генерации war-файла

War файлы предназначены для использования в качестве веб-приложений и обычно развертываются на серверах приложений (в нашем случае это Apache Tomcat). Jar файлы могут использоваться для создания автономных приложений или библиотек и могут быть развернуты, например на вашем локальном компе

Для генерации war файла необходимо отключить выполнение тестов (так как я их не пока не написал) и запустить джобу package:

Docker для начинающих: простое развертывание приложения за несколько шагов - 4

После удачной компиляции создается папка target, и внутри будет лежать war-файл:

Docker для начинающих: простое развертывание приложения за несколько шагов - 5

Далее на основе этого war-файла необходимо создать образ и упаковать его в контейнер.

Что такое образ и контейнер?

Первое, что надо сделать - установить на ПК docker-desktop.

Что же такое образ? Образ - это основа для создания контейнера. Он включает в себя операционную систему, программное обеспечение и настройки, необходимые для запуска приложения. Образы могут быть использованы для создания новых контейнеров или обновления существующих. Для своего кастомного докер образа используется Dockerfile, в котором мы пишем инструкции для сборки образа. В нашем проекте он лежит в корне и выглядит вот так:

Docker для начинающих: простое развертывание приложения за несколько шагов - 6

Этот Dockerfile начинает с образа adoptopenjdk/openjdk11:ubi как основы или базового слоя, который по факту будет просто скачивается с докер-хаба - хранилища образов. Затем он принимает аргумент WAR_FILE, который представляет собой путь к war-файлу Java-приложения в каталоге target. COPY ${WAR_FILE} application.war копирует указанный war-файл в папку контейнера с именем application.war. COPY src/main/webapp. копирует jsp страницы в контейнер. ENTRYPOINT [“java”, “-jar”, “/application.war”] указывает команду, которая должна быть выполнена при запуске контейнера. Это запускает Java-приложение, используя указанный war-файл.

Таким образом каждая команда в нашем Dockerfile добавляет новый слой к базовому образу. На выходе мы уже получим кастомный образ

Docker для начинающих: простое развертывание приложения за несколько шагов - 7

Чтобы собрать образ, выполним в консоли команду

docker build -t app .

В результате получим образ с именем app.

Далее этот образ может быть использован, как база для создания контейнера с помощью команды docker run. Однако, выполнив эту команду, мы получим ошибку, так как наше приложение работает с базой данных и по программе должны накатываться миграционные скрипты, а базы данных у нас нет. Т.е. Для полноценной работы нашего приложения необходимо запустить еще один контейнер с образом базы данных.

Как запустить несколько контейнеров?

Для таких случаев, когда нужно запускать много контейнеров, лучше использовать утилиту docker-compose. В нашем простейшем случае для работы приложения необходимо два контейнера - один с приложением, а один с базой данных. И формирование образов, создание и запуск всех контейнеров выполняется лишь одной командой - docker-compose up. А все инструкции для создания и запуска всех контейнеров будут хранится в файле  docker-compose.yml.

Файл docker-compose.yml выглядит следующим образом:

Docker для начинающих: простое развертывание приложения за несколько шагов - 8

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

  1. Начнем со второго контейнера - db, сделанного на основе образа postgresql, который будет скачан с докер-хаба. Т.е. тут мы используем чисто однослойный образ, лишь добавив переменные среды. Таким образом, при дальнейшем создании/запуске контейнера через команду docker-compose up будет скачан образ, создан контейнер, установятся переменные среды и будет создана БД с именем restaurant и будет запущена на порту 5433. 

  2. Контейнер на основе слоеного образа из Dockerfile. Т.е. При выполнении команды Тут заданы простейшие настройки. Например настройка:

depends_on:

  - db

указывает, что сначала должен запуститься контейнер с БД, а только потом наш сервис. 

Настройки environment - переменные среды для подключения к БД

environment:

  - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5433/restaurant

Также, чтобы достучаться до нашего приложения через браузер по localhost, задаем настройку перенаправления:

ports:

  - "8080:8080"

Тут следует сделать небольшое отступление и рассказать про сетевое взаимодействие. Зачем? Да потому что мы же хотим достучаться до контейнера, а именно в нашем случае - открыть UI нашего приложения на локальном ПК. Это сделать не так просто, потому что приложение запущенное в контейнере, а контейнер в хосте - как в той сказке про кощея.

Сетевое взаимодействие

Контейнеры, помимо того, чтобы взаимодействовать между собой должны иметь возможность обмениваться данными с внешними сетевыми ресурсами. Немного информации об этом

Упомяну 2 основных режима:

  • None - Контейнер функционирует в полностью изолированной среде. Мы не можем выбраться за пределы контейнера и пропинговать внешние ресурсы.

  • Bridge - используется по умолчанию - как раз наш случай. Вот так будет выглядеть взаимодействие в нашем случае

Docker для начинающих: простое развертывание приложения за несколько шагов - 9

Bridge обеспечивает взаимодействие подключенных к ней контейнеров. Мост (Bridge) перенаправляет трафик. Т.е., чтобы попасть в контейнер с нашим приложением Java App, нужно пройти следующий путь - прийти в докер хост, потом на мост и только потом в нужный контейнер. По сути Bridge обеспечивает определенный уровень изоляции. Между собой могут общаться только те контейнеры, которые связаны через этот общий мост. Для того, чтобы понять, на каком порту запущен мост, необходимо выполнить команду:

docker network inspect bridge

Docker для начинающих: простое развертывание приложения за несколько шагов - 10

Как уже писалось выше, вот такой настройкой можно перенаправить контейнер из Docker-host на localhost:

ports:

  - "8080:8080"

Это нужно, чтобы после запуска контейнеров мы смогли попасть в наше приложение через http://localhost:8080/  в браузере

Собрать образы, запустить контейнеры одной командой? 

Итак, выполним в терминале из папки с проектом простую команду docker-compose up. В результате произойдет следующее:

  1. Соберется образ, описанный в Dockerfile

  2. Запустится контейнер db с базой данных на основе образа postgresql

  3. Запустится контейнер из образа из пункта 1 - а именно наше приложение

Теперь можно открыть docker-desktop (или выполнить в терминале команду docker ps) и увидеть наши собранные образы:

Docker для начинающих: простое развертывание приложения за несколько шагов - 11

Теперь перейдем во вкладку containers и можем увидеть созданные и запущенные контейнеры на основе этих образов:

Docker для начинающих: простое развертывание приложения за несколько шагов - 12

На базе одного образа можно создать множество контейнеров. Чтобы понять аналогию, приведу пример. Представьте, что образ - это класс, а контейнер - это экземпляр этого класса. И вы можете создать сколь угодно много экземпляров класса - сколько вам позволяет выделенная память

Теперь мы можем ввести в браузере http://localhost:8080/ и попадаем на главную страницу нашего запущенного приложения. Т.е. Мы его запустили не локально у себя на компьютере, а в контейнере, который просто перенаправили на наш локальный хост. Также следует заметить, что мы не устанавливали на наш ПК postgresql. Мы лишь использовали образ postgresql, скачанный с докер-хаба и запущенный в контейнере. 

Автор: Rhett_cody

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js