Kubernetes — превосходная платформа для развертывания приложений и управления ими, но иногда простые задачи, такие как наличие одних и тех же данных — в нашем случае секретов — между пространствами имен, могут вызвать небольшие проблемы, и вам придется создавать собственные решения этих проблем.
Привет! Меня зовут Игорь Латкин, я архитектор решений в КТС.
Сегодня я хотел бы поделиться проектом, который мы разработали внутри компании — контроллер зеркала Kubernetes. Мы создали его внутри нашего отдела DevOps, чтобы решить проблему копирования секретов Kubernetes между пространствами имен кластера. В результате зеркала превратились в инструмент общего назначения, предназначенный для синхронизации данных из разных источников. В статье я расскажу историю с чего все началось и к чему мы пришли в конце пути. Надеюсь, это вдохновит вас на создание собственного контроллера, отвечающего вашим потребностям.
Это план статьи:
- Как все началось
- Динамические среды с TLS
- Проблема слишком большого количества сертификатов
- Проблема единого сертификата
- Существующие решения и их недостатки
- SecretMirror для спасения
- Архитектура контроллера
- Копирование секретов между пространствами имен
- Копирование секретов из хранилища HashiCorp в кластер Kubernetes
- Копирование секретов из кластера Kubernetes в хранилище HashiCorp
- Бонус
- Результаты и сравнительные диаграммы
Как все началось
💡 Перед нами стояла одна большая задача — включить TLS в динамических средах в среде разработки KTS.
Все команды разработчиков используют наш dev-кластер, поэтому процесс обеспечения безопасного соединения должен быть полностью автономным. Позже мы поняли, что могли бы использовать разработанное решение и для других задач, о которых я также расскажу в этой статье позже.
Начнем с динамических сред.
Динамические среды с TLS
KTS уже некоторое время сталкивался с задачей копирования секретов между пространствами имен в кластере Kubernetes. Наши процессы построены так, что каждая команда — и даже каждый разработчик в компании — вполне самостоятельны и самодостаточны.
Каждый разработчик может создать репозиторий в Gitlab, подключить общий конвейер CI/CD, определить необходимые доменные имена для проекта в локальном файле gitlab-ci.yml и развернуть проект на нескольких платформах. и производственных сред за пару минут. И им вообще не нужно привлекать для этого команду DevOps.
include: project: mnt/ci file: front/ci.yaml ref: ${CI_REF} variables: DEV_BASE_DOMAIN: projectA.dev.example.com DEV_API_DOMAIN: projectA.dev.example.com PROD_DOMAIN: projectA.prod.example.com
Этот gitlab-ci.yml в проекте превращается в гораздо более крупный пайплайн, охватывающий задачи по сборке и развертыванию проекта в различных средах:
Почти все проекты в среде разработки развертываются на поддомене одного домена, который мы будем называть dev.example.com. То есть проекты можно развернуть на следующие поддомены:
- projectA.dev.example.com
- проектB.dev.example.com
- проектC.dev.example.com
Следует также отметить, что некоторые приложения состоят из нескольких микросервисов, объединенных разными правилами входа. Например:
- Внешнее приложение поддерживает все пути URL-адреса projectA.dev.example.com/
- Приложение API поддерживает все пути URL-адреса projectA.dev.example.com/api
Поскольку они размещены на одном и том же доменном имени, желательно правильно указать один TLS-сертификат для этих входов. Они развертываются в разных пространствах имен для улучшения изоляции и просто потому, что именно так работает «на основе сертификатов» интеграция Gitlab с Kubernetes.
Вообще говоря, интеграция Gitlab с Kubernetes на основе сертификатов уже устарела, и ее стоит перейти на Gitlab Agent (или на другой инструмент GitOps, такой как ArgoCD или FluxCD). Но это не то, что мы собираемся рассказать в этой статье.
Проблема слишком большого количества сертификатов
Можно подумать, что можно просто выдать сертификат при каждом входе каждого проекта, и проблема решена. Именно так мы и жили какое-то время. Но в итоге мы уперлись в Ограничения Let’s Encrypt по количеству выданных сертификатов. Особенно ярко мы это испытали, когда у нас была массовая миграция с одного кластера на другой, или когда по разным причинам приходилось перевыпускать все сертификаты.
Второй недостаток этого решения заключается в том, что вам нужно ждать определенное время, пока сертификат действительно не будет выдан, например, при создании новой ветки проекта. Кроме того, этот процесс может завершиться неудачей, и у вас может вообще не быть сертификата. Вот почему кажется естественным хранить только один сертификат и предоставлять доступ к нему всем.
Но тогда возникает другая проблема.
Сертификат, выданный *.dev.example.com, действителен для projectA.dev.example.com, но недействителен для feature1.projectA.dev.example.com. Поэтому, если мы хотим построить динамическое окружение с субдоменами, мы будем заложниками этого решения.
Итак, мы сформулировали задачу следующим образом:
💡 Задание №1
Для поддержки TLS в динамических средах проекта необходимо выпустить сертификат для ветки main каждого проекта и скопировать секрет, содержащий данные сертификата, во все остальные пространства имен этого проекта.
Для проекта с именем ‹project_name›, идентификатором ‹project_id› и именем ветки, равным ‹branch_name›, пространства имен будут иметь имена как следующее:
‹project_name›-‹project_id›-‹branch_name›-‹some_hash›
Некоторые примеры:
- проект-А-1120-основной-23ХФ
- проект-а-1120-dev-4hg5
- проект-а-1120-функция-1-aafd
- проект-б-1200-основной-7ds9
- проект-b-1200-feature1–42qq
То есть физически объект Сертификат создается только в project-a-1120-main-23hf и project-b-1200-main -7ds9 пространства имен. Результат выдачи сертификата — Secret, содержащий tls.crt и tls.key — необходимо скопировать во все остальные соответствующие пространства имен:
apiVersion: v1 kind: Secret type: kubernetes.io/tls metadata: name: ktsdev-wild-cert data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0F... tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkF...
Проблема единого сертификата
Теперь давайте рассмотрим вторую задачу, которую мы смогли охватить. Он достаточно близок к первому случаю, но имеет свои особенности.
Представьте, что мы говорим о какой-то производственной среде, и эта среда находится в кластере Kubernetes. Предположим, он состоит из нескольких кластеров, распределенных по географическим областям. И все эти кластеры обслуживают одно доменное имя, например, myshop.example.com. Мы действительно хотим, чтобы наш конечный пользователь видел один сертификат TLS, независимо от того, в какой кластер он попадает. Первоначально сертификат может исходить из различных источников:
- Вы можете купить сертификат.
- Сертификат можно выдать с помощью различных инструментов, таких как cert-manager в одном из кластеров. Затем мы должны решить, как доставить его на все производственные кластеры.
Естественно, имеет смысл использовать централизованное хранилище для хранения сертификата и использовать какой-либо инструмент для его доставки в нужные вам места использования.
До того, как мы внедрили наше обсуждаемое решение, у нас было прямое, но жизнеспособное решение: сертификат был развернут с помощью helm на все необходимые кластеры в составе инфраструктуры. Для обновления сертификата достаточно было обновить его в хелм-чарте и развернуть заново. Но мы хотели повысить автоматизацию и безопасность: например, нам не нравилось хранить сертификат в репозитории git.
Конечно, проблемы возникают и за пределами области сертификатов. Это могут быть любые данные, к которым вы хотите получить доступ в нескольких местах одновременно — учетные данные для реестра образов, логины/пароли баз данных и многое другое.
Итак, теперь мы сформулируем обе задачи, которые у нас были:
💡 Задание №1 (просто запомнить)
Для поддержки TLS в динамических средах проекта необходимо выпустить сертификат для ветки main каждого проекта и скопировать секрет, содержащий данные сертификата, во все остальные пространства имен этого проекта.
💡 Задание №2
Чтобы иметь возможность автоматически синхронизировать Secret из централизованного хранилища со всеми кластерами Kubernetes и выбранными в них пространствами имен.
Существующие решения и их недостатки
После того, как мы поняли, что именно мы хотим сделать, мы начали искать удовлетворительные решения. Два проекта, которые мы имели в виду для решения первой задачи:
- кубед от AppsCode
- kubernetes-отражатель от EmberStack
Мы сразу откладываем kubed, так как он зависит от меток, которые вам нужно добавить как в Secrets или ConfigMaps, так и от пространств имен, в которые вы их копируете. У нас не было возможности динамически добавлять метки при создании пространств имен. Узнайте подробнее о том, как работает kubed.
kubernetes-reflector работает аналогично, но может следовать за объектами Сertificate и добавлять дополнительные аннотации или метки, чтобы включить синхронизацию секретов.
Также допускается указывать регулярное выражение, которому должно соответствовать пространство имен, чтобы в него копировался секрет. Нет необходимости маркировать объекты пространства имен. Давайте посмотрим на пример:
apiVersion: v1 kind: Secret metadata: name: source-secret annotations: reflector.v1.k8s.emberstack.com/reflection-allowed: "true" reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "project-a-1120-*" data: ...
Здесь наиболее важной частью является аннотация reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces. С его помощью мы должны были легко решить первую задачу: развернуть Сертификат только для основной ветки, чтобы он автоматически копировался во все остальные ветки. Маска project-a-1120-* прекрасно описывает, в какие ветки нужно скопировать секрет.
Мы настроили все проекты и развернули это решение в нашем кластере разработки. В общем, kubernetes-reflector делал свое дело, пока разработчики не начали жаловаться, что иногда сертификаты не выдаются. На самом деле сертификат был выдан, но почему-то не скопировался в новую фиче-ветку.
На графиках использования ресурсов kubernetes-reflector мы увидели следующую картину (разные цвета обозначают разные поды):
Из-за высокого потребления памяти и многоцветности графика было ясно, что отражатель был убит OOM, из-за чего он каждый раз перезапускался. Утилизация сетевых ресурсов также была неожиданной и означала, что контроллер пытался загрузить много данных из kube-api.
К сожалению, по мере роста числа проектов выявились недостатки внутренней архитектуры рефлектора. На тот момент в кластере было около 10 тысяч разных Секретов. Учитывая все обстоятельства, отражатель наблюдал за каждым из них и неэффективно использовал детали пространства имен. Вот почему мы наблюдали интенсивное использование сетевого трафика на графиках.
В более поздних версиях Reflector проблемы с производительностью были решены, но это произошло через два месяца после того, как мы перешли на собственное решение.
Кроме того, нам очень хотелось, чтобы Secret появлялся в нужном namespace только через мгновения после его создания в кластере Kubernetes, а не после какого-то периода внутренней синхронизации.
В конце концов, мы решили как можно скорее разработать наш внутренний инструмент. Мы хотели, чтобы этот инструмент справлялся с нашими задачами и решал проблемы с производительностью, которые были у рефлектора. Мы справились за две недели: развернули его на нашем dev-кластере и перевели все команды на этот инструмент.
SecretMirror для спасения
Итак, вот требования к нашему новому контроллеру:
- Он должен работать со своим CRD. Он будет называться SecretMirror. Это требование резко снижает нагрузку на сервер API и ресурсы производительности контроллера: очевидно, что количество сущностей SecretMirror гораздо меньше, чем самих секретов.
- SecretMirror должен принимать список регулярных выражений пространств имен, в которые необходимо скопировать секрет. Это позволяет нам гибко управлять целевым расположением ресурсов.
- Контроллер должен следить не только за SecretMirror, но и за объектами Namespace. Это позволяет копировать секрет в новое пространство имен сразу после создания пространства имен, а не после произвольного периода синхронизации.
- Он должен поддерживать актуальный список пространств имен в кэше памяти. Поэтому мы экономим много трафика, не извлекая один и тот же список каждый раз, когда нам нужно синхронизировать секрет с пространствами имен. Этот кеш прост в поддержке благодаря выполнению пункта № 3, и мы можем динамически добавлять и удалять пространство имен в него или из него.
- При удалении SecretMirror все секреты, созданные контроллером, должны быть автоматически удалены. Однако нам нужна возможность отключить такое поведение.
- В случае изменения исходного секрета его синхронизация не требуется немедленно. В противном случае нам пришлось бы снова наблюдать за всеми секретными объектами в кластере. Достаточно хорошо, если синхронизация происходит один раз в определенный промежуток времени: например, с интервалом в 3 минуты. Этот интервал должен изменяться для каждого конкретного SecretMirror.
- Контроллер должен быть расширяемым. Поэтому внешние системы, такие как HashiCorp Vault, могут использоваться в качестве источника или назначения секрета.
Архитектура контроллера
Все довольно просто. Внутри зеркал запускаются два контроллера, которые следят за любыми изменениями двух ГВК — mirrors.kts.studio/v1alpha2.SecretMirror и v1.Пространство имен. Все изменения пространства имен сохраняются в памяти контроллера, чтобы свести к минимуму количество обращений к Kubernetes API.
Теперь вернемся к задаче №2: синхронизировать данные между несколькими кластерами. Здесь, в КТС, мы активно используем ХашиКорп Хранилище для хранения секретных данных, и здесь решили пойти по тому же пути.
Vault будет использоваться как система синхронизации состояний. Для этого нам нужно было научить SecretMirror читать и записывать секреты из/в Vault. В примерах использования ниже мы увидим, как это можно использовать.
Копирование секретов между пространствами имен
Для начала подробно рассмотрим первый сценарий, для которого изначально создавался SecretMirror. Манифест выглядит следующим образом:
apiVersion: mirrors.kts.studio/v1alpha2 kind: SecretMirror metadata: name: mysecret-mirror namespace: default spec: source: name: mysecret destination: namespaces: - project-a-.+ - project-b-.+
Его задачей является копирование секрета с именем mysecret из пространства имен по умолчанию во все пространства имен, начинающиеся с префикса project-a- или project-b-. Применим манифест и выведем список всех SecretMirrors:
$ kubectl apply -f mysecret-mirror.yaml $ kubectl get secretmirrors NAME SOURCE TYPE SOURCE NAME DESTINATION TYPE DELETE POLICY POLL PERIOD MIRROR STATUS LAST SYNC TIME AGE mysecret-mirror secret mysecret namespaces delete 180 Pending 1970-01-01T00:00:00Z 15s
SecretMirror находится в состоянии Pending — он ожидает появления исходного секрета. Давайте развернем его:
apiVersion: v1 kind: Secret metadata: name: mysecret namespace: default type: Opaque stringData: username: hellothere password: generalkenobi
Статус SecretMirror меняется с Pending на Active:
$ kubectl get secretmirrors NAME SOURCE TYPE SOURCE NAME DESTINATION TYPE DELETE POLICY POLL PERIOD MIRROR STATUS LAST SYNC TIME AGE mysecret-mirror secret mysecret namespaces delete 180 Active 2022-08-05T21:28:55Z 5m2s
Теперь давайте создадим пространства имен, в которые должен быть скопирован Secret:
$ kubectl create ns project-a-main $ kubectl create ns project-b-main
Секреты немедленно копируются в эти новые пространства имен:
$ kubectl get secret -A | grep "mysecret" NAMESPACE NAME TYPE DATA AGE default mysecret Opaque 2 6m23s project-a-main mysecret Opaque 2 23s project-b-main mysecret Opaque 2 23s
В kubectl description SecretMirror вы можете увидеть более подробную информацию, связанную с событиями объекта:
Name: mysecret-mirror Namespace: default Labels: <none> Annotations: <none> API Version: mirrors.kts.studio/v1alpha2 Kind: SecretMirror Metadata: Creation Timestamp: 2022-08-05T21:23:55Z Finalizers: mirrors.kts.studio/finalizer Generation: 2 Resource Version: 109673149 UID: 825ded22-0e90-4576-9608-1b63a1b02428 Spec: Delete Policy: delete Destination: Namespaces: project-a-.+ project-b-.+ Type: namespaces Poll Period Seconds: 180 Source: Name: mysecret Type: secret Status: Last Sync Time: 2022-08-05T21:38:41Z Mirror Status: Active Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning NoSecret 10m (x11 over 15m) mirrors.kts.studio secret default/mysecret not found, waiting to appear Normal Active 10m mirrors.kts.studio SecretMirror is synced
Копирование секретов из хранилища HashiCorp в кластер Kubernetes
Рассмотрим другой сценарий. Представьте, что в нашем кластере Vault есть секрет со следующим содержимым:
И наша цель — регулярно синхронизировать эти данные с Secret в кластере Kubernetes. Вот так выглядит манифест для SecretMirror, решающий эту задачу:
apiVersion: mirrors.kts.studio/v1alpha2 kind: SecretMirror metadata: name: myvaultsecret-mirror namespace: default spec: source: name: myvaultsecret-sync type: vault vault: addr: https://vault.example.com path: /secret/data/myvaultsecret auth: approle: secretRef: name: vault-approle destination: type: namespaces namespaces: - project-c-.+
Из-за этой конфигурации контроллер зеркал будет синхронизировать секрет хранилища с именем myvaultsecret с секретом Kubernetes с именем myvaultsecret-sync для пространства имен, имена которых начинаются с префикса project-c-. В настоящее время наша интеграция с Vault поддерживает 2 типа аутентификации:
- на основе токенов
- на основе AppRole
Подробнее о том, как настроить аутентификацию, вы можете узнать в README проекта.
Таким образом, задача №2 легко решается по описанному сценарию с использованием централизованного хранилища. В частности, мы можем разместить данные tls.crt и tls.key в Хранилище, настроить SecretMirror и получить возможность поддерживать сертификат в актуальном состоянии. в одном или нескольких кластерах в любое время, просто обновив сертификат в самом хранилище.
Копирование секретов из кластера Kubernetes в хранилище HashiCorp
Возвращаясь к одной из наших основных задач, можно вспомнить, что TLS-сертификат также может быть выдан cert-manager. Мы хотели бы синхронизировать его с другими кластерами нашей производственной среды. Здесь можно использовать ту же интеграцию с Vault. Только на этот раз мы будем синхронизировать секрет не изХранилища, а к немуиз Секрета Kubernetes.
Меньше слов, больше YAML:
apiVersion: mirrors.kts.studio/v1alpha2 kind: SecretMirror metadata: name: myvaultsecret-mirror-reverse namespace: default spec: source: name: mysecret destination: type: vault vault: addr: https://vault.example.com path: /secret/data/myvaultsecret auth: approle: secretRef: name: vault-approle
В этом случае должно быть понятно, что в качестве источника будет использоваться секрет mysecret, а в качестве назначения — секрет myvaultsecret в хранилище. Для синхронизации Secret с другими кластерами необходимо внутри них создать SecretMirror, как и в предыдущих сценариях, описывающих синхронизацию из Vault в Secret Kubernetes.
Бонус
Давайте рассмотрим несколько бонусных сценариев, в которых SecretMirror может помочь благодаря своему дизайну.
1. Распространение динамических секретов из Vault в Kubernetes Secret
HashiCorp Vault также известен тем, что может генерировать динамические учетные данные для доступа к поддерживаемым базам данных на лету. Например, он может сгенерировать временный пароль для доступа к кластеру PostgreSQL или MongoDB для некоторого сценария резервного копирования или другой процедуры cron. Статические логины/пароли могут утекать самыми разными способами: в логи, в мессенджер или могут храниться в незашифрованном виде на ПК разработчика. С помощью динамических секретов вы можете избежать этой проблемы, создав временный доступ и уничтожив его по истечении времени ожидания.
Ниже приведен пример того, как SecretMirror может помочь в синхронизации динамического пароля для MongoDB:
apiVersion: mirrors.kts.studio/v1alpha2 kind: SecretMirror metadata: name: secretmirror-from-vault-mongo-to-ns namespace: default spec: source: name: mongo-creds type: vault vault: addr: https://vault.example.com path: mongodb/creds/somedb auth: approle: secretRef: name: vault-approle destination: type: namespaces namespaces: - default
Обратите внимание, что в каждый момент синхронизации зеркала будут продлевать так называемую аренду, а не генерировать каждый раз новый пароль. Вот почему учетные данные останутся неизменными в течение периода max_ttl, установленного в хранилище.
2. Копирование секретов из Vault в Vault
Вы могли догадаться, что есть возможность указать также source.type = vault и destination.type = vault. Это действительно так, и секреты Kubernetes вообще не используются. Одним из возможных приложений является копирование определенного секрета из одного кластера Vault в другой или копирование ключа из одного места в другое в пределах одного кластера Vault.
Пример SecretMirror копирования между кластерами Vault:
apiVersion: mirrors.kts.studio/v1alpha2 kind: SecretMirror metadata: name: secretmirror-from-vault-to-vault namespace: default spec: source: name: mysecret type: vault vault: addr: https://vault1.example.com path: /secret/data/mysecret auth: approle: secretRef: name: vault1-approle destination: type: vault vault: addr: https://vault2.example.com path: /secret/data/mysecret auth: approle: secretRef: name: vault2-approle
Полученные результаты
Удалось ли нам решить основные задачи? Без сомнения.
Теперь все команды довольны — сертификаты появляются в фиче-ветках сразу, секреты между кластерами синхронизируются для нескольких DevOps-клиентов, ничего не делая вручную, и еще есть куда улучшать.
Наш контроллер потребляет очень мало ресурсов ЦП, памяти и сети и практически не имеет дополнительной нагрузки на кластер.
Диаграммы для сравнения с kubernetes-reflector:
Это был первый пользовательский контроллер Kubernetes, который мы разработали в команде, когда никакое другое решение не могло удовлетворить наши потребности.
Оказалось, что это довольно несложная процедура, позволяющая создавать множество пользовательских рабочих процессов внутри кластеров Kubernetes, но кроме того, она просто расширяет ваши знания о том, как работает Kubernetes внутри.
Если вы хотите сами попробовать зеркала, вот несколько полезных ссылок:
- https://github.com/ktsstudio/mirrors — основной репозиторий GitHub.
- Шлем-схема с инструкцией по установке.
- Терраформ-модуль, который устанавливает карту выше.
Спасибо за уделенное время и до скорой встречи :)