Запустить облачные микросервисы или 12-факторные приложения в Kubernetes относительно просто. Но как насчет запуска приложений, которые явно не предназначены для работы в контейнерной среде? Команда Kubernetes as a Service Mail.ru Cloud Solutions перевела статью об одном из самых мощных инструментов в Kubernetes — мультиконтейнерном поде. Он позволяет менять поведение приложения, не изменяя его кода. Эта функция удобна для приложений, которые изначально не предназначены для работы в контейнерах. Итак, посмотрим на примере. Elasticsearch разработали до того, как контейнеры стали популярны (хотя сейчас его можно запустить в Kubernetes). И его можно рассматривать в качестве замены для устаревшего Java-приложения, которое предназначено для работы на виртуальной машине. В этой статье я буду использовать Elasticsearch в качестве примера приложения, которое нужно улучшить с помощью мультиконтейнерных подов. Вот очень простой, но не для продакшена, деплой и сервис для Elasticsearch: Примечание: переменная окружения dicsovery.type нужна, чтобы запустить одиночную реплику. Elasticsearch слушает HTTP-порт 9200 по умолчанию. Вы можете убедиться, что под работает, запустив в кластере другой под и подключившись к службе elasticsearch: Теперь предположим, что вы хотите получить модель безопасности с нулевым доверием и зашифровать весь трафик в сети. Как поступить, если в приложении нет встроенной поддержки TLS? Примечание: последние версии Elasticsearch поддерживают TLS, раньше эта функция была платной. Первая мысль — выполнить терминацию TLS при помощи Nginx Ingress, поскольку Ingress маршрутизирует внешний трафик в кластере. Это не соответствует требованиям: трафик между подом входящего трафика и подом Elasticsearch может проходить по сети в незашифрованном виде. 
Внешний трафик проходит через Ingress и направляется к подам 
Если вы завершите TLS на входе, остальной трафик не будет зашифрован Решение, которое соответствует требованиям, — прикрепить прокси-контейнер Nginx к поду, доступному по TLS. Тогда трафик будет зашифрован на всем пути от пользователя к поду. 
Если вы включаете прокси-контейнер в модуль, то можете завершить TLS в модуле Nginx 
До контейнера Elasticsearch трафик идет в зашифрованном виде Вот как выглядит деплоймент: Давайте немного поясню: Таким образом, запросы извне пода поступают в Nginx через порт 9200 HTTPS, а затем попадают в Elasticsearch через порт 9201.  Вы можете проверить, что все работает, отправив HTTPS-запрос из кластера: Примечание: ключ -k нужен для самоподписанных сертификатов TLS. В продакшене необходимо использовать доверенный сертификат. Беглый взгляд на логи показывает, что запрос прошел через прокси Nginx: Вы также можете убедиться: нельзя подключиться к Elasticsearch через незашифрованные соединения. Поздравляю, вы включили TLS, не меняя кода Elasticsearch или образа в контейнере! Практика добавления прокси-контейнера в под распространена, поэтому у нее есть свое имя — шаблон Ambassador. Примечание: Все шаблоны в этом посте подробно описаны в отличной статье от Google. Добавление базовой поддержки TLS — только начало. Вот еще несколько вещей, которые можно сделать с шаблоном Ambassador: Давайте вернемся назад и посмотрим, в чем разница между подами и контейнерами в Kubernetes. Это поможет лучше понять происходящее под капотом. Традиционный контейнер, например запущенный при помощи Docker, обеспечивает несколько форм изоляции: Примечание: Docker позволяет настраивать и другие вещи, но эти — наиболее важные. Инструменты под капотом — пространства имен Linux и контрольные группы (cgroups). Контрольные группы — удобный способ ограничить ресурсы, которые может использовать конкретный процесс, например CPU или память. Допустим, вы указываете: процесс может использовать только 2 ГБ памяти и одно из четырех ядер CPU. В то же время пространства имен отвечают за изоляцию процесса и ограничивают то, что он может видеть. Например, процесс может видеть только сетевые пакеты, непосредственно с ним связанные. И не может видеть все сетевые пакеты, проходящие через сетевой адаптер. Другой пример — вы можете изолировать файловую систему и заставить процесс поверить, что у него есть доступ ко всей системе. 
Начиная с версии ядра 5.6, есть восемь видов пространств имен, и mount — одно из них 
Используя пространство имен mount, вы заставляете процесс поверить, что ему доступны все каталоги на хосте, хотя это не так 
Пространство имен mount предназначено для изоляции ресурсов, в данном случае — файловой системы 
Каждый процесс видит одну и ту же файловую систему, но при этом изолирован от других Примечание: если вам нужно освежить в памяти контрольные группы и пространства имен, то вот отличная статья, в которой рассматриваются некоторые технические детали. В Kubernetes контейнер обеспечивает все перечисленные формы изоляции, кроме изоляции сети. Изоляция сети происходит на уровне пода. Другими словами: у каждого контейнера в поде — своя файловая система, таблица процессов и так далее. Но все они совместно используют одно и то же сетевое пространство имен. Чтобы лучше понять, как все работает, давайте поиграемся с простым подом, состоящим из нескольких контейнеров: Немного разберем: Вы можете убедиться, что том смонтирован в первом контейнере, используя kubectl exec: Команда подключает терминальный сеанс к контейнеру c1 в поде podtest. Вы можете проверить прикрепленные тома с1 с помощью команды: Как видите, том смонтирован на /shared — общем томе, который мы создали ранее. Теперь создадим несколько файлов: Проверим те же файлы из второго контейнера. Сначала подключимся к нему с помощью команды: Как видите, файл, созданный в общем каталоге, доступен в обоих контейнерах, а файл в /tmp — нет. Так случилось, поскольку за исключением тома, файловые системы контейнеров полностью изолированы друг от друга. Теперь посмотрим на сеть и изоляцию процессов. Простой способ понять, как настроена сеть, — использовать команду ip link, она показывает сетевые устройства системы Linux. Выполним команду в первом контейнере: А теперь ту же команду в другом контейнере: Вы можете видеть, что в обоих контейнерах есть: Предполагается, что MAC-адреса глобально уникальны, значит, это явный признак того, что модули используют одно и то же устройство. Теперь давайте посмотрим на совместное использование сети в действии. Подключимся к первому контейнеру с помощью команды: Запустим очень простой сетевой листенер nc: Команда запускает листенер на локальном хосте на порту 5000 и показывает дату любому подключенному TCP-клиенту. Может ли к нему подключиться второй контейнер? Откроем терминал во втором контейнере с помощью команды: Посмотрите сами — второй контейнер может подключиться к сетевому порту, но не видит процесс nc: Подключившись через telnet, вы можете увидеть дату, это доказывает, что листенер nc работает. Но ps aux, показывающий все процессы в контейнере, вообще не отображает nc. Это связано с тем, что у контейнеров внутри модуля есть изоляция процессов, но нет изоляции сети. И это объясняет, как работает паттерн Ambassador: Контейнер, который получает внешний трафик, называют Ambassador — отсюда и название шаблона.  Примечание: поскольку сетевое пространство имен общее, то несколько контейнеров в поде не могут прослушивать один и тот же порт! Давайте посмотрим на другие варианты использования многоконтейнерных подов. Допустим, вы используете Prometheus для мониторинга всех сервисов в вашем кластере Kubernetes. Но есть приложения, которые изначально не экспортируют метрики Prometheus, например Elasticsearch. Можете ли вы добавить метрики Prometheus в свои поды, не меняя код приложения? Можете, если используете шаблон адаптера. В примере с Elasticsearch давайте добавим в под контейнер «экспортер», он предоставляет метрики Elasticsearch в формате Prometheus. Это легко сделать, поскольку для Elasticsearch есть экспортер с открытым исходным кодом (вам нужно добавить соответствующий порт в сервис). Как только вы примените этот манифест, метрики станут доступны на порту 9114. Еще раз — вы можете изменить поведение приложения, фактически не меняя код или образы контейнеров. В данном случае мы предоставили стандартизированные метрики Prometheus, которые используют кластерные инструменты (такие, как оператор Prometheus). И таким образом добились хорошего разделения между приложением и базовой инфраструктурой. Давайте посмотрим на паттерн Sidecar, с его помощью в под добавляют улучшающий приложение контейнер. Шаблон Sidecar — общий и применяется в различных вариантах использования. Так, вы можете услышать, что любые контейнеры в модуле, кроме первого, называют «sidecars». Сначала рассмотрим один из классических вариантов использования — log tailing sidecar. В контейнерной среде рекомендуют всегда писать в стандартный вывод, чтобы собирать и агрегировать журналы логов централизованно. Но многие старые приложения разработаны для логирования в файлы, и изменить это иногда нетривиальная задача. Благодаря log tailing sidecar вам, возможно, не придется ничего менять! Вернемся к примеру с Elasticsearch. Он немного надуманный, поскольку контейнер Elasticsearch по умолчанию ведет журнал в стандартном режиме, и нетривиально заставить его писать в файл. Вот как выглядит деплой: Примечание: Файл конфигурации ведения журнала логов — отдельный файл ConfigMap, он слишком большой, чтобы включать его сюда. У обоих контейнеров есть общий том с именем logs. Контейнер Elasticsearch записывает журналы на этот том, а контейнер журналов логов — читает из соответствующего файла и выводит его в стандартный формат. Вы можете получить поток журнала логов, указав нужный контейнер в kubectl logs: Самое прекрасное в Sidecar: потоковая передача в стандартный вывод — не единственный вариант. Если нужно переключиться на настраиваемую службу агрегации журналов логов, то можно просто изменить контейнер Sidecar, ничего не меняя в приложении. Есть множество вариантов использования Sidecars, контейнер логов — только один и довольно простой пример. Вот еще несколько вариантов, которые могут вам пригодиться: Во всех примерах многоконтейнерных подов, которые мы рассмотрели выше, несколько контейнеров работают одновременно. Еще в Kubernetes можно запускать контейнеры инициализации — контейнеры, которые выполняются и завершаются до запуска «обычных» контейнеров. Это позволяет запустить сценарий инициализации до того, как под запустится полностью. Почему иногда нужно выполнять инициализацию в отдельном контейнере, а не, допустим, добавлять инициализацию в сценарий точки входа вашего контейнера? Давайте посмотрим на примере Elasticsearch. В документации Elasticsearch рекомендуют устанавливать параметр sysctl vm.max_map_count в продакшен развертываниях. В контейнерных средах это проблематично — для sysctl нет изоляции на уровне контейнера, и любые изменения происходят на уровне узла. Что делать с этим, если вы не можете настраивать узлы Kubernetes? Один из способов — запустить Elasticsearch в привилегированном контейнере. Это позволяет Elasticsearch изменять системные настройки на своем хост-узле и сценарий точки входа для добавления sysctl. Но это чрезвычайно опасно с точки зрения безопасности! Если служба Elasticsearch будет скомпрометирована, то злоумышленник получил бы root-доступ к своему хост-узлу. Чтобы снизить этот риск, можно использовать контейнер инициализации: Под устанавливает sysctl в привилегированном контейнере инициализации, после чего контейнер Elasticsearch запускается, как и ожидалось. Вы по-прежнему используете привилегированный контейнер, что не идеально. Но он, по крайней мере, маленький и работает недолго, так что поверхность атаки намного ниже. Примечание: этот подход рекомендуют в Elastic Cloud Operator. Использование привилегированного контейнера инициализации для подготовки узла к запуску пода — распространенный шаблон. Например, Istio использует контейнеры инициализации для настройки правил iptables при каждом запуске пода. Еще одна причина использовать контейнер инициализации — подготовить файловую систему пода. Один из распространенных вариантов использования — управление секретами. Если для управления секретами вместо секретов Kubernetes вы используете что-то вроде HashicCorp Vault, то можете получить секреты в контейнере инициализации и сохранить их на общем томе emptyDir. Теперь секрет secret/my-secret доступен в файловой системе для контейнера myapp. Это основная идея того, как работают системы вроде инжектора Sidecar Vault Agent. Однако на практике они намного сложнее: комбинируют изменяющие веб-хуки, контейнеры инициализации и вспомогательные Sidecar. Вот еще несколько причин использовать контейнер инициализации: Мы рассмотрели много вариантов, так что вот таблица с шаблонами использования. В ней перечислены основные шаблоны и варианты их использования: Обязательно прочтите официальную документацию и исходный документ с дизайном контейнеров, если хотите разобраться в теме. Что еще почитать: Эмпатичный маркетолог