Поднимаем стенд Spring микросервисов в Kubernetes

Гайд для начинающих по поднятию домашнего стенда для экспериментов c k8s c базовым CI/CD для микросервисов Spring.

3К открытий4К показов

В статье будет описан процесс поднятия домашнего стенда для экспериментов c k8s, c базовым CI/CD для микросервисов Spring.

Автор: Александр Леонов, руководитель группы разработки одной из распределённых команд Usetech.

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

Код статьи c инструкцией установки доступен в репозиториях:

  1. https://github.com/alexandr-leonov/eda-configuration
  2. 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
		
Поднимаем стенд Spring микросервисов в Kubernetes 1

Отлично, кластер мы подняли и запустили. Теперь создадим неймспейс наших сервисов. Пускай это будет 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
		
  1. zookeeper-cluster.yaml, устанавливающий zookeeper (если используете не самую последнюю версию Kafka);
  2. kafka-broker.yaml, устанавливает Kafka.

Исходный код:

  1. https://github.com/alexandr-leonov/eda-configuration/blob/master/dev/kafka/zookeeper-cluster.yaml
  2. 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. Смотрим поды:

Поднимаем стенд Spring микросервисов в Kubernetes 2

Ждём, когда сервис стартанёт.

Поднимаем стенд Spring микросервисов в Kubernetes 3

Готово, сервис поднят, теперь можно стучаться к сваггеру. Для этого определим какой ip адрес у ноды k8s, а затем найдём порт, по которому доступно приложение:

Поднимаем стенд Spring микросервисов в Kubernetes 4

Открываем сваггер:

Поднимаем стенд Spring микросервисов в Kubernetes 5

В целом можно остановиться. Но, не каждый же раз руками вызывать деплой сервиса! Поэтому автоматизируем и этот процесс так, как договорились ранее.

Перед тем как создавать шедуллер джобу, надо отметить, что она должна выполняться, как системный процесс, а значит её необходимо наделить соответствующими правами, используя политику 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 и подняли тестовый стенд для экспериментов.

Если вам понравилась эта статья, то скоро выйдет продолжение, где мы рассмотрим более сложную работу микросервисов в Kubernetes с балансировкой через Spring Cloud Gateway, хранением конфигураций приложений в Spring Config Service, фиксированием инцидентов через Sentry.

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий4К показов