Меня зовут Игорь Латкин, я архитектор в компании KTS. Сегодня хочу поделиться нашей внутренней разработкой — Kubernetes-контроллером mirrors. Мы создали его внутри нашего DevOps-отдела для копирования Kubernetes-секретов между неймспейсами кластера. В итоге mirrors превратился в универсальный инструмент синхронизации данных из разных источников. В статье расскажу, с чего все начиналось и к чему мы в итоге пришли. Возможно, статья вдохновит вас на написание собственного контроллера под ваши задачи. Что будет в статье: С чего все началось Динамические окружения с TLS Проблемы большого количества сертификатов Проблема единственного сертификата Существующие решения и их недостатки SecretMirror for the resque Архитектура контроллера Копирование секретов между неймспейсами Копирование секрета из HashiCorp Vault в Kubernetes-кластер Копирование секрета из Kubernetes-кластера в HashiCorp Vault Бонусы Распространение динамических секретов из Vault в Kubernetes Secret Копирование из Vault в Vault  Итоги и графики для сравнения 💡 Перед нами стояла единственная большая задача — обеспечение TLS в динамических окружениях в dev-контуре KTS.  Нашим dev-кластером пользуются все команды разработки, поэтому процесс обеспечения защищенного соединения должен быть полностью автономным. В дальнейшем мы поняли, что можем решать с помощью разработанного решения и другие задачи, о которых подробнее расскажу ниже. Начнем с динамических окружений. Задача копировать секреты между неймспейсами в Kubernetes кластере возникла в KTS уже давно. Наши процессы построены так, что каждая команда — даже каждый разработчик в компании — достаточно независим и самостоятелен. Каждый может сам себе создать репозиторий в Gitlab, подключить общий CI/CD пайплайн, указав желаемые доменные имена в настройках проекта, и буквально за несколько минут получить задеплоенный проект для dev- и production-окружений. И необходимости привлекать для этого devops-команду практически нет. Такой gitlab-ci в проекте превращается в развернутый пайплайн, покрывающий задачи сборки и раскладки проекта в dev- и prod-окружении: Для dev-окружения практически все проекты раскладываются на одном и том же поддомене, в дальнейшем будем ссылаться на него как dev.example.com. То есть проекты могут быть разложены на такие поддомены: projectA.dev.example.com projectB.dev.example.com projectC… Также необходимо иметь в виду, что некоторые приложения состоят из нескольких микросервисов, которые объединены разными ingress-правилами. Например: Фронтенд-приложение обслуживает все пути домена projectA.dev.example.com/ API-приложение обслуживает все пути домена projectA.dev.example.com/api Так как они обслуживают один и тот же домен, для этих ingress желательно корректно прописать один и тот же TLS-сертификат. А они деплоятся в разные неймспейсы для большей изоляции и просто потому, что так по дефолту работает интеграция Gitlab с Kubernetes через сертификаты. Интеграция Gitlab с Kubernetes через сертификаты, вообще говоря, уже deprecated и надо бы переходить на Gitlab Agent. Но сейчас пока не об этом. Казалось бы, можно в каждом ingress каждого проекта просто выписывать свой собственный сертификат, и проблема решена. Именно так мы и жили какое-то время. Но в конце концов уперлись в ограничения Let’s Encrypt по количеству выписываемых сертификатов. Это особенно остро ощутилось в период массового переезда с одного кластера на другой, когда все сертификаты надо было перевыпустить. Второй минус этого решения: нужно ждать, пока сертификат выпишется при создании новой ветки. Этот процесс может еще и зафейлиться. Поэтому кажется естественной идея держать один единственный сертификат и давать всем к нему доступ. Но тогда всплывает другая проблема. Сертификат, выписанный на *.dev.example.com, валиден для домена projectA.dev.example.com, но не валиден для feature1.projectA.dev.example.com. Поэтому когда мы захотим построить динамические окружения с поддоменами, то окажемся в заложниках такого решения. Поэтому сначала мы сформулировали такую задачу: 💡 Задача 1  Для обеспечения TLS в динамических окружениях проектов необходимо выписывать сертификат для ветки main каждого проекта и копировать Secret во все остальные неймспейсы этого проекта Для проекта <project_name>-<project_id>-<branch_name>-<some_hash> неймспейсы получат примерно такие названия: project-a-1120-main-23hf project-a-1120-dev-4hg5 project-a-1120-feature-1-aafd project-b-1200-main-7ds9 project-b-1200-feature1-42qq То есть физически объект Certificate создается только в неймспейсах project-a-1120-main-23hf и project-b-1200-main-7ds9. Во все остальные должен быть скопирован результат выписывания сертификата — Secret, содержащий tls.crt и tls.key: Теперь рассмотрим вторую задачу, которую нам удалось покрыть. Она достаточно близка к первому случаю, но имеет свои особенности. Представим, что мы говорим о неком prod-окружении, и это окружение расположено в Kubernetes-кластере. Допустим, оно состоит из нескольких кластеров, распределенных по разным географическим зонам. И все эти кластеры обслуживают один и тот же домен, например myshop.example.com. Мы очень хотим, чтобы наш конечный пользователь видел один и тот же сертификат, независимо от того, на какой из кластеров попадет. И тут уже могут быть варианты, откуда он изначально берется: Сертификат может быть куплен. Сертификат может быть выписан также через cert-manager в одном из кластеров. Тогда встает задача по его доставке в соседние кластеры. Естественно предположить, что логично использовать некое централизованное хранилище и из него переливать сертификат в нужные места использования. До того, как мы внедрили наше решение, задача решалась в лоб, но в целом жизнеспособно: сертификат накатывался как часть инфраструктуры через helm во все нужные кластеры. Чтобы обновить сертификат, достаточно было обновить его в helm-чарте и заново выкатить. Но хотелось больших автоматизации и безопасности: например, нам не нравилось хранить сертификат в git-репозитории. Конечно, проблемы возникают не только с сертификатами. Это могут быть любые данные, которые хочется иметь сразу в нескольких местах — credentials для registry образов, логины/пароли от баз данных и многое другое. Теперь мы сформулировали вторую задачу: 💡 Задача 2 Уметь автоматически синхронизировать Secret из централизованного хранилища во все нужные Kubernetes-кластеры и выбранные внутри них неймспейсы. Поняв, что именно хотим сделать, мы начали искать удовлетворительные решения. Два проекта, которые мы рассматривали для решения первой задачи: kubed от AppsCode kubernetes-reflector от EmberStack kubed нам не подошел сразу, т.к. он полагается на лейблы, которые нужно проставить как на сами Secret или ConfigMap, так и на неймспейсы, в которые их нужно скопировать. Ставить лейблы в динамике при создании неймспейса у нас не было возможности. Подробнее про работу с kubed расписано тут. kubernetes-reflector работает похожим образом, но умеет сам следить за объектами Сertificate и прописывать дополнительные аннотации/лейблы, чтобы активировать синхронизацию сeкрета. Также в нем была возможность указать регулярное выражение, под которое должен попасть неймспейс, чтобы в него скопирован был скопирован сeкрет. Необходимости навешивать лейблы на сами объекты неймспейса нет. Посмотрим на пример: Здесь самой важной является аннотация reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces. Благодаря ей мы должны были легко решать первую задачу: деплоили сертификат только для ветки main, во все остальные он автоматически скопирован, а куда именно, идеально описывает маска project-a-1120-*. Мы настроили все проекты и задеплоили это решение в наш dev-кластер. В целом kubernetes-reflector хорошо справлялся со своей задачей, пока мы не начали получать жалобы от разработчиков по типу: «Сертификат не выписывается, помогите». На деле сертификат-то, конечно, был выписан, но почему-то не копировался в новый фича-бранч. На графиках мы видели следующую картину (цвета обозначают разные поды): Из-за высокого потребления памяти и того, что график разноцветный, уже понятно, что OOM просто убивал reflector, после чего он начинал работу заново. Потребление сетевых ресурсов также удивляло. К сожалению, с ростом числа проектов недостатки внутренней архитектуры reflector давали о себе знать. На тот момент в кластере было около 10к различных Secret. Судя по всему, reflector следил за каждым из них и достаточно неэффективно использовал информацию о неймспейсе. Поэтому мы видели большое использование сетевого трафика на графиках. В более новых версиях reflector были адресованы проблемы с перфомансом, но это было уже спустя 2 месяца нашего переезда на собственное решение Еще нам очень хотелось, чтобы Secret в нужном неймспейсе появлялся сразу же после его создания в Kubernetes-кластере, а не спустя какой-то внутренний период синхронизации. В конечном счете мы решили в кратчайшие сроки разработать собственный инструмент, который действовал бы согласно нашим задачам и учитывал проблемы с производительностью reflector. За 2 недели нам удалось это сделать, выкатить в наш dev-кластер и пересадить все команды на его использование. Итак, какие требования предъявлялись к новому контроллеру: Должен работать со своим собственным CRD. Он будет назван SecretMirror. Это требование радикально снижает нагрузку на API-сервер и производительность контроллера: сущностей типа SecretMirror в кластере априори в разы меньше, чем самих Secret. В SecretMirror должен задаваться список регулярных выражений неймспейса, в которые указанный Secret должен быть скопирован. Это нам позволит гибко управлять целевым местоположением ресурсов. Контроллер должен следить не только за SecretMirror, но и за объектами неймспейса. Это позволит копировать Secret в новый неймспейс сразу после его появления, а не после периода синхронизации. Должен поддерживать актуальный список неймспейсов в кэше в памяти. Тогда мы экономим на походах за этим списком всякий раз, когда нужно синхронизировать Secret в указанные неймспейсы. Такой кэш как раз легко поддерживать благодаря выполнению пункта 3, и мы можем динамично добавлять в него неймспейс и удалять. При удалении SecretMirror все Secret, созданные контроллером, автоматически должны быть удалены. Однако необходимо оставить возможность отключить такое поведение. Синхронизация Secret не должна происходить моментально при его изменении. Иначе нам придется опять таки следить за всеми секретами в кластере. Достаточно, если синхронизация будет происходить раз в какой-то период: например, 3 минуты. При этом должна быть возможность изменить этот интервал для каждого отдельного SecretMirror. Контроллер должен быть расширяем. Тогда в качестве источника или назначения Secret можно использовать внешние системы, например Vault. Все достаточно просто. Внутри mirrors запущены 2 контроллера, которые отслеживают любые изменения двух GVK — mirrors.kts.studio/v1alpha2.SecretMirror и v1.Namespace. Все изменения, касающиеся неймспейса, сохраняются в памяти контроллера, чтобы минимизировать походы в Kubernetes API. Вспомним вторую задачу: синхронизировать данные между несколькими кластерами. Внутри KTS мы активно пользуемся HashiCorp Vault для хранения секретных данных, по этому же пути решили пойти и здесь. Vault будет использоваться как система синхронизации состояния. Для этого нужно было научить SecretMirror читать из Vault секреты и писать в него. Ниже в примерах сценариев применения посмотрим, как этим можно пользоваться. Для начала подробно рассмотрим первый сценарий, ради которого первоначально создавался SecretMirror. Манифест выглядит так: Его задача — копировать Secret с именем mysecret из неймспейса default во все неймспейсы, которые будут начинаться либо с project-a-, либо с project-b-. Применим манифест и выведем список всех SecretMirror: Задеплоим Secret, который ожидает SecretMirror: Статус SecretMirror при этом изменится c Pending на Active: Создадим неймспейсы, в которые должен скопироваться Secret: Секреты моментально будут скопированы в эти новые неймспейсы: В describe SecretMirror можно увидеть более подробную информацию по событиям, происходящим с объектом: Посмотрим на другой сценарий. Представим, что в нашем Vault-кластере есть секрет со следующим содержимым… … и наша цель — периодически синхронизировать эти данные с Secret в Kubernetes-кластере. Посмотрим, как будет выглядеть манифест для SecretMirror: Благодаря такой конфигурации контроллер mirrors будет синхронизировать Vault-секрет с именем myvaultsecretв Kubernetes Secret с именем myvaultsecret-sync в неймспейсах, имена которых начинаются с project-c-. На данный момент наша интеграция с Vault поддерживает 2 вида аутентификации: Token AppRole Подробнее про то, как настроить аутентификацию, можно прочитать в README проекта. Описанный выше второй сценарий с легкостью решает задачу синхронизации секретных данных с использованием централизованного хранилища. В частности, в Vault мы можем положить данные tls.crt и tls.key, настроить SecretMirror и получить возможность всегда иметь актуальное состояние сертификата в одном или нескольких кластерах. Возвращаясь к одной из наших исходных задач, можно вспомнить, что условный TLS-сертификат может быть также выписан с помощью cert-manager. Хочется иметь возможность синхронизировать его с остальными кластерами нашего production-контура. Здесь можно воспользоваться той же интеграцией с Vault. Только на этот раз мы будем синхронизировать секрет не из Vault, а в него из Kuberentes Secret. Меньше слов, больше YAML: В данном случае видно, что в качестве source будет использован Secret mysecret, а в качестве назначения — Secret myvaultsecret в Vault. Для синхронизации Secret в остальные кластеры в них нужно будет создать SecretMirror, как в предыдущем сценарии: для синхронизации из Vault в Secret Kubernetes. Посмотрим на несколько «бонусных» сценариев, которые можно решить с помощью SecretMirror ввиду заложенной в него архитектуры. HashiCorp Vault известен также тем, что умеет «на лету» генерировать данные для входа в ту или иную поддерживаемую базу данных. Например, сгенерировать временный пароль для доступа к PostgreSQL или MongoDB для какого-нибудь скрипта, создающего бэкап БД. Статические логины/пароли могут утекать самыми разными способами: в логах, в мессенджере, просто храниться в открытом виде на компьютере разработчика. Динамические секреты позволяют избежать этой проблемы, создавая временные доступы и самостоятельно уничтожая их по истечении таймаута. Так выглядит пример SecretMirror, синхронизирующий динамический пароль для MongoDB: Обратите внимание, что в каждый момент синхронизации mirrors будет продлевать так называемый lease, а не генерировать каждый раз новый пароль. Поэтому данные для входа будут одинаковые на протяжении всего периода max_ttl, задаваемого в Vault. Вы могли догадаться, что есть возможность указать source.type = vault и destination.type = vault. Это действительно так, и в данном случае Kubernetes Secret вообще не используются. Одно из возможных применений — копирование конкретного секрета из одного кластера Vault в другой, или копирование ключа из одного места в другое в рамках одного Vault. Пример копирования между кластерами Vault: Была ли решена первоначальная задача? Безусловно. Все команды теперь счастливы — сертификаты на фича-ветках появляются моментально, секреты между кластерами у нескольких DevOps-клиентов синхронизируются без каких-либо ручных действий, и еще остается поле для улучшений и доработок. Использование CPU, памяти и сети нашим контроллером находится на очень низком уровне и на кластер не оказывает практически никакой дополнительной нагрузки.  Это был первый Kubernetes-контроллер, который мы разработали самостоятельно для своих нужд. Как оказалось, это не так уж сложно и позволяет создавать очень кастомные сценарии использования Kubernetes, а также просто шире открывает глаза на его внутреннее устройство. Если интересно попробовать mirrors у себя, вот несколько ссылок: https://github.com/ktsstudio/mirrors — GitHub репозиторий контроллера. Helm-чарт с инструкцией по установке. Terraform-модуль для установки чарта выше. Другие статьи про DevOps для начинающих: Сравнение Kubernetes с другими оркестраторами: Docker Swarm, Nomad, Openshift Как изучать Kubernetes джуну — и зачем Как деплоить приложение на Django в Kubernetes с нуля Другие статьи про DevOps для продолжающих: Как построить эргономичный мониторинг Как экономить ресурсы, затрачиваемые Kubernetes За последние годы де-факто стандартом оркестрации и запуска приложений стал Kubernetes. Поэтому умение управлять Kubernetes-кластерами является особенно важным в работе любого современного DevOps-инженера.  Порог входа может казаться достаточно высоким из-за большого числа компонентов и связей между ними внутри системы. На курсе «Деплой приложений в Kubernetes» мы рассмотрим самые важные концепции, необходимые для управления кластерами любой сложности и научим применять эти знания на практике. 👉 Почитать про курс подробнее можно здесь: https://vk.cc/cn0ty6  Системный архитектор