Использование микросервисов в работе с Kubernetes и GitOps

Автор оригинала: Rick Spurgeon
  • Перевод

Архитектуры микросервисов продолжают развиваться в инженерных организациях, поскольку команды стремятся увеличить скорость разработки. Микросервисы продвигают идею модульности как объекты первого класса в распределенной архитектуре, обеспечивая параллельную разработку и компоненты с независимыми циклами выпуска. Как и при принятии любых технологических решений, необходимо учитывать компромиссы. В случае микросервисов они включают потенциальную потерю централизованных стандартов разработки, а также повышенную сложность эксплуатации.

К счастью, существуют стратегии решения этих проблем. Сначала мы рассмотрим рефакторинг сервиса на основе Kafka Streams с использованием Microservices Framework, который обеспечивает стандарты для тестирования, конфигурации и интеграции. Затем мы используем существующий проект streaming-ops для создания, проверки и продвижения нового сервиса из среды разработки в рабочую среду. Хотя это и не обязательно, но вы если хотите выполнить шаги, описанные в этой заметке, то вам понадобится собственная версия проекта streaming-ops, как описано в документации.

Проблемы микросервисной архитектуры

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

  • Множественные решения общих потребностей в рамках всей организации нарушают принцип "Не повторяйся".

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

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

Spring Boot

Чтобы снизить эти риски, разработчики обращаются к микросервисным фреймворкам для стандартизации общих задач разработки, и Spring Boot (расширение фреймворка Spring) является популярным примером одного из таких фреймворков.

Spring Boot предоставляет согласованные решения для общих проблем разработки программного обеспечения, например, конфигурация, управление зависимостями, тестирование, веб-сервисы и другие внешние системные интеграции, такие как Apache Kafka​. Давайте рассмотрим пример использования Spring Boot для переписывания существующего микросервиса на основе Kafka Streams.

Сервис заказов

Проект streaming-ops - это среда, похожая на рабочую, в которой работают микросервисы, основанные на существующих примерах Kafka Streams. Мы рефакторизовали один из этих сервисов для использования Spring Boot, а полный исходный код проекта можно найти в репозитории GitHub. Давайте рассмотрим некоторые основные моменты.

Интеграция Kafka

Библиотека Spring for Apache Kafka обеспечивает интеграцию Spring для стандартных клиентов Kafka, Kafka Streams DSL и приложений Processor API. Использование этих библиотек позволяет сосредоточиться на написании логики обработки потоков и оставить конфигурацию и построение зависимых объектов на усмотрение Spring dependency injection (DI) framework. Здесь представлен компонент сервиса заказов Kafka Streams, который агрегирует заказы и хранит их по ключу в хранилище состояний:

@Autowired
public void orderTable(final StreamsBuilder builder) {
  logger.info("Building orderTable");
  builder
    .table(
	this.topic,
    	Consumed.with(Serdes.String(), orderValueSerde()),
    	Materialized.as(STATE_STORE))
    .toStream()
    .peek((k,v) -> logger.info("Table Peek: {}", v));
}

Аннотация @Autowired выше предписывает фреймворку Spring DI вызывать эту функцию при запуске, предоставляя инстанс StreamsBuilder, который мы используем для построения нашего DSL-приложения Kafka Streams. Этот метод позволяет нам написать класс с узкой направленностью на бизнес-логику, оставляя детали построения и конфигурирования объектов поддержки Kafka Streams  фреймворку.

Конфигурация

Spring предоставляет надежную библиотеку конфигурации, позволяющую использовать различные методы для внешней настройки вашего сервиса. Во время выполнения Spring может объединять значения из файлов свойств, переменных окружения и аргументов программы для конфигурирования приложения по мере необходимости (порядок приоритета доступен в документации).

В примере с сервисом заказов мы решили использовать файлы свойств Spring для конфигурации, связанной с Apache Kafka. Значения конфигурации по умолчанию предоставляются во встроенном ресурсе application.properties, и мы переопределяем их во время выполнения с помощью внешних файлов и функции Profiles в Spring. Здесь вы можете увидеть сниппет ресурсного файла application.properties по умолчанию:

# ###############################################
# For Kafka, the following values can be
# overridden by a 'traditional' Kafka
# properties file
bootstrap.servers=localhost:9092
...
# Spring Kafka
spring.kafka.properties.bootstrap.servers=${bootstrap.servers}
...

Например, значение spring.kafka.properties.bootstrap.servers обеспечивается значением в bootstrap.servers с использованием синтаксиса плейсхолдер ${var.name} .

Во время выполнения Spring ищет папку config в текущем рабочем каталоге запущенного процесса. Файлы, найденные в этой папке, которые соответствуют шаблону application-<profile-name>.properties, будут оценены как активная конфигурация. Активными профилями можно управлять, устанавливая свойство spring.profiles.active в файле, в командной строке или в переменной окружения. В проекте streaming-ops мы разворачиваем набор файлов свойств, соответствующих этому шаблону, и устанавливаем соответствующие активные профили с помощью переменной окружения SPRING_PROFILES_ACTIVE.

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

В приложении сервиса заказов мы решили использовать Spring Gradle и плагин управления зависимостями Spring. dependency-management plugin впоследствии будет управлять оставшимися прямыми и переходными зависимостями за нас, как показано в файле build.gradle:

plugins {
  id 'org.springframework.boot' version '2.3.4.RELEASE'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

Следующие библиотеки Spring могут быть объявлены без конкретных номеров версий, поскольку плагин предоставит совместимые версии от нашего имени:

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-actuator'
  implementation 'org.springframework.boot:spring-boot-starter-webflux'
  implementation 'org.apache.kafka:kafka-streams'
  implementation 'org.springframework.kafka:spring-kafka'
  ...

REST-сервисы

Spring предоставляет REST-сервисы с декларативными аннотациями Java для определения конечных точек HTTP. В сервисе заказов мы используем это для того, чтобы использовать фронтенд API для выполнения запросов в хранилище данных Kafka Streams. Мы также используем асинхронные библиотеки, предоставляемые Spring, например, для неблокирующей обработки HTTP-запросов:

@GetMapping(value = "/orders/{id}", produces = "application/json")
public DeferredResult<ResponseEntity> getOrder(
  @PathVariable String id,
  @RequestParam Optional timeout) {   

  final DeferredResult<ResponseEntity> httpResult = 
    new DeferredResult<>(timeout.orElse(5000L));
...

Смотрите полный код в файле OrdersServiceController.java.

Тестирование

Блог Confluent содержит много полезных статей, подробно описывающих тестирование Spring для Apache Kafka (например, смотрите Advanced Testing Techniques for Spring for Apache Kafka). Здесь мы кратко покажем, как легко можно настроить тест с помощью Java-аннотаций, которые будут загружать Spring DI, а также встроенный Kafka для тестирования клиентов Kafka, включая Kafka Streams и использование AdminClient:

@RunWith(SpringRunner.class)
@SpringBootTest
@EmbeddedKafka
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class OrderProducerTests {
...

С помощью этих полезных аннотаций и фреймворка Spring DI создание тестового класса, использующего Kafka, может быть очень простым:

@Autowired
private OrderProducer producer;
...
@Test
public void testSend() throws Exception {
  ...
  List producedOrders = List.of(o1, o2);
  producedOrders.forEach(producer::produceOrder);
  ...

Смотрите полный файл OrderProducerTests.java для наглядного примера.

Проверка в dev

Код сервиса заказов содержит набор интеграционных тестов, которые мы используем для проверки поведения программы; репозиторий содержит задания CI, которые вызываются при появлении PR или переносе в основную ветвь. Убедившись, что приложение ведет себя так, как ожидается, мы развернем его в среде  dev для сборки, тестирования и дальнейшего подтверждения поведения кода.

Проект streaming-ops запускает свои рабочие нагрузки микросервисов на Kubernetes и использует подход GitOps для управления операционными проблемами. Чтобы установить наш новый сервис в среде dev, мы изменим развернутую версию в dev, добавив переопределение Kustomize в сервис заказов Deployment, и отправим PR на проверку.

Когда этот PR будет объединен, запустится процесс GitOps, модифицируя объявленную версию контейнера службы заказов. После этого контроллеры Kubernetes развертывают новую версию, создавая заменяющие Поды и завершая работу предыдущих версий.

После завершения развертывания мы можем провести валидацию новой службы заказов, проверив, правильно ли она принимает REST-звонки, и изучив ее журналы. Чтобы проверить конечную точку REST, мы можем открыть приглашение внутри кластера Kubernetes с помощью хелпер-команды в предоставленном Makefile, а затем использовать curl для проверки конечной точки HTTP:

$ make prompt
bash-5.0# curl -XGET http://orders-service
curl: (7) Failed to connect to orders-service port 80: Connection refused

Наша конечная точка HTTP недостижима, поэтому давайте проверим журналы:

kubectl logs deployments/orders-service | grep ERROR
2020-11-22 20:56:30.243 ERROR 21 --- [-StreamThread-1] o.a.k.s.p.internals.StreamThread     	: stream-thread [order-table-4cca220a-53cb-4bd5-8c34-d00a5aa77e63-StreamThread-1] Encountered the following unexpected Kafka exception during processing, this usually indicate Streams internal errors:
           org.apache.kafka.common.errors.GroupAuthorizationException: Not authorized to access group: order-table

Эти ошибки, скорее всего, ортогональны и поэтому потребуют независимых исправлений. Не имеет значения, как они будут устранены, необходимо быстро вернуть нашу систему в работоспособное состояние. GitOps предоставляет хороший путь для ускорения этого процесса путем отмены предыдущего коммита. Мы используем функцию возврата GitHub PR, чтобы организовать последующий PR, который отменяет изменения.

Как только PR будет объединен, процесс GitOps применит отмененные изменения, возвращая систему в предыдущее функциональное состояние. Для лучшей поддержки этой возможности целесообразно сохранять изменения небольшими и инкрементными. Среда dev полезна для отработки процедур отката.

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

  1. HTTP-порт по умолчанию был другим, из-за чего служба Kubernetes не могла правильно направить трафик сервису заказов.

  2. Идентификатор приложения Kafka Streams по умолчанию отличался от настроенного списка контроля доступа (ACL) в Confluent Cloud, что лишало наш новый сервис заказов доступа к кластеру Kafka.

Мы решили отправить новый PR, исправляющий значения по умолчанию в приложении. Изменения содержатся в конфигурационных файлах, расположенных в развернутых ресурсах Java Archive (JAR).

В файле application.yaml мы изменяем порт HTTP-сервиса по умолчанию:

Server:
  Port: 18894

А в файле application.properties (который содержит соответствующие конфигурации Spring для Apache Kafka) мы модифицируем ID приложения Kafka Streams на значение, заданное декларациями Confluent Cloud ACL:

spring.kafka.streams.application-id=OrdersService

Когда новый PR будет отправлен, процесс CI/CD на основе GitHub Actions запустит тесты. После слияния PR другой Action опубликует новую версию  Docker-образа службы заказов.

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

$ make prompt
bash-5.0# curl http://orders-service/actuator/health
{"status":"UP","groups":["liveness","readiness"]}

bash-5.0# curl -XGET http://orders-service/v1/orders/284298
{"id":"284298","customerId":0,"state":"FAILED","product":"JUMPERS","quantity":1,"price":1.0}

Наконец, с нашего устройства разработки мы можем использовать Confluent Cloud CLI для потоковой передачи заказов из темы orders в формате Avro (см. документацию Confluent Cloud CLI для инструкций по настройке и использованию CLI).

➜ ccloud kafka topic consume orders --value-format avro
Starting Kafka Consumer. ^C or ^D to exit
{"quantity":1,"price":1,"id":"284320","customerId":5,"state":"CREATED","product":"UNDERPANTS"}
{"id":"284320","customerId":1,"state":"FAILED","product":"STOCKINGS","quantity":1,"price":1}
{"id":"284320","customerId":1,"state":"FAILED","product":"STOCKINGS","quantity":1,"price":1}
^CStopping Consumer.

Продвижение в prd

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

Сначала оценим хелпер-команду, которую можно запустить для проверки разницы в объявленных версиях сервиса заказов в каждой среде. С устройства разработчика в репозитории проекта мы можем использовать Kustomize для сборки и оценки окончательно материализованных манифестов Kubernetes, а затем поиска в них визуальной информации о сервисе заказов. Наш проект streaming-ops предоставляет полезные команды Makefile для облегчения этой задачи:

➜ make test-prd test-dev >/dev/null; diff .test/dev.yaml .test/prd.yaml | grep "orders-service"
< image: cnfldemos/orders-service:sha-82165db 
> image: cnfldemos/orders-service:sha-93c0516

Здесь мы видим, что версии тегов образов Docker отличаются в средах dev и prd. Мы сохраним финальный PR, который приведет среду prd в соответствие с текущей версией dev. Для этого мы модифицируем тег изображения, объявленный в базовом определении для службы заказов, и оставим на месте переопределение dev. В данном случае оставление dev-переопределения не оказывает существенного влияния на развернутую версию службы заказов, но облегчит будущие развертывания на dev. Этот PR развернет новую версию на prd:

Перед слиянием мы можем повторно выполнить наши тестовые команды, чтобы убедиться, что в развернутых версиях службы заказов не будет различий, о чем свидетельствует отсутствие вывода команд diff и grep:

➜ make test-prd test-dev >/dev/null; diff .test/dev.yaml .test/prd.yaml | grep "orders-service"
➜

Этот PR был объединен, и контроллер FluxCD в среде prd развернул нужную версию. Используя jq и kubectl с флагом --context, мы можем легко сравнить развертывание сервиса заказов на кластерах dev и prd:

➜ kubectl --context= get deployments/orders-service -o json | jq -r '.spec.template.spec.containers | .[].image'
cnfldemos/orders-service:sha-82165db

➜ kubectl --context= get deployments/orders-service -o json | jq -r '.spec.template.spec.containers | .[].image'
cnfldemos/orders-service:sha-82165db

Мы можем использовать curl внутри кластера, чтобы проверить, что развертывание работает правильно. Сначала установите контекст kubectl на ваш рабочий кластер:

➜ kubectl config use-context <your-prd-k8s-context>
Switched to context "kafka-devops-prd".

Хелпер-команда подсказки в репозитории кода помогает нам создать терминал в кластере prd, который мы можем использовать для взаимодействия с REST-сервисом службы заказов:

➜ make prompt
Launching-util-pod--------------------------------
➜ kubectl run --tty -i --rm util --image=cnfldemos/util:0.0.5 --restart=Never --serviceaccount=in-cluster-sa --namespace=default
If you don't see a command prompt, try pressing enter.
bash-5.0#

Внутри кластера мы можем проверить работоспособность (“здоровье” - health) службы заказов:

bash-5.0# curl -XGET http://orders-service/actuator/health
{"status":"UP","groups":["liveness","readiness"]}
bash-5.0# exit

Наконец, мы можем убедиться, что заказы обрабатываются правильно, оценив журналы из orders-and-payments-simulator:

➜ kubectl logs deployments/orders-and-payments-simulator | tail -n 5
Getting order from: http://orders-service/v1/orders/376087   .... Posted order 376087 equals returned order: OrderBean{id='376087', customerId=2, state=CREATED, product=STOCKINGS, quantity=1, price=1.0}
Posting order to: http://orders-service/v1/orders/   .... Response: 201
Getting order from: http://orders-service/v1/orders/376088   .... Posted order 376088 equals returned order: OrderBean{id='376088', customerId=5, state=CREATED, product=STOCKINGS, quantity=1, price=1.0}
Posting order to: http://orders-service/v1/orders/   .... Response: 201
Getting order from: http://orders-service/v1/orders/376089   .... Posted order 376089 equals returned order: OrderBean{id='376089', customerId=1, state=CREATED, product=JUMPERS, quantity=1, price=1.0}

Симулятор заказов и платежей взаимодействует с конечной точкой REST сервиса заказов, публикуя новые заказы и получая их обратно от конечной точки /v1/validated. Здесь мы видим код 201 ответа в журнале, означающий, что симулятор и сервис заказов взаимодействуют правильно, и сервис заказов правильно считывает заказы из хранилища состояния Kafka Streams.

Резюме

Успешное внедрение микросервисов требует тщательной координации в вашей инженерной организации. В этом посте вы увидели, как микросервисные фреймворки полезны для стандартизации практики разработки в ваших проектах. С помощью GitOps вы можете уменьшить сложность развертывания и расширить возможности таких важных функций, как откат. Если у вас есть идеи относительно областей, связанных с DevOps, о которых вы хотите узнать от нас, пожалуйста, не стесняйтесь задать вопрос в проекте, или, что еще лучше - PRs открыты для этого!

Все коды на изображениях для копирования доступны здесь.


Перевод материала подготовлен в рамках курса «Microservice Architecture». Всех желающих приглашаем на открытый урок «Атрибуты качества, тактики и паттерны». На этом вебинаре рассмотрим, что такое качественная архитектура, основные атрибуты качества и тактики работы с ними.

OTUS
Цифровые навыки от ведущих экспертов

Комментарии 1

    0

    Кстати, для взаимодействия с Kubernetes в этом проекте его авторы (из Confluent) использовали наш shell-operator:

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

    Самое читаемое