Продолжая эту статью, добавим нереляционную MongoDB в кластер Kubernetes и в приложение Spring Boot.

Добавление MongoDB в приложение Spring Boot

Приступим

При разработке службы Kubernetes начинаем с локального Kind-кластера Kubernetes. В репозитории GitHub имеются файлы конфигурации для настройки, клонируем его:

git clone [email protected]:MartinHodges/aquarium-with-mongo-db.git

Почему MongoDB?

Доля рынка MongoDB вдвое больше, чем у ближайших конкурентов. Эта база данных совершенно нетривиальна, имеется две версии: версия сообщества и корпоративная. Когда говорят о нереляционных базах данных, MongoDB вспоминается одной из первых.

Техническое сравнение с другими отложим в сторону, MongoDB выбрана нами из-за ее популярности  —  и способности «выполнять работу».

Установка MongoDB

MongoDB устанавливается в кластер Kubernetes аналогично другим приложениям  —  посредством оператора.

Оператор MongoDB в Kubernetes

Оператором Kubernetes устанавливается, контролируется и отслеживается жизненный цикл приложения, при необходимости выполняются действия.

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

Создание кластера Kubernetes для разработки

Kind  —  легковесное приложение с поддержкой узлов, его проще настроить для локального кластера.

Вот инструкции по установке для разных ОС.

Воспользуемся командами среды macOS, понадобятся homebrew и Docker.

Устанавливаем Kind:

brew install kind

Проверяем версию:

kind version

Например, такая:

kind v0.22.0 go1.21.7 darwin/arm64

Установка завершена.

Создаем кластер Kind с такой конфигурацией:

kind/kind-config.yml

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
extraPortMappings:
# API-интерфейсы
- containerPort: 30080
hostPort: 30080
- role: worker
- role: worker
- role: worker

Так получается четырехузловой кластер: один узел-контроллер, три рабочих, а порт 30080 становится доступным на машине разработки. Используя все это, создаем локальный кластер Kubernetes:

kind create cluster --config kind/kind-config.yml

Установка оператора

Устанавливаем поддерживаемый сообществом оператор, добавляя в локальный репозиторий ссылку Helm:

helm repo add mongodb https://mongodb.github.io/helm-charts

Смотрим, какие чарты добавились:

helm search repo mongo

В списке содержится оператор сообщества, помещаем его вместе с базой данных в отдельное пространство имен mongo:

kubectl create namespace mongo

Устанавливаем оператор:

helm install community-operator mongodb/community-operator -n mongo

Чтобы создаваемые ресурсы отслеживались оператором в другом пространстве имен, добавляем в эту команду --set operator.watchNamespace="<other namespace>".

Проверяем состояние готовности 1/1 Running:

kubectl get pods -n mongo

Оператор запущен, определения специальных ресурсов установлены:

kubectl get crds
kubectl describe crd mongodbcommunity.mongodbcommunity.mongodb.com

Создание кластера

Установленным оператором прослушивается любой запрос на создание базы данных MongoDB. Сделаем запрос, применив манифест MonogDB к кластеру Kubernetes с помощью загруженных оператором определений специальных ресурсов.

Но прежде зададим пароль пользователя базы данных. Для этого создаем секрет Kubernetes, меняя <…> на задаваемый пароль:

kubectl create secret generic my-user-password -n mongo --from-literal="password=<your password>"

Проверяем так:

kubectl get secrets -n mongo my-user-password -o jsonpath={.data.password} | base64 -d; echo

Все секреты Kubernetes кодируются в base 64, поэтому декодируем пароль с помощью base64 -d. Командой create secret пароль автоматически кодирован в base 64 из-за использования --from-literal.

Для создания кластера MongoDB и базы данных с пользователем-администратором, у которого имеется этот пароль, делаем файл манифеста:

k8s/my-mongo-db.yml

apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
name: my-mongo-db
namespace: mongo
spec:
members: 3
type: ReplicaSet
version: "7.0.11"
security:
authentication:
modes: ["SCRAM"]
users:
- name: my-user
db: admin
passwordSecretRef: # ссылка на секрет, из которого сгенерируется пароль пользователя
name: my-user-password
key: password
roles:
- name: clusterAdmin
db: admin
- name: userAdminAnyDatabase
db: admin
scramCredentialsSecretName: my-user-scram
additionalMongodConfig:
storage.wiredTiger.engineConfig.journalCompressor: zlib

Обратите внимание: в стандартном storageClass создаются запросы на выделение постоянных томов. Следует убедиться, что постоянный том для этого запрашиваемого класса создается оператором постоянных томов корректно. Это касается Kind, другим понадобится что-то вроде nfs-client. Если эти постоянные тома недоступны, кластер не запустится.

Применяем это так:

kubectl apply -f k8s/my-mongo-db.yml

И проверяем ход выполнения:

kubectl get pods -n mongo

Ожидаем создания трех экземпляров. На моем MacBook Pro M2 Max Apple silicon с четырехузловым кластером Kind все три запустились примерно за пять минут.

После запуска проверяем работоспособность службы:

kubectl get svc -n mongo

И получаем:

NAME              TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
my-mongo-db-svc ClusterIP None <none> 27017/TCP 6m

Тестирование базы данных

С приложением мы подключимся к базе данных прямо из Kubernetes, используя для службы DNS-имя. Однако в целях тестирования нужно подключиться из машины локальной разработки.

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

MongoServerError[NotWriteablePrimary]: not primary

То есть для проброса порта выбран не основной для кластера под. Второстепенные поды  —  это копии только для чтения, поэтому все записи выполняются через основной.

Проблема решается подключением к основному поду.

Находясь вне кластера и используя такой клиент MongoDB, как Compass, найти основной под по названиям подов Kubernetes, например my-mongo-db-1.my-mongo-db-svc.mongo.svc.cluster.local, не получится: извне они недоступны.

Основной узел определяется по логам любого из узлов:

kubectl logs my-mongo-db-0 -n mongo -c mongod | grep "\"primary\":"

Если результатов нет, попался основной.

Если результат получен, даже если всего несколько строк, они очень длинные и трудные для восприятия. При наличии средства структурированного вывода JSON, такого как jq, воспользуйтесь этим:

kubectl logs my-mongo-db-0 -n mongo -c mongod | grep "\"primary\":" | jq

Появится такая строка:

...
"primary": "my-mongo-db-1.my-mongo-db-svc.mongo.svc.cluster.local:27017",
...

Из нее понятно, к какому поду подключаться, в моем случае это my-mongo-db-1. Теперь пробрасываем порт для этого пода:

kubectl port-forward my-mongo-db-1 -n mongo 27017:27017

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

Как только этот проброс порта задействуется, подключаемся к базе данных, устанавливая клиент MongoDB Compass. При подключении к базе данных появится строка подключения mongodb://localhost:27017.

Меняем настройки. В Advanced Connection Options нажимаем Direct connection, иначе получим ненайденный адрес, потому что клиентом используется внутренний адрес Kubernetes.

Во вкладке Authentication выбираем Username/Password и вводим имя пользователя my-user и выбранный ранее пароль. Как базу данных добавляем Admin, затем выбираем механизм аутентификации SCRAM-SHA-256, при необходимости прокручивая вниз.

Нажимаем Save and Connect, называем подключение и оказываемся на консоли Compass, подключенной к базе данных.

В кластере появятся созданные базы данных admin, config и local.

Значит, кластер MongoDB запущен.

Создание пользователя приложения

Но, чтобы воспользоваться созданным my-user, приложению мало подключиться к MongoDB. Этим пользователем база данных сопровождается.

Чтобы разрешить приложению задействовать кластер базы данных, создадим базу данных и пользователя для доступа к ней.

Внизу окна Сompass нажимаем >_MONGOSH и попадаем в командную строку.

Создаем пользователя:

use aquarium
db.createUser( { user: "my-app-user",
pwd: "<password>",
roles: [ {db: "aquarium", role: "dbOwner"} ] } )

На что здесь обратить внимание?

Во-первых, переход к несуществующей базе данных aquarium, то есть до ее создания. Это согласуется с принципом отсутствия необходимости что-либо определять перед использованием. База данных и любые коллекции создаются при первом добавлении документа.

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

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

...
ok: 1,
...

Чтобы проверить этого пользователя, открываем новое подключение Compass через меню или комбинацией Cmd + N на Mac. Нажимаем только раз, наберитесь терпения  —  подключение появится спустя секунды.

При появлении окна нового подключения проще скопировать подключение, сохраненное ранее, с помощью меню  рядом с подключением.

Меняем имя пользователя и пароль, поменяем также Authentication Database на aquarium. Подключаемся.

Теперь перед нами новая база данных aquarium. Протестируем ее, нажав + рядом с названием базы данных и создав коллекцию fishes. Затем добавляем в базу данных данные в виде документа:

{
"_id": 123,
"fish": "Guppy"
}

Теперь MongoDB готова добавиться в приложение Spring Boot.

Создание приложения Spring Boot

Простой пример с базой данных начинаем приложением aquarium. Рыбы и аквариумы создаются и управляются REST API-интерфейсами. Затем рыбы добавляются в один из аквариумов.

Функционал добавления рыбы в аквариум и их взаимосвязи мы не рассматриваем.

Код

Доступен в репозитории GitHub.

Зависимости

Запускать приложение Spring Boot всегда проще со Spring Initializr.

Добавляем Spring Web и Spring Data MongoDB в качестве зависимостей и создаем проект.

Чтобы не писать шаблонный код, включаем также Lombok.

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

В зависимости от приложения пакеты структурируются по типу компонентов, например controllers, services и repositories, или по предметной области.

Создаваемое приложение небольшое, поэтому основываем этот проект на двух предметных областях  —  рыбах и аквариумах:

fishes
FishController
FishService
FishRepository
fishtanks
FishTankController
FishTankService
FishTankRepository

Придерживаемся стандартного многоуровневого подхода с уровнями контроллеров, служб и репозиториев.

Конечные точки API

Контроллерами предоставляются конечные точки CRUD-операций для соответствующих API.

Сущности и документы

Знакомы с JPA и базами данных SQL вроде Postgres? Тогда будете как рыба в воде среди сущностей и репозиториев.

В нереляционной базе данных таблицы заменяются коллекциями, а строки таблицы  —  документами.

То есть репозитории для нереляционных баз данных немного отличаются от того, чем они являются для реляционных баз данных.

Нереляционная база данных работает с любой структурой, поэтому сущности или документы становятся простыми Java-объектами POJO. В примере нашего приложения объекты создаются так:

aquarium/fishes/Fish.java

...

@Setter
@Getter
@Document("fishes")
@NoArgsConstructor
public class Fish {

@Id
public UUID id;

public String type;

public Fish(String type) {
this.id = UUID.randomUUID();
this.type = type;
}
...
}

Обратите внимание:

  1. Вместо @Entity теперь определяется @Document, которым принимается название коллекции.
  2. Вместо @mongoId применяется @Id. Если не указать, в MongoDB добавится свой, а так мы сможем управлять собственными идентификаторами UUID.
  3. Lombok, например @Getter, используется для удаления части шаблонного кода.

Таким же образом создаем аквариумы:

aquarium/fishtanks/FishTank.java

@Setter
@Getter
@Document("fish tanks")
@NoArgsConstructor
public class FishTank {

@Id
public UUID id;

public String name;

public FishTank(String name) {
this.id = UUID.randomUUID();
this.name = name;
}

@Override
public String toString() {
return String.format(
"FishTank[id=%s, type='%s']",
id.toString(), name);
}
}

Репозитории

Итак, документы имеются, но как получить к ним доступ?

Покажем изменения в репозиториях, например, рыб:

...
public interface FishRepository extends MongoRepository<Fish, UUID> {

public List<Fish> findAll();

public Optional<Fish> findFirstById(UUID id);

public Optional<Fish> findFirstByType(String type);
}
...

Этот репозиторий почти тот же, что и в реляционной базе данных. Единственное отличие  —  интерфейсом расширяется не CrudRepository, а MongoRespository.

Оставим тему сопоставлений «один ко многим» и т. д. другим статьям, ограничимся созданием и управлением рыб и аквариумов.

Свойства приложения

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

Для файла свойств Spring Boot используем файлы YAML, заменяем поля < > собственными значениями и получаем такую конфигурацию:

resources/application.yml

spring:
application:
name: aquarium-with-mongo-db

data:
mongodb:
host: localhost
port: 27017
database: aquarium
username: my-app-user
password: <password>

Вернемся к ней, когда поговорим о профилях.

Контроллеры и службы

Теперь добавим контроллеры и службы так же, как это было бы с реляционной базой данных. Они доступны в репозитории GitHub.

Тестирование приложения

Завершайте код или клонируйте мой репозиторий, затем запускайте приложение в интегрированной среде разработки. Если продолжаете проброс порта в основной под, приложение должно запуститься.

Тестируем его этими командами curl:

curl localhost:8080/api/v1/fishes -H "Content-Type: application/json" -d '{"type": "guppy2"}' 
curl localhost:8080/api/v1/fish-tanks -H "Content-Type: application/json" -d '{"name": "big one"}'
curl localhost:8080/api/v1/fishes
curl localhost:8080/api/v1/fish-tanks

Теперь в клиенте Compass перезагружаем базу данных aquarium, должны появиться две коллекции: fishes и fish tanks. Внутри этих коллекций  —  созданные рыбы и аквариумы.

Финальный этап

Мы подключили приложение Spring Boot к MongoDB, запускаемой в кластере Kubernetes. Теперь загрузим приложение Spring Boot в этот кластер.

Для этого:

  1. Создаем «жирный» jar-файл, в который включены все зависимости.
  2. Создаем из него образ Docker.
  3. Подгружаем образ в репозиторий Docker.
  4. Создаем файл манифеста Deployment.
  5. Применяем манифест Deployment к кластеру Kubernetes.

Поскольку используем Kind, 3-й пункт заменяется простой загрузкой без репозитория Docker.

Профили

Прежде чем создавать jar-файл, чтобы приложение запускалось в подключенном режиме  —  как это было до сих пор  —  и в кластере Kubernetes, сделаем два профиля Spring Boot:

  • connected … для запуска вне кластера;
  • local-cluster … для запуска внутри кластера.

В первом режиме мы запускаемся сейчас. То есть просто копируем файл application.yml или application.properties в application-connected.yml, затем в командную строку JVM добавляем JVM-аргумент:

-Dspring.profiles.active=connected

То же самое проделываем для файла local-cluster, но с одним изменением:

...
data:
mongodb:
host: my-mongo-db-svc.mongo.svc.cluster.local
port: 27017
...

Используя DNS-имя, подключаемся к корректному поду. Благодаря тому, как в поде настроены правила поиска DNS, часть имени опускается, например my-mongo-db-svc.mongo.svc. Поэтому при развертывании в разных кластерах приложение остается рабочим.

Создание образа

Перейдем к созданию образа. Поскольку мы разместили на GitHub проект Gradle, создаем jar-файл из корневой папки проекта так:

gradle build

Чтобы манифестом указывалось на основной файл приложения, в gradle.build добавили следующее:

gradle.build

jar {
manifest {
attributes "Main-Class": "com.requillion_solutions.aquarium.AquariumWithMongoDbApplication"
}
}

Так создается jar-файл build/libs/aquarium-with-mongo-db-0.0.1-SNAPSHOT.jar.

Для образа Docker создаем Dockerfile:

FROM openjdk:17.0.2-slim-buster
RUN addgroup --system spring && useradd --system spring -g spring
USER spring:spring
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

Во избежание проблем с Lombok начинаем с базового образа Java 17. Чтобы не запускаться как root, добавляем нового пользователя spring. Затем jar-файл копируется в образ, и для запуска приложения создается entrypoint.

Образ Docker создается так:

docker build -t aquarium .

И с Kind он загружается прямо в кластер Kubernetes:

kind load docker-image aquarium

Дальше запустим образ с помощью манифеста Deployment.

Манифест Deployment

Чтобы запустить загруженный в кластере образ, создадим файл манифеста Deployment:

k8s/deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
name: aquarium
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: aquarium
template:
metadata:
labels:
app: aquarium
spec:
containers:
- name: aquarium
image: aquarium
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
# Следующая переменная окружения при считывании в Spring преобразуется в
# свойство «override», называемое «spring.profiles.active»
- name: SPRING_PROFILES_ACTIVE
value: local-cluster
---
apiVersion: v1
kind: Service
metadata:
name: aquarium
namespace: default
spec:
selector:
app: aquarium
type: NodePort
ports:
- port: 8080
targetPort: 8080
nodePort: 30080

На что обратить внимание здесь?

  1. Приложение развертывается в пространстве имен default, то есть пространство имен не задано.
  2. Имеется только одна реплика.
  3. Образ добавляется, только если его нет. А он есть, загружен ранее.
  4. Профилю задано значение local-cluster.
  5. Чтобы сопоставлять порт «8080» приложения и пробрасывать через порт «30080» на машину разработки, создается служба.

Теперь развертываемся так:

kubectl apply -f k8s/deployment.yml

Проверяем успешность запуска:

kubectl get pods

После развертывания тестируем API теми же curl-командами, но с портом «30080»:

curl localhost:30080/api/v1/fishes -H "Content-Type: application/json" -d '{"type": "guppy2"}' 
curl localhost:30080/api/v1/fish-tanks -H "Content-Type: application/json" -d '{"name": "big one"}'
curl localhost:30080/api/v1/fishes
curl localhost:30080/api/v1/fish-tanks

Новые документы просматриваем в пользовательском интерфейсе Compass, не забывая задействовать проброс порта.

Заключение

Мы изучили, как MongoDB устанавливается в Kind-кластер Kubernetes и интегрируется в приложение Spring Boot, продемонстрировали, что нереляционную базу данных использовать с Kubernetes и Spring Boot довольно просто.

Но на практике придется поработать над безопасностью, резервным копированием и отказоустойчивостью.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Martin Hodges: My experience adding a MongoDB No-SQL database to my Kubernetes cluster, Using Kind to develop and test your Kubernetes deployments

Предыдущая статьяПрогрессивное совершенствование и JavaScript-фреймворки: сложные взаимоотношения