Поднимаем стенд Spring микросервисов в Kubernetes
https://t.me/Golang_googleВ статье будет описан процесс поднятия домашнего стенда для экспериментов c k8s, c базовым CI/CD для микросервисов Spring.
Автор: Александр Леонов, руководитель группы разработки одной из распределённых команд Usetech.
Дисклеймер Эта статья ориентирована на тех, кто не имеет полной картины разворачивания сервиса на инфраструктуре или является готовым шаблоном для быстрого поднятия своего стенда.
Код статьи c инструкцией установки доступен в репозиториях:
- https://github.com/alexandr-leonov/eda-configuration
- https://github.com/alexandr-leonov/eda-order-service
Итак, вам понадобилось обкатать какое-то решение на практике, но на работе это делать не разрешено/опасно/коллеги не поймут… Тогда самое время развернуть свой стенд для испытаний и экспериментами с инфраструктурой и сервисами.
Дано:
- Кластер на Kubernetes;
- Пару сервисов на Spring с PostgresSQL базой, кешом Redis и взаимодействием по Kafka и REST API;
- Аккаунт на Github (конечно, ближе к настоящей системе поднять свой Gitlab, но т. к. экспериментируем для себя и в одиночку, то Github достаточно);
- Аккаунт на DockerHub.
Найти:
Поднять у себя на локальной машине кластер k8s, который бы по пушу в Github деплоил на стенд необходимый сервис.
Решение:
Для начала поднимем свой кластер k8s. Для домашнего использования и экспериментов достаточно одного миникуба. Для этого установим его и запустим из терминала, с тем количеством памяти, которое вы можете позволить, но желательно не меньше 2‑х гигов.
minikube start --memory=6144
Отлично, кластер мы подняли и запустили. Теперь создадим неймспейс наших сервисов. Пускай это будет Event Driven Architecture Develop — слишком долго, поэтому укоротим до eda-dev.
МТС, Москва, можно удалённо, По итогам собеседования
Создадим namespace.yaml с неймспейсом:
apiVersion: v1 kind: Namespace metadata: name: eda-dev labels: name: eda-dev
Применяем конфиг в k8s c помощью команды:
kubectl apply -f namespace.yaml
Теперь попробуем установить Postgress, но перед тем как искать нужный yml в документации, необходимо озаботиться тем, где будем хранить секреты. В этой статье не будем затрагивать тему Vault. Для начала используем стандартный механизм k8s для хранения секретов.
Оформим secret.yaml:
apiVersion: v1 kind: Secret metadata: name: postgres-credentials namespace: eda-dev type: Opaque # при работе с PosrgreSQL по умолчанию, без доп.настроек, необходимо кодировать креды в Base64 data: POSTGRES_USER: ZWRhLWRlbW8tbG9naW4= POSTGRES_PASSWORD: ZWRhLWRlbW8tcGFzc3dvcmQ= POSTGRES_DB: ZWRhLWRlbW8tZGI=
Запускаем той же командой:
kubectl apply -f secret.yaml
Теперь найдём эталонный конфиг k8s для Postgres и модифицируем его под себя.
Итоговый файл postgres-cluster.yaml:
apiVersion: v1 kind: Service metadata: name: postgres-service namespace: eda-dev labels: app: postgres-service spec: selector: app: postgres-container ports: - name: postgres port: 5432 protocol: TCP targetPort: 5432 nodePort: 30003 #Внешний порт для работы с БД type: NodePort --- apiVersion: apps/v1 kind: Deployment metadata: name: postgres-deployment namespace: eda-dev labels: app: postgres-deployment spec: replicas: 1 selector: matchLabels: app: postgres-container template: metadata: labels: app: postgres-container spec: containers: - name: postgres-container image: postgres:14.0 # указываем образ из Docker Hub env: - name: POSTGRES_DB valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_DB - name: POSTGRES_USER valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_USER - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_PASSWORD ports: - containerPort: 5432
Здесь конфиг состоит из двух частей — самого сервиса и плана развёртывания этого сервиса, с указанием секретов и докер образа постгреса. Для экономии ресурсов выделим 1 реплику на БД.
Далее применяем yaml.
kubectl apply -f postgres-cluster.yaml
- zookeeper-cluster.yaml, устанавливающий zookeeper (если используете не самую последнюю версию Kafka);
- kafka-broker.yaml, устанавливает Kafka.
Исходный код:
- https://github.com/alexandr-leonov/eda-configuration/blob/master/dev/kafka/zookeeper-cluster.yaml
- https://github.com/alexandr-leonov/eda-configuration/blob/master/dev/kafka/kafka-broker.yaml
После применим наши конфиги по очереди — сначала zookeeper, потом Kafka.
Помимо установки стандартным путём, можно пойти другим путём, воспользовавшись helm чартами, готовыми сборками компонент, которые можно развернуть одной командой как есть или же переопределить какую-то часть пропертей в values.yaml.
Установим Redis через helm.
Для начала установим сам helm, в терминале Ubuntu это делается установкой snap пакета:
snap install helm
Далее добавим репозиторий с компонентами. Довольно распространены сборки компонент от компании Bitnami.
helm repo add bitnami https://charts.bitnami.com/bitnami
А теперь установим полноценный Redis, как есть с указанием неймспейса:
helm install redis-cluster bitnami/redis --namespace eda-dev
Готово! Установка helm с переопределением некоторых переменных на примере Mongo DB выглядит так:
helm install mongo-cluster -f mongodb/values.yaml bitnami/mongodb --namespace eda-dev
Пример конфига переопределённых переменных.
Отлично, теперь напишем один из пары микросервисов. Пускай это будет сервис заказов, написанный с использованием Kotlin, Spring Boot, Webflux, Gradle. Так как написание такого сервиса не является целью этой статьи, то можно воспользоваться готовым кодом.
Теперь, когда код сервиса написан, осталось настроить его CI/CD в кластер Kubernetes. Для CI части будем использовать Github Actions, в то время как для CD части обычно используют Webhook. Однако представим, что у нас не белый ip и мы вообще не хотим предоставлять какие-то данные наружу и готовы пожертвовать частью удобств, дабы не увлечься и не сожрать лимиты бесплатного использования service registry, в качестве, которого будем использовать Docker Hub, настроим крон джобу, которая периодически будет обновлять стенд самыми новыми образами сервисов.
Итак, CI делаем на стороне order-service, сначала подготовим Dockerfile для образа приложения:
FROM openjdk:11.0.7-jdk EXPOSE 80 EXPOSE 8083 EXPOSE 30301 EXPOSE 9092 WORKDIR /order-service COPY ./build/libs/. . ENTRYPOINT ["java","-jar","order-service-1.0.0.jar"]
Затем, модифицируя стандартный github-ci конфиг, а также используя стандартные секреты Github для хранения логинов и паролей, получим:
name: Docker Image CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 name: Set up JDK 11 - uses: actions/setup-java@v2 with: java-version: '11' distribution: 'adopt' - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b - name: Make gradlew executable run: cd order-service/order-service && chmod +x ./gradlew # для работы с hidden папками в Github Action необходимо предоставить соответствующие права доступа - name: Build image run: cd order-service/order-service && ./gradlew clean bootJar && docker build -t ${{ secrets.DOCKER_HUB_LOGIN }}/order-service:latest -f- ./ < Dockerfile && docker login -u ${{ secrets.DOCKER_HUB_LOGIN }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} && docker push ${{ secrets.DOCKER_HUB_LOGIN }}/order-service:latest
Данный конфиг будет собирать образ и публиковать его Docker Hub при каждом пуше в мастер.
Настало время CD части.
Создадим order-service.yaml, который будет разворачивать наше приложение в k8s с заданными настройками:
apiVersion: v1 kind: Service metadata: name: order-service namespace: eda-dev labels: app: order-service spec: externalName: order-service.eda-dev.svc.cluster.local selector: app: order-service-container ports: - name: order-service port: 8083 protocol: TCP targetPort: 8083 nodePort: 30004 #порт, открытый для тестирования сервиса type: NodePort --- apiVersion: apps/v1 kind: Deployment metadata: name: order-service-deployment namespace: eda-dev labels: app: order-service-deployment spec: replicas: 1 selector: matchLabels: app: order-service-container template: metadata: labels: app: order-service-container spec: containers: - name: order-service-container image: ${{ secrets.DOCKER_HUB_LOGIN }}/order-service:latest imagePullPolicy: Always # всегда идём в Docker Hub за новым образом ports: - containerPort: 8083 name: rest - containerPort: 5432 name: postgres - containerPort: 9092 name: kafka env: - name: DATABASE_HOST value: $(POSTGRES_SERVICE_SERVICE_HOST) - name: DATABASE_PORT value: $(POSTGRES_SERVICE_SERVICE_PORT) - name: DATABASE_SCHEMA valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_DB - name: DATABASE_LOGIN valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_USER - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: postgres-credentials key: POSTGRES_PASSWORD - name: KAFKA_HOST value: $(KAFKA_SERVICE_HOST) - name: KAFKA_PORT value: $(KAFKA_SERVICE_PORT)
Так как конфиги лежат в отдельной Github репе, то креды Docker Hub скрыты подобным образом. Однако при локальной работе их нужно будет заменить настоящими значениями. Значения вида $(POSTGRES_SERVICE_HOST) стандартные, их можно не менять.
Применим конфиг:
kubectl apply -f services/order-service.yaml
Готово, сервис пошёл тянуть образ из Docker Hub. Смотрим поды:
Ждём, когда сервис стартанёт.
Готово, сервис поднят, теперь можно стучаться к сваггеру. Для этого определим какой ip адрес у ноды k8s, а затем найдём порт, по которому доступно приложение:
Открываем сваггер:
В целом можно остановиться. Но, не каждый же раз руками вызывать деплой сервиса! Поэтому автоматизируем и этот процесс так, как договорились ранее.
Перед тем как создавать шедуллер джобу, надо отметить, что она должна выполняться, как системный процесс, а значит её необходимо наделить соответствующими правами, используя политику RBAC k8s.
Создадим конфиг прав для плана order-service-deployment — rights.yaml:
kind: ServiceAccount #создаём пользователя apiVersion: v1 metadata: name: deployment-restart namespace: eda-dev --- apiVersion: rbac.authorization.k8s.io/v1 # создаём роль с группами доступа kind: Role metadata: name: deployment-restart namespace: eda-dev rules: - apiGroups: ["apps", "extensions"] resources: ["deployments"] resourceNames: ["order-service-deployment"] verbs: ["get", "patch", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 # навешиваем роль на пользователя kind: RoleBinding metadata: name: deployment-restart namespace: eda-dev roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: deployment-restart subjects: - kind: ServiceAccount name: deployment-restart namespace: eda-dev
Теперь создадим джобу scheduler.yaml, которая будет каждые 4 часа обновлять order-service в кластере, если в Docker Hub появились новые образы.
# Docker image update tracking. # By default K8s config has limit on 3 success job and 1 failed. apiVersion: batch/v1beta1 kind: CronJob metadata: name: deployment-restart namespace: eda-dev spec: concurrencyPolicy: Forbid schedule: '* */4 * * *' # каждые 4 часа джоба запускает CD процесс jobTemplate: spec: backoffLimit: 2 activeDeadlineSeconds: 100 template: spec: serviceAccountName: deployment-restart restartPolicy: Never containers: - name: kubectl image: bitnami/kubectl command: - 'kubectl' - 'rollout' - 'restart' - 'deployment/order-service-deployment'
Применим данные конфиги:
kubectl apply -f rights.yaml kubectl apply -f scheduler.yaml
Ура! Мы развернули кластер с микросервисом Spring, с полноценным CI/CD процессом, познакомились с некоторыми сущностями k8s, helm и подняли тестовый стенд для экспериментов.