Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes

Как поднять стенд микросервисов в Kunernetes из монорепозитория, чтобы быстрее зарелизить микросервисы на стенд для экспериментов.

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

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

В статье основной упор делается на приготовление одного из рецептов CI в монорепозитории, прохождение по стадиям настройки Spring Cloud Gateway, в конце добавлена щепотка мониторинга — подключение Sentry к микросервисам.

Данная статья является продолжением статьи:

Код статьи размещён в монорепозитории: 

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

Дисклеймер:

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

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

Для начала проведём подготовку со стороны кода, выделив в отдельные модули важные части (будем дробить по бизнес-ценности), затем изолируем модули друг от друга и сделаем их самостоятельными. Spring Boot приложениями. В итоге, проект будет выглядеть так:

Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes 1

Отлично, теперь у нас есть 4 отдельных разрозненных микросервиса, которые разные и под которые нужно переделывать фронт. Но нам, по возможности, этого бы не хотелось, поэтому пришла идея использовать Spring Cloud Gateway. Но не стоит сразу с места в карьер. В теории, если успех магазина по продаже книжек зайдёт, то его можно будет развести до целой платформы и потом продавать другим разработчикам сертификаты о её знании. Для того, чтобы безболезненно внедрять микросервисы, нам нужно хранить их конфигурации в одном месте, всё равно уже в монорепозитории делаем. Так как проект учебный, то почему бы не использовать стандартный Spring Config Service. Добавим к нашим разрозненным сервисам config-service и вынесем в специальную папку config все отличающиеся части application.yml файлов сервисов, унифицировав оставшиеся к одному стандартному виду, который будет тянуть всё содержимое из сервиса конфигурации.

В итоге получается вот такая структура:

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

Новый сервис по коду — обычный spring boot application, но с особенностями:

1) В помнике присутствуют две зависимости:

			org.springframework.boot
	spring-boot-starter-actuator





	org.springframework.cloud
	spring-cloud-config-server
		

2) Над стартером приложения размещена аннотация @EnableConfigServer

Рассмотрим application.yml нового config-service:

			# only for config service

server:
  port: 8888  # порт на котором будет крутиться сервис конфигурации

spring:
  application:
    name: config-service
  cloud:
    config:
      server:
        native: classpath:/config # папка, куда сервисы будут стучаться за своими конфигами
  profiles:
    active: native
		

Рассмотрим конфигурацию order-service, которую мы будем подгружать из конфиг сервиса config/order-service.yml:

			server:
  port: 8084

management:
  endpoints:
    web:
      exposure:
        include: "*"

properties:
  mark: order-service
		

А теперь увидим изменённый application.yml внутри сервиса order-service:

			application:
    name: order-service
  config:
    import: configserver:http://localhost:8888 # приложение будет стучаться за конфигом по этому адресу при старте приложения
		

Всё, теперь order-service будет подгружать конфигурацию из config-service. Помимо плюсов, стоит отметить, что будут минусы при написании тестов, т. к. нам нужно будет или поднимать config-service или копировать содержимое части order-service.yml в application-test.yml. Но мы пока о тестах не думали, поэтому вывели все конфигурации сервисов в наш config-service. 

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

Отлично, унифицировав наши микросервисы, давайте избавимся от необходимости вносить правки на фронте, применяя один API Gateway для взаимодействия со всеми сервисами. Для этого создадим новый сервис — gateway-service, который будет использовать Spring Cloud Gateway и крутится на 8080 порту, как наш старый монолит.

Из особенностей gateway-service можно выделить только:

1) наличие необходимых зависимостей

			org.springframework.boot
	spring-boot-starter-actuator



	org.springframework.cloud
	spring-cloud-starter-gateway



	org.springframework.boot
	spring-boot-starter-webflux
		

2) наличие в конфиге application.yml необходимых перенаправлений на сервисы:

			spring:
  cloud:
    gateway:
      routes:
        - id: profile_route
          uri: http://localhost:8083/profiles/**
          predicates:
            - Path=/profiles/**
        - id: order_route
          uri: http://localhost:8084/orders/**
          predicates:
            - Path=/orders/**
		

Отлично, теперь наши сервисы работают внешне как один бэкенд, фронт переделывать не нужно. Но, как теперь из монорепозитория собирать артефакты по сервисам отдельно, чтобы полностью не собирать проект?

Нам поможет сборка отдельных модулей maven + одна из стратегий CI в монорепозитории через деплой по коммиту в отдельную ветку, созданную под отдельный сервис. Это проще для понимания, нежели чем стратегия определения изменений в конкретных директориях модулей.

Итак, напишем наш ci файл. Как и раньше будем использовать github-ci. В итоге, получим такой конфиг сборки артефактов наших сервисов и упаковки их в образы, в docker-hub:

.github/workflows/maven.yml (по стандартному шаблону github):

			name: Common pipeline for all services

on:
  push:
    branches: [
      "config-service",
      "gateway-service",
      "order-service",
      "payment-service",
      "profile-service",
      "product-service"
    ]
  pull_request:
    branches: [
      "config-service",
      "gateway-service",
      "order-service",
      "payment-service",
      "profile-service",
      "product-service"
    ]

jobs:
  build-config-service:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/config-service' # по коммиту в ветку config-service происходит триггер джобы деплоя
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven
    - name: Build config-service artifact
      run: mvn -pl config-service clean install # сборка только нужного сервиса
    - name: Deploy app
      run: cd config-service  &&
        docker build  -t ${{ secrets.DOCKER_HUB_LOGIN }}/config-service:latest -f- ./ < Dockerfile &&
        docker login -u ${{ secrets.DOCKER_HUB_LOGIN }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} &&
        docker push ${{ secrets.DOCKER_HUB_LOGIN }}/config-service:latest # сборка образа и его публикация в Docker Hub, перед этим необходимо создать там такие репозитории.

  build-gateway-service:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/gateway-service'
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven
      - name: Build gateway-service artifact
        run: mvn -pl gateway-service clean install
      - name: Deploy app
        run: cd gateway-service &&
          docker build  -t ${{ secrets.DOCKER_HUB_LOGIN }}/gateway-service:latest -f- ./ < Dockerfile &&
          docker login -u ${{ secrets.DOCKER_HUB_LOGIN }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} &&
          docker push ${{ secrets.DOCKER_HUB_LOGIN }}/gateway-service:latest
build-order-service:
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/order-service'
  steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven
    - name: Build order-service artifact
      run: mvn -pl order-service clean install -Dmaven.test.skip=true
    - name: Deploy app
      run: cd order-service &&
        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
...
		

Ура, мы сделали CI из монорепозитория только тех сервисов, которые будем править. Но есть одно но: сервисы за нашим Spring Cloud Gateway не будут масштабироваться автоматически, т. к. выше мы их завязали на порты, т. е. Нам нужно будет править ещё и Spring Cloud Gateway. Давайте поднимем свой service discovery и подружим его со Spring Cloud Gateway. В свободном доступе все предлагают использовать Eureka, её и будем использовать.

Добавим ещё один сервис discovery-service, который будет ничем иным, как Eureka Server со следующими приметами:

1) Над стартером используется аннотация @EnableEurekaServer

2)

			org.springframework.cloud
    spring-cloud-starter-netflix-eureka-server



    io.sentry
    sentry-spring-boot-starter-jakarta
    ${sentry.version}
		

3) во все существующие сервисы добавим над стартером аннотацию: @EnableDiscoveryClient

и зависимость в помники:

			org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

а ещё, убедимся, что у всех сервисов есть имя, это важно!
spring:
  application:
    name: order-service # Для сервиса заказов
		

Теперь переделаем наш конфиг gateway-service:

			spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: product_route
          uri: lb://product-service
          predicates:
            - Path=/products/**
        - id: profile_route
          uri: lb://profile-service
          predicates:
            - Path=/profiles/**
		

Ура, вот теперь мы можем не трогать Spring Cloud Gateway при масштабировании наших сервисов. Работа через Eureka это, конечно, хорошо, но мы бы хотели стенд на kubernetes (т. к. монолит деплоился раньше туда). К тому же, там просто чудесные средства масштабирования приложений из коробки. В таком случае, давайте переведем наши сервисы в k8s:

Создадим пространство имён:

			apiVersion: v1
kind: Namespace
metadata:
  name: library-platform
  labels:
    name: library-platform

На каждый сервис создадим свой конфиг деплоя:
apiVersion: v1
kind: Service
metadata:
  name: payment-service
  namespace: library-platform
  labels:
    app: payment-service
spec:
  externalName: payment-service.library-platform.svc.cluster.local
  selector:
    app: payment-service-container
  ports:
    - name: payment-service
      port: 8083
      protocol: TCP
      targetPort: 8083
      nodePort: 30005
  type: NodePort
  
---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service-deployment
  namespace: library-platform
  labels:
    app: payment-service-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: payment-service-container
  template:
    metadata:
      labels:
        app: payment-service-container
    spec:
      containers:
        - name: payment-service-container
          image: ${{ secrets.DOCKER_HUB_LOGIN }}/payment-service:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8085
              name: rest
            - containerPort: 8888
              name: config
            - containerPort: 8761
              name: discovery
            - containerPort: 9092
              name: kafka
          env:
            - name: CONFIG_SERVICE_HOST
              value: $(CONFIG_SERVICE_SERVICE_HOST) # Сетевая связность обеспечивается через стандартные механизмы наименования переменных сред k8s
            - name: CONFIG_SERVICE_PORT
              value: $(CONFIG_SERVICE_SERVICE_PORT)
            - name: EUREKA_HOST
              value: $(DISCOVERY_SERVICE_SERVICE_HOST)
            - name: EUREKA_PORT
              value: $(DISCOVERY_SERVICE_SERVICE_PORT)
		

В принципе на этом этапе, можно забыть про  Eureka, т. к. далее мы всё равно переделаем наш gateway-service для работу с Kubernetes абстракцией под. Если бы не было Kubernetes, то Eureka была бы незаменимой вещью, но тут она может отдохнуть и остаться в конфигах, в памяти о былых заслугах.

Обновленный gateway-service.yml:

			spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: product_route
          uri: lb://product-service # роуты, которые работают через Eureka
          predicates:
            - Path=/products/**
        - id: profile_route
          uri: lb://profile-service
          predicates:
            - Path=/profiles/**
        - id: order_route
          uri: http://localhost:8084/orders/** # роуты, которые работают только локально
          predicates:
            - Path=/orders/**
        - id: payment_route
          uri: http://${PAYMENT_SERVICE_HOST}:${PAYMENT_SERVICE_PORT}/payments/** # роуты, которые работают с k8s
          predicates:
            - Path=/payments/**

eureka:
  client:
    service-url:
      defaultZone: http://${EUREKA_HOST}:${EUREKA_PORT}/eureka
      instance:
        preferIpAddress: true
		

Готово, давайте задеплоим наши поды, отмасштабируем payment-service и проверим, что всё работает, как мы задумывали:

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

Т.к. у нас ещё жива Eureka давайте проверим, что она видит 2 поды:

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

Постучимся через API Gateway к нашему сервису:

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

Супер, а теперь постучимся за детализацией несуществующих платежей и проверим, что балансер Kubernetes распределяет трафик по двум подам:

Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes 6
Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes 7

Итого, из 6 минус идентификаторов — 2 ушли в ошибку на одной поде, 4 на второй (основная).

Мы достигли цели, осталось накрутить мониторинг Sentry, для наработки навыка работы с фоном ошибок в домашних условиях.

1) Зарегистрируемся в https://sentry.io/

2) Создадим своё пространство, в нём создадим Spring Boot проект:

Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes 8

3) Добавим нужные зависимости в pom.xml по инструкции и dsn путь для отправки ивентов в payment-service.yml (см. код проекта по ссылке) + сделаем выставление значений через переменные среды в deployment файле payment-service.yaml k8s, для замены значений на лету.

Всё, теперь посмотрим как выглядит фон ошибок по нашим двум подам payment-service:

Поднимаем Full Spring стенд микросервисов из монорепозитория в Kubernetes 9

Отлично, мы подняли свой стенд микросервисов из дробленого монолита с деплоем из монорепозитория и мониторингом Sentry. Конечно, можно продолжить улучшать то, что уже есть: пирамида тестов, безопасность через sso, зеркалирование трафика kubernetes, внедрение Vault конфигов, но это потом. 

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

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