Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Всем известно, что pod в Kubernetes может включать в себя несколько контейнеров: для Service Mesh, работы с внешним хранилищем секретов, журналирования и т. д. В итоге это множество вызывает вопросы. Правильно ли использовать столько контейнеров? Как их изолировать от пользовательских приложений? Можно ли вообще исключить дополнительные контейнеры из пользовательских релизов?
Я Максим Чудновский, занимаюсь Synapse Service Mesh в СберТехе. Расскажу, какие есть паттерны применения дополнительных контейнеров в Kubernetes, как они могут помочь в платформенной инженерии, и, самое главное, как полностью автоматизировать процесс управления жизненным циклом таких контейнеров.
Поскольку тема контейнеров довольно объёмна, в этом материале коснусь того, какие виды дополнительных «полезных» контейнеров бывают и как добавлять их в Kubernetes так, чтобы развести релизные процессы прикладных и платформенных команд. А в следующей статье поговорим, как автоматизировать управление дополнительными контейнерами и управлять кластером через политики.
Можно ли использовать много контейнеров в одном поде
Для ответа на этот вопрос обратимся к замечательной книге «Kubernetes Patterns» от O'Reilly.
В части, посвящённой структурным паттернам, описано, как организовать своё приложение в Kubernetes с точки зрения структуры пода в Kubernetes: что там должно быть, а от чего лучше отказаться.
В общем виде можно выделить 4 вида дополнительных «полезных» контейнеров:
Init;
Sidecar;
Adapter;
Ambassador.
Давайте рассмотрим их подробнее.
Init Containers
Как видно из названия, init-контейнеры нужны для того, чтобы инициализировать приложение. В отличие от обычных, они имеют собственный жизненный цикл: всегда выполняются первыми, а основные контейнеры стартуют только по их завершению. Это удобно, когда необходимо что-то подготовить, например, можно получить доступ к каким-то секретам или сделать редирект сетевого трафика через модификацию iptables. А после этого запустить само приложение и забыть про эти вещи.
Sidecar Containers
В общем случае sidecar-контейнер — это контейнер с законченной функциональностью, которая нужна приложению, но не является частью его бизнес-логики. За счёт такого разделения разработчики могут фокусироваться на одной задаче. Платформенная команда отвечает за дополнительные возможности, например, делает приложение более отказоустойчивым, надёжным. В свою очередь прикладные разработчики занимаются исключительно бизнес-логикой приложения.
Sidecar-контейнеры чаще всего используются для добавления платформенной функциональности, например:
Service Mesh — в этом случае добавляется сетевой прокси, который обрабатывает все запросы, добавляет observability, делает mutual TLS и остальные полезные вещи.
Журналирование, если вам по каким-то причинам не нравится решение собирать логи через daemon, который бежит на воркере Kubernetes и забирает данные сразу с контейнерного runtime'а.
Централизованный аудит.
Adapter Containers
Адаптеры — такие же sidecar’ы, но узкоспециализированные. Они используются тогда, когда в приложение нужно добавить новый API, но не хочется (или не получается) сделать это на уровне приложения.
Вот классические примеры:
Metrics API. Есть система мониторинга на базе Prometheus и приложение, которое про эту систему мониторинга вообще ничего не знает. У этого приложения есть либо только свои метрики, либо нет вообще нет никаких. Для «неинвазивного» решения проблемы достаточно добавить Prometheus экспортер отдельным контейнером, который опубликует все нужные метрики в правильном формате.
Custom API. Позволяет добавить произвольный API по аналогии с примером метрик.
RBAC Proxy. Дополнительный контейнер становится точкой входа в приложение и добавляет стандартный RBAC Kubernetes. Это делается очень быстро и во многих случаях бывает полезно.
Ambassador Containers
Эти контейнеры похожи на адаптеры, но работают в обратную сторону: они инкапсулируют в себя всю сложность внешнего API и позволяют использовать его понятным для приложения способом.
Распространённые примеры использования:
Kubernetes API Access. Допустим, наше приложение умеет работать только с файлами и может читать оттуда свои конфигурации. При этом количество файлов не ограничено, и состав часто меняется — пользователи добавляют новые и удаляют неактуальные. Стандартная история с ConfigMaps тут не слишком удобная.
Придётся либо постоянно менять спецификацию приложения и добавлять новые каталоги монтирования, либо все файлы будут в одной ConfigMap, и её крайне сложно редактировать в многопользовательском режиме. Для решения этой проблемы можно научить приложение самостоятельно ходить в Kubernetes API и доставать нужную информацию. Но ещё проще поставить рядом Ambassador sidecar, который всё это сделает и за нас, и за приложение.
Distributed or Local Cache Access. Можно унифицировать доступ к сложным распределённым кешам со стороны приложения.
Data Access within a Data Mesh. Можно построить витрину данных, которая доступна непосредственно приложению.
Как добавлять контейнеры?
Самый простой способ — использовать обычные Kubernetes-манифесты. Достаточно добавить нужные контейнеры в спецификацию деплоймента (template spec). Но этот способ приводит к огромному количеству boilerplate yaml-файлов.
Кажется, что решением могут стать различные template-менеджеры (Helm, Kustomize, CUE и т. д.). Они дадут какое-никакое управление зависимостями: платформенные команды смогут публиковать свои артефакты, на которые уже прикладные разработчики будут ссылаться и использовать. В результате количество yaml-кода сильно сократится.
Но есть нюанс: template-менеджеры в конечном итоге развернут все в тот самый Raw Kubernetes Resource, который будет использован в кластере.
В целом это рабочий подход. Но есть нюанс: у платформенных и прикладных разработчиков разный релизный цикл. И платформенным командам нужно публиковать свои sidecars независимо, без запроса на обновление бизнес-приложения, а сделать это будет невозможно.
Для enterprise-решений это довольно серьезная проблема, поэтому важно разводить эти два релизных процесса. Хорошо, что Kubernetes позволяет это сделать.
Automated Approach — k8s admission
Когда вы создаёте объект внутри Kubernetes (например, делаете kubectl apply -f myfile.yaml
), он проходит предобработку из нескольких этапов, которые реализуются с помощью специализированных admission контроллеров.
Они встроены в kube-api, включаются определёнными флагами и делают много полезного. Например, есть admission-контроллер Service Account, который монтирует токен Service Account в под, чтобы вы могли ходить в kube-api. Но нам интересен другой — mutation admission.
Когда Kubernetes видит новый запрос, он вызывает этот контроллер (как и все остальные), чтобы обработать ресурс. Суть в том, что mutating admission— кастомизируемый. Таким образом мы получаем инструмент расширения admission-контроллеров, чтобы создавать свою кастомную логику, которая отвечает за изменение («мутацию») создаваемых ресурсов «на лету».
Это очень удобно и Kubernetes-native. Мы создаем Mutating Admission Configuration, регистрируем webhook и выполняем нужные мутации. Например, автоматически добавляем sidecar-контейнеры при старте пода.
Решения, которые используют данный подход, называются Sidecar Injectors. В следующем материале подробно расскажу, как они устроены, а заодно поделюсь опытом автоматизации управления sidecars и готовыми open source решениями, которые позволяют комплексно решать такие задачи.