Ко всем новостям

Как управлять sidecar-контейнерами с помощью Platform V Synapse Service Mesh и Kubelatte

Публикации в СМИ
Технологии
01.03.2024

Источник: Хабр "Как управлять сотнями sidecar-контейнеров без боли и сожаления"

5357a6f564ff0603c30f14a5a6d1a6a1.jpg

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

Меня зовут Максим Чудновский, я развиваю Platform V Synapse Service Mesh в СберТехе. Продолжаю свой рассказ о паттернах использования дополнительных контейнеров в Kubernetes. В этот раз поговорим, как устроены Sidecar Injector'ы, как перейти к управлению кластером через политики и какие готовые решения для этого существуют.

Типичный Sidecar Injector

Технически это обычный HTTP-сервер, который обрабатывает admission request от Kubernetes API. Выглядит это так:

d051a6c05a5d3c1d0e1e1d85c338f075.jpg

В admission request приходит информация о спецификации того ресурса, который сейчас будет сохраняться. С ней можно сделать что-нибудь полезное и вернуть результат в виде admission response. Кроме того, в HTTP-сервере, помимо логики обработки, есть логика триггеринга: необходимо уметь определять ресурсы, требующие изменения, и задавать конкретный шаблон. Чаще всего это настраивается в виде ConfigMap, которая монтируется к injector. Конструкция простая, и в этом её основное достоинство. Также она Kubernetes native и используется повсеместно.

Но есть ряд проблем, которые можно назвать проблемами переиспользования. Представим, что у нас есть кластер с kube-api, на котором нет никаких webhook'ов. Мы хотим на основе этого кластера делать платформу и добавлять функциональность.

Например, мы разрабатываем Service Mesh и хотим, чтобы пользователи получали sidecar-контейнеры с сетевыми прокси автоматически, независимо от своего релизного цикла. Тогда мы добавляем webhook-configuration и разворачиваем Sidecar Injector:

ddc5d18aa46cbc8c0daf3e1f860e2748.jpg

Кажется, что всё хорошо и задача решена. Однако со временем выясняется, что один Service Mesh на один кластер — не очень живучая связка. Как только несколько команд сойдутся в одном Service Mesh, начнётся взаимное влияние, конфигурации будут противоречить друг другу и всё непременно сломается. Или потребует долгой и тщательной настройки.

Есть другое решение. В одном кластере можно иметь несколько разных Service Mesh, которые обслуживают свои пулы namespace. Теперь всё отлично, но при таком подходе нам нужно добавить ещё немножко webhooks и webhook-серверов, которые обслуживают уже разные service mesh.

Добавили — но что делать, если мы хотим развиваться и больше не намерены строить Service Mesh на sidecar'ах? Меняем архитектурный принцип: ставим daemon на воркере Kubernetes, чтобы он занимался всем сетевым процессингом. Тогда поды, которые запускаются на этом воркере, будут переиспользовать этот daemon.

Это удобно и сильно экономит ресурсы. Но в таком случае Sidecar Injector больше не нужен. Вместо него нужен injector, который будет добавлять init-контейнеры. Они должны проверять service mesh daemon на рабочем узле: перенаправлен ли трафика для пода, нет ли гонок в случае работы кластерного автомасштабирования, и т. д. Для нас это означает, что появляется ещё пачка webhook'ов и injector'ов, которые обслуживают новый тип Service Mesh.

acbceb289664f8335e636f7d20311acf.jpg

Казалось бы, на этом всё. Но у нас, как и у всех хороших разработчиков, есть план, по которому мы выпускаем фичи и новые версии продукта Service Mesh. А пользователи не всегда хотят или могут обновляться своевременно. Например, они могут пропустить достаточное количество версий, а за это время поменяется поколение Service Mesh и пропадёт обратная совместимость API. Соответственно, нам нужно поддерживать версионирование, чтобы была возможность бесшовно мигрировать с одной версии на другую. В результате мы снова добавляем webhook'и, чтобы обслуживать версионированные конфигурации Service Mesh.

414524ce01c9bce6c82595e03a204565.jpg

Но и на этом препятствия не заканчиваются. Появляется другая команда, которая занимается, например, внешними хранилищами секретов. И коллегам тоже нужен init-контейнер, который достанет все эти секреты и отдаст приложению при запуске пода. Вдобавок чаще всего потребуется ещё и sidecar-контейнер, который обеспечит ротацию секретов. В результате возникает та же самая изначальная проблема, что и у нас с Service Mesh. Поэтому добавляется ещё один комплект webhook'ов и webhook configuration.

37487dbce507179aa506a35c26aa2b36.jpg

Становится ещё сложнее, когда приходят ребята с журналированием и говорят: «Мы не хотим собирать логи в кластере через daemon на воркерах. Давайте решим это sidecar-контейнерами, чтобы пользователи могли их самостоятельно фильтровать и структурировать».

В итоге получается следующая картина:

bd7572b5bfabea1538f03e4ec77101d6.jpg

На эту страшную схему нельзя смотреть без боли и сожаления. У нас появляется кластер, в котором безумное количество webhook'ов. При этом сильно снижается доступность системы, потому что в обработке kube-api запросов участвует множество компонентов. А сам API, даже будучи супер быстрым и надёжным, может работать медленно, потому что каждый из этих webhook-серверов вносит свои задержки, может обладать побочными эффектами или просто быть перегруженным. Поэтому будет казаться, что кластер Kubernetes работает медленно, хотя он тут ни при чём.

В конце концов вся эта история заканчивается такой конфигурацией:

9011d3b7756ce62173aa0830b9b74cbb.jpg

Разработчики webhook-серверов знают, что webhook'и исполняются по очереди в алфавитном порядке. Рано или поздно кто-нибудь захочет, чтобы его webhook оказался первым. Тогда WebhookConfiguration получит имя, похожее на то, что в примере, с префиксом «aaa». А дальше, например, укажут failurePolicy: Fail, который заблокирует создание ресурса Kubernetes при недоступности webhook-сервера. Кроме того, будет добавлен достаточно широкий namespaceSelector, чтобы не добавлять свои метки к пространствам имён в кластере. В результате такой injector получит весьма серьёзную нагрузку и рано или поздно сломается, полностью заблокировав создание обслуживаемых ресурсов в кластере.

Для решения этой проблемы есть изящный способ.

Generic Sidecar Injector

Суть точно такая же: есть HTTP-сервер, который обрабатывает admission-запросы. Отличие в том, что теперь он может обрабатывать разные admission-запросы и подбирать нужные шаблоны. Это становится возможным благодаря тому, что логика срабатывания и шаблонов применяемых изменений выносится в параметры webhook-сервера.

7aaac542e082d654de17639516083b5c.jpg

На поде остаётся аннотация по применению шаблона. Таким образом, вместо множества webhook-серверов появляется один, который настраивается под каждый конкретный случай. При этом вся эта конструкция — stateless workload в Kubernetes, поэтому нам доступно лёгкое масштабирование стандартным автоскейлером. Но есть один аспект. Представим, что такой Generic Sidecar Injector доступен всем платформенным командам, и его можно использовать для внедрения своих контейнеров. В результате мы получаем широкий набор конфигураций и разных шаблонов. Часть этой конфигурации принадлежит команде журналирования (sidecars-контейнеры для логов), другая часть — конфигурации команды, которая отвечает за внешнее хранилище секретов. При всём этом они находятся наравне с конфигурациями sidecars Service Mesh. Получается, что теперь все команды живут в рамках одного компонента.

69733d3c7a37d62a07e6501d5626a09d.jpg

И тут начинаются проблемы. Становится непонятно, кто будет сопровождать это решение, потому что оно принадлежит идеологически разным командам. Но даже если мы найдём такого человека или группу людей, то всё равно возникнут трудности. Из-за взаимного влияния при изменении любого из шаблонов или триггеров чаще всего потребуется тестировать всю конфигурацию, которая может быть довольно разухабистой. В случае сложного релизного цикла это может стать большой проблемой. В качестве решения прямо просится некий API для пользователей инжектора, для того, чтобы не ходить к администратору и менять только кусочек общей конфигурации. Так как мы живём в k8s, то и API должен быть Kubernetes-Native: CRD+оператор. Так возникает концепция Mutation Manager.

Mutation Manager

54c2b202f6529062685aa00513bf4051.jpg

Здесь по-прежнему есть mutation webhook-сервер, который обрабатывает admission-запросы. Но теперь добавляются два CRD, которые можно использовать для настройки системы: template и trigger. Для того, чтобы это работало, мы встраиваем два контроллера k8s и оператор, который их объединяет.

В итоге:

  • Можно по-прежнему относительно легко масштабироваться.
  • Нет проблем со взаимным влиянием, потому что конфигурация разводится по пространствам имён Kubernetes, и у каждой команды есть свои экземпляры CRD.
  • Простота в использовании: не нужно менять общую конфигурацию, так как нужные шаблоны складываются прямо в дистрибутивы платформенных решений (например, Service Mesh) в виде CRD.

Но есть и минусы. Если раньше был простой HTTP-сервер, то здесь в конструкцию добавляется CRD, оператор и зарегистрированный API в Kubernetes, который обладает своим жизненным циклом, а значит будет меняться, что повлечёт миграции и т. д.

Сам API может выглядеть довольно просто:

9cdb36da59da9e57b0483cec110c0cf8.jpg

Есть Template, в котором определён шаблон изменений и указаны желаемые параметры. Например, мы хотим, чтобы у всех подов initialDelaySeconds на readinessProbe был равен 10 секундам для всех контейнеров, которые называются my-container.

Дальше есть триггер:

0027d77c81840df75c62ba46ec35b255.jpg

При создании подов они проверятся на предмет соответствия labelSelector. Если встречаем app: myApp, то будет выполнена мутация спецификации пода с применением шаблона, который называется my-inject-template и имеет updateStrategy: merge. Иными словами, меняется не весь контейнер в поде, а только его часть, которая зафиксирована в шаблоне. Таким образом, становится не важно, что укажет пользователь в своём Deployment, политика проб будет одинаковой для всех и начнёт задаваться централизованно.

Validation Manager

От мутации легко перейти к проверке. Например, мы хотим защитить кластер от YAML-инъекций, которые пользователь может принести через шаблоны мутации. Согласитесь, было бы неплохо получить итоговую спецификацию пода, который будет сохраняться в Kubernetes и проверять её. Для этого достаточно зарегистрировать validation webhook-сервер. Так мы расширим функциональность и внимательно проверим результат всех мутаций. При этом можно проверять не только свои действия, но и все другие пользовательские активности. Такую концепцию реализует, например, проект Gatekeeper.

40fcdc75fc5f952dc04c2d1c8d774402.jpg

Сами правила проверки могут выглядеть очень просто:

83d5d982afa77cfa27f3eacc691f03ba.jpg

Есть CRD с правилами, в которых прописано, что мы хотим видеть и что разрешаем. Таким образом, владелец кластера может проверить только эти правила и быть спокойным за действия пользователей в кластере. Но можно пойти ещё дальше: в генерацию.

Creation Manager

Рассмотрим типовой случай:

6dca2fb47783836d4d47379e877fc57e.jpg

Ещё один случай из жизни. У нас есть Service Mesh и egress gateway — сетевой шлюз, который чаще всего используется как единая точка выхода из кластера. Для того, чтобы перенаправить трафик своего приложения на этот шлюз, в Istio Service Mesh нужно сделать четыре YAML-файла: два VirtualService и два DestinationRule. При этом, согласно общему требованию использовать Egress, все пользователи Kubernetes должны подготовить эти маршруты.

И тут нехитрая математика: 100 приложений дают нам 400 практически одинаковых YAML-файлов, а 1000 приложений — уже 4000 файлов. Было бы гораздо удобнее повесить на развёртывание аннотацию, которая скажет: «Дорогой Kubernetes, пожалуйста, для этого приложения выведи трафик через egress gateway на такой-то внешний хост. Также возьми вот этот секрет, сделай TLS origination на egress и установи там mutual TLS connection». И на самом деле это простой в реализации сценарий в случае, если он срабатывает на эту аннотацию по мутации. Достаточно добавить побочный эффект и сгенерировать нужные ресурсы.

Но бывают ситуации, когда одной только аннотации недостаточно. Например, мы хотим создать набор конфигураций заранее. Тогда мы можем добавить простой якорный CRD, который будет триггером для генерации ресурсов. Вот как он выглядит:

aae8aca8c038421ee76ef4617be35cf7.jpg

У него пустая спецификация, есть только аннотация, которая ссылается на конкретные шаблоны. Достаточно добавить такой экземпляр CRD себе в дистрибутив, и все необходимые конфигурации будут сгенерированы на лету.

Что есть из готового?

Основной недостаток рассмотренной нами концепции в том, что её трудно реализовать: нужно повозиться, покодить и создать все webhook-серверы и контроллеры. А, как мы знаем, лучший код — тот, который мы вообще не пишем. И в open source уже есть готовые решения.

Kyverno from Nirmata

Схема достаточно проста:

07655c95feccfaee02b05f4f2d87e286.jpg

У Kyverno есть open source-часть и enterprise-сервис. Каждый выбирает сам. Но, как всегда, энтерпрайз-фичи сильно упрощают использование: есть мониторинг, библиотеки, дополнительные коннекторы.

Также с Kyverno есть одна сложность — он не везде доступен. Поэтому важно рассмотреть и другие решения.

KubeLatte

Это проект СберТеха, слоган которого звучит так: «Enjoy Kubernetes as a Cup of Latte». Отсюда и название :) Как и Kyverno, Kubelatte помогает перестать писать тонны YAML-файлов. Достаточно лишь добавить аннотацию, и вы автоматически получаете необходимый набор платформенной функциональности. Логическая схема KubeLatte выглядит так:

c15acfe187627a35d7b187bfb2853e02.jpg

Это ровно та концепция, эволюцию которой мы рассмотрели в статье. Kubelatte — это Generic Policy Manager, при это есть и Policy Market — набор готовых политик, которые могут использоваться в кластерах и решать совершенно разные задачи на любой вкус. Фактически, Policy Market — это квинтэссенция опыта эксплуатации больших и нагруженных кластеров K8s и таких платформенных решений, как Service Mesh.

Результаты

→ Практики использования многих контейнеров в рамках пода Kubernetes широко распространены.

Когда в поде много контейнеров — это нормально и даже полезно в некоторых сценариях.

→ Процесс управления дополнительными контейнерами следует автоматизировать.

Ни в коем случае не нужно добавлять sidecar'ы вручную. Это приведёт только к боли и сожалению.

→ Generic Policy Manager — удачный подход для реализации платформенной функциональности.

Если отталкиваться от sidecar, Generic Policy Manager — это крутое решение, которое позволяет перейти к управлению кластером через политики. Так можно полностью контролировать ситуацию, что сильно облегчает жизнь.

→ Policy Market — основная ценность готовых решений.

Как мы рассмотрели, концепт Generic Policy Manager комплексно решает проблемы с sidecar-контейнерами и успешно расширяет границы своей применимости. Но для успеха этого недостаточно — для того, чтобы управлять кластером через политики, как бы банально это не звучало, нужны политики. И если своего опыта не хватает, всегда можно обратиться к решениям, которые предлагают не только набор webhook-серверов и операторов, но и опыт жизни в Kubernetes в виде Policy Marketplace.