# Лабораторная работа 3. ### Задание **Цель**: изучение современных технологий контейнеризации. **Задачи**: - Установить средство контейнеризации docker. - Изучить применение и принципы docker. - Изучить утилиту docker-compose и структуру файла docker-compose.yml. - Развернуть не менее 3х различных сервисов при помощи docker-compose. - Оформить отчёт в формате Markdown и создать Pull Request в git-репозитории. ### Как запустить лабораторную работу В директории с файлом характеристик docker-compose.yaml выполнить команду: ``` docker-compose -f docker-compose.yaml up ``` ### Описание лабораторной работы #### Создание базы данных Каждый сервис реализует CRUD-операции, поэтому были выбраны следующие сущности, соответствующие теме диплома: упражнение и тренировка. Эти сущности связаны отношением один ко многим. Созданные таблицы базы данных: ```sql -- Создание таблицы тренировок CREATE TABLE t_training ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); -- Создание таблицы упражнений CREATE TABLE t_exercise ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, id_training INTEGER, FOREIGN KEY (id_training) REFERENCES t_training(id) ); ``` Также в файле `docker-compose.yaml` создадим соответствующий сервис: ```dockerfile #database postgresql: #configuration image: postgres:latest ports: - 5432:5432 environment: POSTGRES_PASSWORD: admin POSTGRES_USER: admin POSTGRES_DB: traininarium volumes: - ./database.sql:/docker-entrypoint-initdb.d/database.sql restart: always networks: - mynetwork ``` - `image: postgres:latest` указывает, что мы хотим использовать последнюю версию образа `PostgreSQL`. - `ports: - 5432:5432` пробрасывает порт 5432 контейнера (стандартный порт PostgreSQL) на порт 5432 хоста, чтобы можно было подключаться к базе данных с внешнего устройства. - `environment` определяет переменные окружения, которые будут использоваться контейнером. В данном случае, устанавливаются значения для переменных `POSTGRES_PASSWORD`, `POSTGRES_USER` и `POSTGRES_DB`. - `volumes` указывает путь к файлу `database.sql` в текущей директории, который будет использоваться для инициализации базы данных при запуске контейнера. - `restart: always` гарантирует, что контейнер будет перезапущен автоматически, если он остановится или перезагрузится. - `networks` определяет сеть, к которой будет присоединен контейнер. В данном случае, контейнер будет присоединен к сети с именем `mynetwork`. В итоге будет создан контейнер с базой данных `PostgreSQL`, настроенной с указанными переменными окружения, проброшенным портом и файлом `database.sql` для инициализации базы данных. #### Создание микросервиса *Упражнения* После реализации необходимых элементов микросервиса, таких как: controller, model, modelDTO, repository и service, создадим конфигурационный файл Dockerfile, в котором пропишем следующее: ```dockerfile FROM openjdk:17-jdk WORKDIR /app COPY ./exercise-app/build/libs/exercise-app-0.0.1-SNAPSHOT.jar /app/exercise-app-0.0.1-SNAPSHOT.jar EXPOSE 8081 CMD ["java", "-jar", "exercise-app-0.0.1-SNAPSHOT.jar"] ``` В данном файле мы указываем базовый образ, который будет использован для создания контейнера (*OpenJDK* версии 17 с установленным *JDK*); устанавливаем рабочую директорию внутри контейнера, где будут размещены файлы приложения; указываем, что приложение внутри контейнера будет слушать на порту 8081 и определяем команду, которая будет выполнена при запуске контейнера, а именно запуск приложения *Java* из файла `exercise-app-0.0.1-SNAPSHOT.jar` внутри контейнера. Также в файле `docker-compose.yaml` создадим соответствующий сервис: ```dockerfile exercise-service: build: context: . dockerfile: ./exercise-app/Dockerfile ports: - 8081:8081 environment: DATASOURCE_URL: jdbc:postgresql://postgresql:5432/traininarium DATASOURCE_USERNAME: admin DATASOURCE_PASSWORD: admin restart: always #wait build database depends_on: - postgresql networks: - mynetwork ``` - `exercise-service` является именем сервиса, который будет создан и запущен в контейнере. - `context: .` указывает, что контекстом для сборки Docker-образа является текущая директория. - `dockerfile: ./exercise-app/Dockerfile` указывает путь к `Dockerfile`, который будет использоваться для сборки образа. - `ports` определяет проброс портов между хостом и контейнером. `8081:8081` указывает, что порт 8081 на хосте будет проброшен на порт 8081 внутри контейнера. - `environment` определяет переменные окружения, которые будут доступны внутри контейнера. `DATASOURCE_URL: jdbc:postgresql://postgresql:5432/traininarium` определяет URL для подключения к базе данных `PostgreSQL`. `DATASOURCE_USERNAME: admin` определяет имя пользователя для подключения к базе данных. `DATASOURCE_PASSWORD: admin` определяет пароль для подключения к базе данных. - `restart: always` указывает, что контейнер будет автоматически перезапущен в случае его остановки. depends_on определяет зависимость данного сервиса от другого сервиса. - `postgresql` указывает, что данный сервис зависит от сервиса с именем `postgresql`. - `mynetwork` указывает, что контейнер будет присоединен к сети с именем `mynetwork`. #### Создание микросервиса *Тренировки* Аналогично реализуем controller, model, modelDTO, repository и service для микросервиса *Тренировки*, но дополняя кодом отправки запроса к сервису *Упражнений*. Пример запроса к сервису *Упражнений*: ```java public List findAllTraining() throws Exception { RestTemplate restTemplate = new RestTemplate(); HttpEntity entity = new HttpEntity<>(new HttpHeaders()); List trainingDtos = new ArrayList<>(); for (Training training : trainingRepository.findAll()) { TrainingDto trainingDto = new TrainingDto(); trainingDto.setId(training.getId()); trainingDto.setName(training.getName()); trainingDto.setDescription(training.getDescription()); ResponseEntity response = restTemplate.exchange( "http://" + exercise_service_host + "/exercise/training/" + trainingDto.getId(), HttpMethod.GET, entity, String.class); if (response.getStatusCode().is2xxSuccessful()) { String responseBody = response.getBody(); ObjectMapper objectMapper = new ObjectMapper(); List exerciseDtos; try { exerciseDtos = objectMapper.readValue(responseBody, ArrayList.class); trainingDto.setExercises(exerciseDtos); } catch (JsonProcessingException e) { throw new Exception("Не удалось десериализовать тело запроса в объект"); } } else { throw new Exception("Ошибка получения объекта"); } trainingDtos.add(trainingDto); } return trainingDtos; } ``` Теперь создадим конфигурационный файл Dockerfile, в котором пропишем следующее: ```dockerfile FROM openjdk:17-jdk WORKDIR /app COPY ./training-app/build/libs/training-app-0.0.1-SNAPSHOT.jar /app/training-app-0.0.1-SNAPSHOT.jar EXPOSE 8082 CMD ["java", "-jar", "training-app-0.0.1-SNAPSHOT.jar"] ``` В данном файле мы указываем базовый образ, который будет использован для создания контейнера (*OpenJDK* версии 17 с установленным *JDK*); устанавливаем рабочую директорию внутри контейнера, где будут размещены файлы приложения; указываем, что приложение внутри контейнера будет слушать на порту 8082; копируем файл `training-app-0.0.1-SNAPSHOT.jar` из локальной директории `./training-app/build/libs/` внутрь контейнера в папку `/app` и определяем команду, которая будет выполнена при запуске контейнера, а именно запуск приложения Java из файла `training-app-0.0.1-SNAPSHOT.jar` внутри контейнера. Также в файле `docker-compose.yaml` создадим соответствующий сервис: ```dockerfile training-service: build: context: . dockerfile: ./training-app/Dockerfile ports: - 8082:8082 environment: EXERCISE_SERVICE_HOST: exercise-service:8081 DATASOURCE_URL: jdbc:postgresql://postgresql:5432/traininarium DATASOURCE_USERNAME: admin DATASOURCE_PASSWORD: admin restart: always #wait build database depends_on: - postgresql networks: - mynetwork ``` - `training-service` является именем сервиса, который будет создан и запущен в контейнере. - `context: .` указывает, что контекстом для сборки Docker-образа является текущая директория. - `dockerfile: ./training-app/Dockerfile` указывает путь к `Dockerfile`, который будет использоваться для сборки образа. - `8082:8082` указывает, что порт 8082 на хосте будет проброшен на порт 8082 внутри контейнера. - `environment` определяет переменные окружения, которые будут доступны внутри контейнера. `EXERCISE_SERVICE_HOST: exercise-service:8081` определяет хост и порт для подключения к сервису `exercise-service`. `DATASOURCE_URL: jdbc:postgresql://postgresql:5432/traininarium` определяет URL для подключения к базе данных `PostgreSQL`. `DATASOURCE_USERNAME: admin` определяет имя пользователя для подключения к базе данных. `DATASOURCE_PASSWORD: admin` определяет пароль для подключения к базе данных. - `restart: always` указывает, что контейнер будет автоматически перезапущен в случае его остановки. - `depends_on` определяет зависимость данного сервиса от другого сервиса, данный сервис зависит от сервиса с именем `postgresql`. - `mynetwork` указывает, что контейнер будет присоединен к сети с именем `mynetwork`. #### Реализация gateway при помощи *nginx* Для того, чтобы использовать *Nginx* для обработки HTTP-запросов и маршрутизации их к соответствующим сервисам, создадим файл конфигурации *Nginx*: ``` events { worker_connections 1024; } http { upstream exercise-service { server exercise-service:8081; } upstream training-service { server training-service:8082; } server { listen 80; listen [::]:80; server_name localhost; location /exercise-service/ { proxy_pass http://exercise-service/; } location /training-service/ { proxy_pass http://training-service/; } } } ``` - `events` определяет настройки событий для сервера *Nginx*. - `worker_connections 1024` указывает максимальное количество одновременных соединений, которые могут быть обработаны сервером. - `http` определяет настройки HTTP-сервера *Nginx*. - `upstream` определяет группу серверов, которые могут обрабатывать запросы. `exercise-service` определяет группу серверов с именем `exercise-service`, в которой находится только один сервер `exercise-service:8081`. `training-service` определяет группу серверов с именем `training-service`, в которой находится только один сервер `training-service:8082`. - `server` определяет настройки для конкретного виртуального сервера. - `listen 80` указывает на порт, на котором сервер будет слушать входящие HTTP-запросы. - `listen [::]:80` указывает на IPv6-адрес и порт, на котором сервер будет слушать входящие HTTP-запросы. - `server_name localhost` указывает имя сервера. - `location /exercise-service/` определяет местоположение для обработки запросов, которые начинаются с `/exercise-service/`. `proxy_pass http://exercise-service/` указывает, что все запросы, начинающиеся с `/exercise-service/`, должны быть перенаправлены на группу серверов `exercise-service`. - `location /training-service/` определяет местоположение для обработки запросов, которые начинаются с `/training-service/`. `proxy_pass http://training-service/` указывает, что все запросы, начинающиеся с `/training-service/`, должны быть перенаправлены на группу серверов `training-service`. Таким образом, при запуске сервера *Nginx* с использованием этого конфигурационного файла, сервер будет слушать входящие HTTP-запросы на порту 80 и маршрутизировать запросы, начинающиеся с `/exercise-service/`, на группу серверов `exercise-service`, а запросы, начинающиеся с `/training-service/`, на группу серверов `training-service`. Далее в файле `docker-compose.yaml` создадим соответствующий сервис: ```dockerfile nginx: #configuration image: nginx:latest ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf restart: always depends_on: - training-service - exercise-service networks: - mynetwork ``` - `nginx` - это название сервиса, который будет запущен в контейнере. - `image: nginx:latest` указывает на использование последней версии образа *Nginx* из *Docker Hub*. - `80:80` пробрасывает порт 80 контейнера на порт 80 хостовой машины. - `volumes` определяет привязку тома между контейнером и хостовой машиной. - `./nginx.conf:/etc/nginx/nginx.conf` привязывает файл `nginx.conf` из текущего рабочего каталога хостовой машины к файлу `nginx.conf` внутри контейнера *Nginx*. Это позволяет настроить *Nginx* с помощью внешнего файла конфигурации. - `restart: always` указывает, что контейнер должен быть автоматически перезапущен при его остановке или падении. - `depends_on` указывает на зависимость этого сервиса от других сервисов. - `training-service` указывает, что контейнер Nginx должен быть запущен после контейнера `training-service`. - `exercise-service` указывает, что контейнер Nginx должен быть запущен после контейнера `exercise-service`. - `networks` определяет сети, к которым будет присоединен контейнер. - `mynetwork` добавляет контейнер в сеть с именем `mynetwork`. ### Видео https://disk.yandex.ru/i/OYPw_Tzl0QrzIw