Pull to refresh
155.25
KTS
Создаем цифровые продукты для бизнеса

Зачем мы сделали собственный контроллер для копирования секретов в Kubernetes

Reading time 13 min
Views 6.7K

Меня зовут Игорь Латкин, я архитектор в компании KTS.

Сегодня хочу поделиться нашей внутренней разработкой — Kubernetes-контроллером mirrors. Мы создали его внутри нашего DevOps-отдела для копирования Kubernetes-секретов между неймспейсами кластера. В итоге mirrors превратился в универсальный инструмент синхронизации данных из разных источников.

В статье расскажу, с чего все начиналось и к чему мы в итоге пришли. Возможно, статья вдохновит вас на написание собственного контроллера под ваши задачи.

Что будет в статье:

С чего все началось

💡 Перед нами стояла единственная большая задача — обеспечение TLS в динамических окружениях в dev-контуре KTS.

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

Начнем с динамических окружений.

Динамические окружения с TLS

Задача копировать секреты между неймспейсами в Kubernetes кластере возникла в KTS уже давно. Наши процессы построены так, что каждая команда — даже каждый разработчик в компании — достаточно независим и самостоятелен.

Каждый может сам себе создать репозиторий в Gitlab, подключить общий CI/CD пайплайн, указав желаемые доменные имена в настройках проекта, и буквально за несколько минут получить задеплоенный проект для dev- и production-окружений. И необходимости привлекать для этого 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 в проекте превращается в развернутый пайплайн, покрывающий задачи сборки и раскладки проекта в dev- и prod-окружении:

Для dev-окружения практически все проекты раскладываются на одном и том же поддомене, в дальнейшем будем ссылаться на него как dev.example.com. То есть проекты могут быть разложены на такие поддомены:

  • projectA.dev.example.com

  • projectB.dev.example.com

  • projectC…

Также необходимо иметь в виду, что некоторые приложения состоят из нескольких микросервисов, которые объединены разными ingress-правилами. Например:

Так как они обслуживают один и тот же домен, для этих 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:

apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: ktsdev-wild-cert
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0F...
  tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkF...

Проблема единственного сертификата

Теперь рассмотрим вторую задачу, которую нам удалось покрыть. Она достаточно близка к первому случаю, но имеет свои особенности.

Представим, что мы говорим о неком prod-окружении, и это окружение расположено в Kubernetes-кластере. Допустим, оно состоит из нескольких кластеров, распределенных по разным географическим зонам. И все эти кластеры обслуживают один и тот же домен, например myshop.example.com. Мы очень хотим, чтобы наш конечный пользователь видел один и тот же сертификат, независимо от того, на какой из кластеров попадет. И тут уже могут быть варианты, откуда он изначально берется:

  1. Сертификат может быть куплен.

  2. Сертификат может быть выписан также через cert-manager в одном из кластеров. Тогда встает задача по его доставке в соседние кластеры.

Естественно предположить, что логично использовать некое централизованное хранилище и из него переливать сертификат в нужные места использования.

До того, как мы внедрили наше решение, задача решалась в лоб, но в целом жизнеспособно: сертификат накатывался как часть инфраструктуры через helm во все нужные кластеры. Чтобы обновить сертификат, достаточно было обновить его в helm-чарте и заново выкатить. Но хотелось больших автоматизации и безопасности: например, нам не нравилось хранить сертификат в git-репозитории.

Конечно, проблемы возникают не только с сертификатами. Это могут быть любые данные, которые хочется иметь сразу в нескольких местах — credentials для registry образов, логины/пароли от баз данных и многое другое.

Теперь мы сформулировали вторую задачу:

💡 Задача 2

Уметь автоматически синхронизировать Secret из централизованного хранилища во все нужные Kubernetes-кластеры и выбранные внутри них неймспейсы.

Существующие решения и их недостатки

Поняв, что именно хотим сделать, мы начали искать удовлетворительные решения.

Два проекта, которые мы рассматривали для решения первой задачи:

  1. kubed от AppsCode

  2. kubernetes-reflector от EmberStack

kubed нам не подошел сразу, т.к. он полагается на лейблы, которые нужно проставить как на сами Secret или ConfigMap, так и на неймспейсы, в которые их нужно скопировать. Ставить лейблы в динамике при создании неймспейса у нас не было возможности. Подробнее про работу с kubed расписано тут.

kubernetes-reflector работает похожим образом, но умеет сам следить за объектами Сertificate и прописывать дополнительные аннотации/лейблы, чтобы активировать синхронизацию сeкрета.

Также в нем была возможность указать регулярное выражение, под которое должен попасть неймспейс, чтобы в него скопирован был скопирован сeкрет. Необходимости навешивать лейблы на сами объекты неймспейса нет. Посмотрим на пример:

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. Благодаря ей мы должны были легко решать первую задачу: деплоили сертификат только для ветки main, во все остальные он автоматически скопирован, а куда именно, идеально описывает маска project-a-1120-*.

Мы настроили все проекты и задеплоили это решение в наш dev-кластер. В целом kubernetes-reflector хорошо справлялся со своей задачей, пока мы не начали получать жалобы от разработчиков по типу: «Сертификат не выписывается, помогите». На деле сертификат-то, конечно, был выписан, но почему-то не копировался в новый фича-бранч.

На графиках мы видели следующую картину (цвета обозначают разные поды):

График CPU. В целом приемлемые значения
График CPU. В целом приемлемые значения
График RAM. Какое-то невероятно большое потребление памяти для простого контроллера
График RAM. Какое-то невероятно большое потребление памяти для простого контроллера
График использования сети. Видно, что kubernetes-reflector выкачивал очень большие объемы данных
График использования сети. Видно, что kubernetes-reflector выкачивал очень большие объемы данных

Из-за высокого потребления памяти и того, что график разноцветный, уже понятно, что OOM просто убивал reflector, после чего он начинал работу заново. Потребление сетевых ресурсов также удивляло.

К сожалению, с ростом числа проектов недостатки внутренней архитектуры reflector давали о себе знать. На тот момент в кластере было около 10к различных Secret. Судя по всему, reflector следил за каждым из них и достаточно неэффективно использовал информацию о неймспейсе. Поэтому мы видели большое использование сетевого трафика на графиках.

В более новых версиях reflector были адресованы проблемы с перфомансом, но это было уже спустя 2 месяца нашего переезда на собственное решение

Еще нам очень хотелось, чтобы Secret в нужном неймспейсе появлялся сразу же после его создания в Kubernetes-кластере, а не спустя какой-то внутренний период синхронизации.

В конечном счете мы решили в кратчайшие сроки разработать собственный инструмент, который действовал бы согласно нашим задачам и учитывал проблемы с производительностью reflector. За 2 недели нам удалось это сделать, выкатить в наш dev-кластер и пересадить все команды на его использование.

SecretMirror for the rescue

Итак, какие требования предъявлялись к новому контроллеру:

  1. Должен работать со своим собственным CRD. Он будет назван SecretMirror. Это требование радикально снижает нагрузку на API-сервер и производительность контроллера: сущностей типа SecretMirror в кластере априори в разы меньше, чем самих Secret.

  2. В SecretMirror должен задаваться список регулярных выражений неймспейса, в которые указанный Secret должен быть скопирован. Это нам позволит гибко управлять целевым местоположением ресурсов.

  3. Контроллер должен следить не только за SecretMirror, но и за объектами неймспейса. Это позволит копировать Secret в новый неймспейс сразу после его появления, а не после периода синхронизации.

  4. Должен поддерживать актуальный список неймспейсов в кэше в памяти. Тогда мы экономим на походах за этим списком всякий раз, когда нужно синхронизировать Secret в указанные неймспейсы. Такой кэш как раз легко поддерживать благодаря выполнению пункта 3, и мы можем динамично добавлять в него неймспейс и удалять.

  5. При удалении SecretMirror все Secret, созданные контроллером, автоматически должны быть удалены. Однако необходимо оставить возможность отключить такое поведение.

  6. Синхронизация Secret не должна происходить моментально при его изменении. Иначе нам придется опять таки следить за всеми секретами в кластере. Достаточно, если синхронизация будет происходить раз в какой-то период: например, 3 минуты. При этом должна быть возможность изменить этот интервал для каждого отдельного SecretMirror.

  7. Контроллер должен быть расширяем. Тогда в качестве источника или назначения Secret можно использовать внешние системы, например Vault.

Архитектура контроллера

Все достаточно просто. Внутри mirrors запущены 2 контроллера, которые отслеживают любые изменения двух GVK — mirrors.kts.studio/v1alpha2.SecretMirror и v1.Namespace. Все изменения, касающиеся неймспейса, сохраняются в памяти контроллера, чтобы минимизировать походы в Kubernetes API.

Вспомним вторую задачу: синхронизировать данные между несколькими кластерами. Внутри KTS мы активно пользуемся HashiCorp Vault для хранения секретных данных, по этому же пути решили пойти и здесь.

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-.+

Его задача — копировать Secret с именем mysecret из неймспейса default во все неймспейсы, которые будут начинаться либо с project-a-, либо с project-b-. Применим манифест и выведем список всех SecretMirror:

$ 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

Задеплоим Secret, который ожидает SecretMirror:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  namespace: default
type: Opaque
stringData:
  username: hellothere
  password: generalkenobi

Статус SecretMirror при этом изменится c 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

В describe 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 Vault в 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-.+

Благодаря такой конфигурации контроллер mirrors будет синхронизировать Vault-секрет с именем myvaultsecretв Kubernetes Secret с именем myvaultsecret-sync в неймспейсах, имена которых начинаются с project-c-.

На данный момент наша интеграция с Vault поддерживает 2 вида аутентификации:

  1. Token

  2. AppRole

Подробнее про то, как настроить аутентификацию, можно прочитать в README проекта.

Описанный выше второй сценарий с легкостью решает задачу синхронизации секретных данных с использованием централизованного хранилища. В частности, в Vault мы можем положить данные tls.crt и tls.key, настроить SecretMirror и получить возможность всегда иметь актуальное состояние сертификата в одном или нескольких кластерах.

Копирование секрета из Kubernetes-кластера в HashiCorp Vault

схема сценария
схема сценария

Возвращаясь к одной из наших исходных задач, можно вспомнить, что условный TLS-сертификат может быть также выписан с помощью cert-manager. Хочется иметь возможность синхронизировать его с остальными кластерами нашего production-контура. Здесь можно воспользоваться той же интеграцией с Vault. Только на этот раз мы будем синхронизировать секрет не из Vault, а в него из Kuberentes Secret.

Меньше слов, больше 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

В данном случае видно, что в качестве source будет использован Secret mysecret, а в качестве назначения — Secret myvaultsecret в Vault.

Для синхронизации Secret в остальные кластеры в них нужно будет создать SecretMirror, как в предыдущем сценарии: для синхронизации из Vault в Secret Kubernetes.

Бонусы

Посмотрим на несколько «бонусных» сценариев, которые можно решить с помощью SecretMirror ввиду заложенной в него архитектуры.

1. Распространение динамических секретов из Vault в Kubernetes Secret

HashiCorp Vault известен также тем, что умеет «на лету» генерировать данные для входа в ту или иную поддерживаемую базу данных. Например, сгенерировать временный пароль для доступа к PostgreSQL или MongoDB для какого-нибудь скрипта, создающего бэкап БД. Статические логины/пароли могут утекать самыми разными способами: в логах, в мессенджере, просто храниться в открытом виде на компьютере разработчика. Динамические секреты позволяют избежать этой проблемы, создавая временные доступы и самостоятельно уничтожая их по истечении таймаута.

Так выглядит пример 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

Обратите внимание, что в каждый момент синхронизации mirrors будет продлевать так называемый lease, а не генерировать каждый раз новый пароль. Поэтому данные для входа будут одинаковые на протяжении всего периода max_ttl, задаваемого в Vault.

2. Копирование из Vault в Vault

Вы могли догадаться, что есть возможность указать source.type = vault и destination.type = vault. Это действительно так, и в данном случае Kubernetes Secret вообще не используются. Одно из возможных применений — копирование конкретного секрета из одного кластера Vault в другой, или копирование ключа из одного места в другое в рамках одного Vault.

Пример копирования между кластерами 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-клиентов синхронизируются без каких-либо ручных действий, и еще остается поле для улучшений и доработок.

Использование CPU, памяти и сети нашим контроллером находится на очень низком уровне и на кластер не оказывает практически никакой дополнительной нагрузки.

Графики для сравнения с kubernetes-reflector
График CPU
График CPU
График использования RAM
График использования RAM
График нагрузки на сеть
График нагрузки на сеть

Это был первый Kubernetes-контроллер, который мы разработали самостоятельно для своих нужд.

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

Если интересно попробовать mirrors у себя, вот несколько ссылок:

  1. https://github.com/ktsstudio/mirrors — GitHub репозиторий контроллера.

  2. Helm-чарт с инструкцией по установке.

  3. Terraform-модуль для установки чарта выше.


Другие статьи про DevOps для начинающих:

Другие статьи про DevOps для продолжающих:


За последние годы де-факто стандартом оркестрации и запуска приложений стал Kubernetes. Поэтому умение управлять Kubernetes-кластерами является особенно важным в работе любого современного DevOps-инженера. 

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

👉 Почитать про курс подробнее можно здесь: https://vk.cc/cn0ty6

Tags:
Hubs:
+19
Comments 11
Comments Comments 11

Articles

Information

Website
kts.tech
Registered
Founded
Employees
101–200 employees
Location
Россия