Всем привет!

Меня зовут Иван Гулаков, я техлид 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.



Комментарии (6)


  1. S1M
    00.00.0000 00:00
    +1

    А чем https://strimzi.io/ то не устроил ? Можно к его crd свой контроллер добавить и не пилить всё с нуля


    1. kepiukik Автор
      00.00.0000 00:00
      +3

      Приветствую

      Strimzi смотрели, давно правда. Там основной проблемой было, что ACL были склеены с сущностью KafkaUser, что, в общем-то, логично.

      Я выше писал, что основной целью было дать возможность разработке полностью управлять сущностями, которые

      относятся к их проекту. А в такой конфигурации нужен был бы кто-то, кто имеет доступ к CR соседей, или вообще ко всем. На наш общий чарт и схему деплоя это, соответственно, не ложилось. Контроллер бы(даже целых 2) пришлось бы свой писать, как вы и сказали. Ну а поддерживать форк проекта на джаве с этими контроллерами желания особо не было.

      Свой оператор на operator-sdk написать было проще, да и поддерживать в дальнейшем тоже.


  1. diver22
    00.00.0000 00:00

    А как вы управляете пользователями? На сколько я знаю, это можно сделать либо через запуск консольных команд, либо через java client. Ни Python клиент, ни любой другой, на базе librdkafka, этого не умеют.


    1. kepiukik Автор
      00.00.0000 00:00

      Ваша правда, в golang клиенте(у нас kadm используется) этого тоже нельзя делать. Приходится дергать из User контроллера cli'шку.


  1. Suneater
    00.00.0000 00:00

    Интересное решение, спасибо. Подскажите, а вы креденшелсы в гите храните нешифрованными? При использовании того же SealedSecrets можно было бы добавить в манифест User дополнительную конфигурацию вроде такой:

    spec:
      user: pupkin
      password: pupkin
      existingSecret:
        name: secret_name
        userKey: username
        passwordKey: password

    Тогда values-файл для разработчиков мог бы выглядеть как-то так:

    Kafka:
        Auth:
          User: encrypted_user
          Password: encrypted_pass
          ExistingSecret: secret_name # если определено, то Auth.User и Auth.Password игнорируются
        Topics:
          ...

    По аналогии с Helm-чартами от Bitnami


    1. kepiukik Автор
      00.00.0000 00:00


      Креды шифруем через SOPS https://github.com/mozilla/sops
      Выглядит в репозитории это вот так примерно:

      Kafka:
          User: ENC[AES256_GCM,data:VxCWb+K/PdAe5mL4HhzN,iv:qm9Ju0bVLuzctsm5x1XjFvhBbWPkXovz+SFou3ot7MU=,tag:j367spKx0iafJmQGgpr0FQ==,type:str]
          Password: ENC[AES256_GCM,data:pDkq2ZDGTyJ48jqYH7PAyt2OJ5A=,iv:nlVk0eRUPO8rTOmtZIKPqaaTa5TJMPUIK+FpW0RAw/8=,tag:CE1UcOp1cfSk7+vLS03lgQ==,type:str]

      Идея с инжектом в спеку полей из секрета была, но решили отказаться от нее по нескольким причинам:
      1) API Kafka.User и так скрыто от разработчиков с помощью RBAC, так что расшифрованный секрет и CR User, по сути, представляют из себя одно и то же
      2) В будущем планировали перейти на аутентификацию серт TLS серты от Vault.

      За замечание спасибо, может и реализуем когда-то дополнительную поддержку.