Управляем пользователями и топиками Apache Kafka с помощью оператора Kubernetes

Всем привет!

Меня зовут Иван Гулаков, я техлид DevOps-команды, отвечающей за инфраструктуру, где работают облачные сервисы #CloudMTS.

Сегодня я расскажу, как с помощью самописного оператора Kubernetes мы автоматизировали управление пользователями и топиками наших Kafka-кластеров.

image

Как мы используем Kafka


В инфраструктуре #CloudMTS Kafka используется:

  • для асинхронного взаимодействия микросервисов облачной платформы;
  • в качестве буфера для логов, которые потом вычитываются kafka-connect и отправляются в elasticsearch для дальнейшего анализа и хранения;
  • для сбора метрик в формате Prometheus для дальнейшей отправки в VictoriaMetrics.


Для этого даже написали свой агент.

Всего в инфраструктуре #CloudMTS насчитывается около десятка on-premise-кластеров Kafka, расположенных в различных регионах и дата-центрах. Мы используем версию от Confluent.

Как было раньше


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

Аутентификацию мы настроили SASL/SCRAM. В отличие от PLAIN/SASL, с помощью нее новых пользователей можно добавлять динамически без перезагрузки брокеров.

Авторизацию выбрали на основе ACL: она универсальна и работает в том числе на Apache Kafka, да и знакомо с ней большое число людей.

А вот с автоматизацией добавления пользователей/топиков/ACL мы не заморачивались, она была разрозненна и сделана на полуручном приводе. Когда число сотрудников перевалило за сотню, а количество сервисов — за несколько десятков, такой подход стал стопорить работу. Пришлось сесть и спроектировать решение, которое раз и навсегда избавит нас от головной боли.

Что хотелось получить


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

  • Централизованно создавать топики в кластере. До этого топики создавались клиентами самих приложений при их инициализации. Росло число команд разработки, появился целый зоопарк библиотек для взаимодействия с Kafka. Разработчики часто пользовались дефолтными настройками библиотек, и стали происходить забавные ситуации. Например, создавались мертворожденные топики, у которых количество реплик было меньше заданного в кластере min.insync.replicas, и такие топики блокировались на запись.

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

  • Стандартизировать именование пользователей в кластере. Testuser«ы должны были умереть.
  • Автоматизировать добавление ACL. Таскать за собой портянку переменных для ansible, а уж тем более набивать длинные команды руками изрядно надоело.

    Но была и еще одна проблема: ошибки при конфигурации ACL, которые допускали разработчики. Например, при веточном деплое в dev или на личном стенде разработчика иногда происходили ситуации, когда отладочное приложение начинало «красть» сообщения у основной версии и происходили различные неприятные казусы.

  • И самое главное — сделать так, чтобы все вышеперечисленное мог делать сам разработчик запуском без помощи инженеров по эксплуатации. Я имел дело с заведением всех этих сущностей по заявкам на другом месте работы, и возвращаться к подобному опыту совсем не хотелось. Было жалко и команды разработки, которым пришлось бы тратить время на дополнительное заполнение тикетов по шаблону, что у них точно не вызовет восторга, и своих инженеров, которым бы эти заявки пришлось выполнять.


Как выбирали решение


Очевидно, что следующий вопрос, который вставал перед нами, —, а как это нормально централизовать и стандартизировать?

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

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

Рассматривали вариант с запуском во время деплоя приложения неких отдельных джобов, которые подготовят кластер Kafka. Но тоже отмели, так как в этом случае мы жертвовали бы скоростью деплоя приложения (ведь сперва должны отработать джобы по настройке Kafka) и терялась единая точка правды о состоянии инфраструктуры.

Кроме того, в обоих случаях хромал бы процесс валидации хранимых в Gitlab манифестов.

И вот тут-то и пришло озарение — все наши приложения живут в k8s, так почему бы не воспользоваться его функциональностью и не написать оператор?

Как работают операторы


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

Если кратко, оператор реагирует на некие события в кластере и в зависимости от них производит некоторые действия и приводит подконтрольные ему (оператору) сущности к желаемому состоянию. Это и есть тот самый reconciliation (control loop). В нашем случае мы хотим приводить внешнюю по отношению к Kubernetes систему, то есть наш кластер Kafka, к желаемой конфигурации. Мы без проблем можем завязаться на кубовые ивенты по изменению наших CR и решить сразу несколько проблем:

  • Только сами по себе CRD и CR решают два важных вопроса: как структурированно хранить информацию о пользователях/топиках/ACL и прочих штуках и заодно как эту информацию валидировать не отходя от кассы.
  • Пресловутая событийная модель уже реализована за нас. Не нужно придумывать какие-то велосипеды и решать, когда реагировать, а когда нет. Для это уже есть готовые SDK, есть ретраи до победного «из коробки».
  • Склеить инфраструктуру и приложение в единое целое.


Почему решили писать сами


— Нам был нужен довольно узкий функционал по настройке сущностей в кластере и более тонкий контроль производимых с кластером действий. Грубо говоря, реализовали ровно столько, сколько нужно.

— Это решение в дальнейшем нужно было интегрировать в наш общий helm-чарт для валидации создаваемых ресурсов согласно нашим стандартам. В целом планировалось, что конечным пользователем оператора будет не инженер эксплуатации, а разработчик (пусть и через врапперы). У него будут простенькие статусы в CR, чтобы понять, пошло ли что-то не так или все хорошо.

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

CRD и что умеет наш оператор


Коротко о том, что в итоге оператор умеет делать и как это представлено в виде CR.
Работа с пользователями. Тут все довольно просто, каких-то фишек нет.

apiVersion: kafka.cloud.mts.ru/v1alpha1
kind: User
metadata:
  name: pupkin
  namespace: default
spec:
  user: pupkin
  password: pupkin


Работа с топиками. Здесь уже интереснее — доступен полный менеджмент настроек.
Реплики, партиции, любые доступные в Confluent Kafka настройки можно прокинуть на топик в виде мапы (advanced_options). Также есть флажок для принудительного удаления топика после удаления CR. Иногда это актуально для разработчиков, если надо почистить за собой, особенно на dev.

apiVersion: kafka.cloud.mts.ru/v1alpha1
kind: Topic
metadata:
  name: pupkin-topic
  namespace: default
spec:
  name: pupkin-topic
  replication_factor: 3
  partitions: 3
  purge_on_service_deletion: true
  advanced_options:
    "cleanup.policy": "delete"
    "delete.retention.ms": "86400000"


Работа с ACL. Ну и самый болезненный для нас вопрос — менеджмент ACL. История с ACL сама по себе довольно сложная и запутанная, а тут надо было еще и попытаться как-то упростить ее для разработчиков. Подробно про то, как это работает внутри, можно почитать у Confluent.

Из небольших нюансов: реализовано 2 resource_type — topic и group. Другие сущности нам не понадобились. Компоновка настроек чуть-чуть отличается от формата консольной утилиты, чтобы манифест было удобнее читать.

apiVersion: kafka.cloud.mts.ru/v1alpha1
kind: ACL
metadata:
  name: pupkin-topic
  namespace: default
spec:
  principal: pupkin
  resource_pattern: literal
  resource_type: topic
  resource_name: pupkin-topic
  operations:
    - ALTER
    - ALTER_CONFIGS
    - CREATE
    - DELETE
    - DESCRIBE
    - DESCRIBE_CONFIGS
    - READ
    - WRITE


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

Выглядит это так:

apiVersion: kafka.cloud.mts.ru/v1alpha1
kind: ACL
metadata:
  name: pupkin-topic
  namespace: default
spec:
  operations:
    - READ
    - WRITE
    - DESCRIBE
    - DESCRIBE_CONFIGS
    - habrahabr
  principal: pupkin
  resource_name: pupkin-topic
  resource_pattern: literal
  resource_type: topic
status:
  available_operations:
    - READ
    - WRITE
  bad_operations:
    - habrahabr
  help_message: >-
    Controller is stuck. Remove bad operations [habrahabr] from spec.operations
    before proceeding
  missing_operations:
    - DESCRIBE
    - DESCRIBE_CONFIGS
  orphaned_operations: null
  synced: false


Я уже упоминал ранее, что у нас есть общий Helm-чарт для деплоя приложений.
В двух словах — есть репозиторий с сервисом, там есть несколько наборов *.values.yaml, на основе которых происходит прогон пайплайна выкатки, в том числе рендерится непосредственно чарт с приложением. Именно там лежат шаблоны манифестов, которые были рассмотрены выше, разработчики взаимодействуют с оператором через *values-обертку. Никаких составлений и применений манифестов вручную. Вместе с приложением приезжает и инфраструктура, то есть точка входа для настройки единая.

Выглядит это примерно вот так:

Kafka:
    User: encrypted_user
    Password: encrypted_pass
    Topics:
        - Name: licensing.billing-plugin.start-tact
          ReplicationFactor: 3
          Partitions: 4
          PurgeOnServiceDeletion: false
          AdvancedOptions:
              "cleanup.policy": "delete"
          ACLs:
              ConsumerGroupName: licensing-billing-plugin
        - Name: licensing.billing-plugin.fetch-orgs
          ReplicationFactor: 3
          Partitions: 4
          PurgeOnServiceDeletion: false
          AdvancedOptions:
              "cleanup.policy": "delete"
          ACLs:
              ConsumerGroupName: licensing-billing-plugin
              Custom:
                - Principal: networks-networker
                  ResourcePattern: literal
                  Operations:
                    - READ
                    - WRITE
                    - DESCRIBE


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

Как защитились от ошибок: валидация


Возникает закономерный вопрос —, а как защитить непосредственно саму Kafka от деструктивных запросов? Эту историю можно поделить на две больших части — те запросы, которые мы в принципе хотим отсекать, и то, что может быть нужно, но разработчикам мы давать делать не хотим.

Первая часть вопроса решена на уровне CRD и непосредственно самого кода (внутри использован клиент kadm).

Kubebuilder позволяет гибко настраивать и валидировать поля в CR, так что различные базовые ошибки вроде несовпадения типов и прочего отфильтровываются именно там.

А вот наши стандарты и защита от шаловливых ручек реализованы уже на уровне Helm. Стандартизированные названия топиков, разделители в именах, длина имен, защита от именования своего пользователя/топика именем чужого проекта, исключение ACL вроде CREATE — всю эту бизнес-логику мы проверяем уже на этапе деплоя приложения. Если разработчик заполнил конфиг не по конвентам и инструкции, то деплой упадет и выдаст список ошибок для исправления. Такое разделение сделано для того, чтобы можно было спокойно использовать оператор на своих стендах и ломать их как угодно, но вот сломать коммунальный кластер Kafka уже было бы нельзя.

Что в итоге получили


Оператор (в связке с Helm) в итоге решил для нас следующие проблемы:

  • Единая точка правды о сущностях кластера Kafka в виде Kubernetes.
  • Возможность снять дамп манифестов (CR), быстро восстановиться из него в случае аварии.
  • Приложение и инфраструктура под него теперь становились одним целым — в результате установки Helm-чарта мы гарантированно получаем нужную нам конфигурацию кластера.
  • Вахтер теперь может заняться более полезными делами — кнопку за него нажимает Reconciliation Loop.
  • Значительное ускорение скорости деплоя приложения с точки зрения CI/CD: последовательные джобы не нужны. Более того, оператор обрабатывает событие в десятки раз быстрее, чем запуск пода с новой джобой gitlab runner’ом.
  • Разработчики теперь могут самостоятельно настраивать пользователей/топики и ACL для своих приложений.
  • Статусы о состоянии сущностей в Kafka можно посмотреть рядом с приложением в Kubernetes.
  • Защита от деструктивных изменений и выстрелов по ногам.


Ну и парочка минусов, куда уж без них:

  • Накладные расходы по разработке и поддержке решения выше, чем при использовании условных плейбуков, тут увы.
  • Чисто теоретически появляется дополнительная точка отказа CI/CD-конвейера, но это небольшая цена за удобство и скорость доставки изменений в инфраструктуру.


В целом плюсы для нас значительно перевесили минусы, и мы продолжаем развивать это решение. В дальнейших планах интеграция с Vault PKI, добавление TLS-аутентификации (пользователей) и интеграция оператора с различными нашими служебными приложениями, взаимодействующими с Kafka.
gz5opfttva6zuzxinkdyakvpi1g.png

© Habrahabr.ru