Меня зовут Иван Гулаков, я техлид DevOps-команды, отвечающей за инфраструктуру, где работают облачные сервисы #CloudMTS.
Сегодня я расскажу, как с помощью самописного оператора Kubernetes мы автоматизировали управление пользователями и топиками наших Kafka-кластеров.
Как мы используем 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)
diver22
00.00.0000 00:00А как вы управляете пользователями? На сколько я знаю, это можно сделать либо через запуск консольных команд, либо через java client. Ни Python клиент, ни любой другой, на базе librdkafka, этого не умеют.
kepiukik Автор
00.00.0000 00:00Ваша правда, в golang клиенте(у нас kadm используется) этого тоже нельзя делать. Приходится дергать из User контроллера cli'шку.
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
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.
За замечание спасибо, может и реализуем когда-то дополнительную поддержку.
S1M
А чем https://strimzi.io/ то не устроил ? Можно к его crd свой контроллер добавить и не пилить всё с нуля
kepiukik Автор
Приветствую
Strimzi смотрели, давно правда. Там основной проблемой было, что ACL были склеены с сущностью KafkaUser, что, в общем-то, логично.
Я выше писал, что основной целью было дать возможность разработке полностью управлять сущностями, которые
относятся к их проекту. А в такой конфигурации нужен был бы кто-то, кто имеет доступ к CR соседей, или вообще ко всем. На наш общий чарт и схему деплоя это, соответственно, не ложилось. Контроллер бы(даже целых 2) пришлось бы свой писать, как вы и сказали. Ну а поддерживать форк проекта на джаве с этими контроллерами желания особо не было.
Свой оператор на operator-sdk написать было проще, да и поддерживать в дальнейшем тоже.