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

Как (и зачем) мы разворачивали ActiveMQ Artemis в облаке

Публикации в СМИ
09.10.2024

Источник: хабр

Привет! Меня зовут Артем Безруков, я DevOps‑инженер в команде интеграционных сервисов Platform V Synapse в СберТехе.

Наша команда работает над продуктом из линейки Platform V Synapse — Platform V Synapse Messaging. Это брокер сообщений, в основе которого лежит Apache ActiveMQ Artemis. Мы делаем из него более безопасное и функционально обогащённое решение, разрабатывая дополнительные плагины, и заботимся о том, чтобы его можно было просто и быстро развернуть с помощью наших скриптов автоматизации.

В последние годы набирает обороты тренд на использование облачных технологий, технологий контейнеризации и микросервисной архитектуры, и наша команда решила расширить возможности продукта. И если изначально стенды ограничивались только виртуальными машинами (ВМ), то с недавнего времени мы начали выводить Platform V Synapse Messaging в среды оркестрации контейнеров — Kubernetes (K8s/облако).

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

Поехали!

Почему ActiveMQ Artemis?

Мы выбрали Artemis как open‑source замену IBM MQ. Оба решения выполняют функцию брокера сообщений и поддерживают модель работы point‑to‑point с отправкой и вычиткой сообщений из очередей.

Artemis работает с протоколами Core (Artemis native), OpenWire, AMQP, MQTT, STOMP. Его можно использовать как отдельно, так и в кластере. С остальными особенностями можно ознакомиться в официальной документации. Плюс у нашей команды большой опыт разработки на Java, что позволяет нам дорабатывать продукт, добавляя к нему различную функциональность:

  • формирование событий аудита — для упрощения разбора инцидентов;
  • трассировка сообщений — для прослеживания всего их пути внутри кластера;
  • сбор метрик подключения и сессий — для мониторинга и администрирования кластера;
  • ограничение подключений и скорости отправки сообщений — для регулирования нагрузки на брокера;
  • работа с бэкапами — для восстановления работы кластера в случае возникновения инцидентов;
  • проверка DN‑сертификата у подключённого клиента или сервера — для управления доступами клиентов к кластеру;
  • работа с хранилищем секретов (vault) — для использования внешнего хранилища секретов (паролей и сертификатов);
  • шифрование сообщений, пока они находятся в брокере — для безопасного хранения данных;
  • клиентские перехватчики — для контроля целостности данных при записи и вычитке сообщений.

Подготовка к развёртыванию Artemis в Kubernetes

Пока команда разработки трудится над улучшением Synapse Messaging, внедряя новую функциональность, мы, команда DevOps, решаем задачи по его установке, расширяя возможности и повышая удобство развёртывания. Чтобы не ограничиваться только развёртыванием на ВМ, где всё работает в целом стабильно и бесхитростно, мы рассмотрели опции упаковки приложения в контейнер и его запуск в K8s. Это позволило бы исследовать потенциал быстрого масштабирования, отказоустойчивости, альтернативного подхода к конфигурированию и других особенностей, учитывая при этом вероятные просадки в производительности.

С докеризацией приложения проблем не возникло, учитывая, что в самом репозитории Apache ActiveMQ Artemis разработчики несут несколько Dockerfile с пояснениями. Мы, по сути, использовали тот же подход: перенесли файлы приложения, объявили переменные окружения, создали необходимые директории и пользователей, раскидав права. Также мы немного поменяли скрипт запуска приложения, добавив ожидание статусов сайдкаров Istio и Vault — о них расскажем дальше. Скрипт опрашивает конечную точку Istio‑сайдкара и ожидает в файловой системе наличие файлов с секретами, которые генерируются Vault‑сайдкаром.

# Check Istio and Vault sidecars before launching Artemis if [ "x$ISTIO_ENABLED" = "xtrue" ]; then echo "Checking for Istio Sidecar readiness..." until curl -fsI http://localhost:15020/healthz/ready; do echo "Waiting for Istio Sidecar, sleep for 3 seconds"; sleep 3; done; echo "Istio Sidecar is ready." fi if [ "x$VAULT_ENABLED" = "xtrue" ]; then config_file="$APP_HOME"/etc/waitVault.txt if [ ! -f "$config_file" ]; then echo "Vault wait file $config_file not found, skipping Vault check." else echo "Checking for Vault Sidecar readiness..." checked_files=$(cat "$config_file") files_count=0 for file in $checked_files; do files_count=$(( files_count + 1 )) done exists_files_count=0 time_counter=0 while [ $exists_files_count != $files_count ]; do exists_files_count=0 for file in $checked_files; do if [ -f "$file" ]; then exists_files_count=$(( exists_files_count + 1 )) fi done sleep 1 time_counter=$(( time_counter + 1 )) echo "Waiting Vault Sidecar $time_counter s." done echo "Vault Sidecar is ready." fi fi

В шаблон генерации configmap в Helm‑чарты, про которые расскажем ниже, также добавили создание файла waitVault.txt со списком секретов, который используется в скрипте:

waitVault.txt: |- {{- range $key, $value := .Values.annotations }} {{- if hasPrefix "vault.hashicorp.com/secret-volume-path" $key }} {{ $value }}/{{ $key | trimPrefix "vault.hashicorp.com/secret-volume-path-" }} {{- end }} {{- end }}

Проверяем работу локально, убеждаемся, что всё запускается и, главное, не падает. Довольные пушим в репозиторий и идём пить кофе.

На обычном образе мы не остановились — мы следим за рекомендациями и best practices в нашей сфере, поэтому следующей итерацией была разработка distroless‑образа. Distroless — это тип образов, которые не содержат в себе дистрибутив (Alpine, Debian …), а имеют только всё необходимое для запуска приложения, в нашем случае Java. Это делает их более легковесными и менее уязвимыми ввиду уменьшения области атак.

Здесь подход тоже достаточно тривиальный — из builder‑образа Debian взяли необходимые утилиты, локали, библиотеки и перенесли в Distroless‑образ с Java 11. И такой получившийся образ использовали в качестве базового при сборке самого образа приложения.

# Start from a Debian-based image to install packages FROM debian:bullseye-slim as builder # Install the required packages RUN apt-get update && apt-get install -y \ bash \ coreutils \ curl \ locales \ locales-all # Start from the distroless java 11 image FROM gcr.io/distroless/java:11 # Copy the required libraries COPY --from=builder /lib/x86_64-linux-gnu/libtinfo.so.6 \ /lib/x86_64-linux-gnu/libselinux.so.1 \ /lib/x86_64-linux-gnu/libpthread.so.0 \ /lib/x86_64-linux-gnu/libdl.so.2 \ /lib/x86_64-linux-gnu/libc.so.6 \ /lib/x86_64-linux-gnu/libaudit.so.1 \ /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \ /lib/x86_64-linux-gnu/libcap-ng.so.0 \ /lib/x86_64-linux-gnu/libdl.so.2 \ /lib/x86_64-linux-gnu/libsepol.so.1 \ /lib/x86_64-linux-gnu/libbz2.so.1.0 \ /lib/x86_64-linux-gnu/ COPY --from=builder /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 \ /usr/lib/x86_64-linux-gnu/libacl.so.1 \ /usr/lib/x86_64-linux-gnu/libattr.so.1 \ /usr/lib/x86_64-linux-gnu/libsemanage.so.1 \ /usr/lib/x86_64-linux-gnu/ COPY --from=builder /usr/lib/locale/ /usr/lib/locale/ COPY --from=builder /usr/share/locale/ /usr/share/locale/ # Copy the shell and utilities COPY --from=builder /bin/bash \ /bin/cat \ /bin/chown \ /bin/chmod \ /bin/mkdir \ /bin/sleep \ /bin/ln \ /bin/uname \ /bin/ls \ /bin/ COPY --from=builder /usr/bin/curl \ /usr/bin/env \ /usr/bin/basename \ /usr/bin/dirname \ /usr/bin/locale \ /usr/bin/ COPY --from=builder /usr/sbin/groupadd \ /usr/sbin/useradd \ /usr/sbin/ # Change shell to Bash SHELL ["/bin/bash", "-c"] # Create link sh -> bash RUN ln -s /bin/bash /bin/sh

Переходим к развёртыванию. Далеко ходить не пришлось — ArtemisCloud.io предоставляет K8s‑оператор для развёртывания приложения в облаке. В комплекте идёт оператор, CRD с описанием сущностей брокера, манифесты с ролями, вспомогательные скрипты и инструкция (как это часто бывает, отвечающая не на все вопросы).

Перед установкой оператора в наш namespace надо занести CRD, создать ServiceAccount, Role, RoleBinding, ElectionRole, ElectionRoleBinding. Затем уже можно развёртывать и сам оператор. Набор из custom resource definition покрывает основные сущности Artemis:

  • Broker CRD — создание и конфигурирование развёртывания брокера;
  • Address CRD — создание адресов и очередей;
  • Scaledown CRD — создание контроллера миграции сообщений при уменьшении размера кластера;
  • Security CRD — настройка безопасности и методов аутентификации для брокера.

Солидный комплект! Но тут начинают возникать вопросы:

  • А как нам управлять нашими плагинами и интеграциями?
  • Как теперь конфигурировать кластер? Не через изменение XML‑файлов через Ansible, как привыкли? Переписывать все в YAML под CRD?
  • Как разделять доступ к управлению кластером и управлению очередями?
  • Как дописать необходимую функциональность без большого опыта в Go‑разработке?
  • А что на это скажет безопасность, с которой приложение на ВМ полностью согласовано, а про оператор она ничего не знает?
  • и так далее.

С одной стороны, у нас есть готовый оператор, который надо подробно изучить, понять, как его можно подкрутить под наши нужды, и использовать. С другой — наши Ansible‑плейбуки для работы с ВМ, которые не так долго адаптировать под развёртывание в облаке, и привычные XML‑конфиги.

Недолго думая, мы решили, что не будем использовать оператор, но станем разрабатывать Helm‑манифесты и доделывать наши плейбуки. И тут начинается самое интересное.

Подготовка Helm-чартов

Архитектура, к которой мы стремились прийти, выглядит следующим образом:

d9eb8fdfdf29495ed92f34444a150f7f.png

В Kubernetes namespace разворачивается приложение с несколькими репликами. Кластер находится за единым сервисом. Помимо кластера Artemis в namespace ещё разворачиваются два шлюза (Istio envoy) — ingress и egress, через которые проводится трафик для журналирования. Поды приложения и шлюзов настраиваются на работу с сайдкарами Vault‑agent и Istio‑proxy. Внутри Kubernetes namespace настраивается маршрутизация трафика и mTLS посредством DestinationRule (DR), VirtualService (VS), PeerAuthentication (PA), ServiceEntry (SE) манифестов Istio. Начнём с самого приложения, а затем перейдём к «обвязке».

Мы используем Helm‑чарты для развёртывания наших приложений в Kubernetes и управления ими. Helm‑чарт состоит из шаблонов‑манифестов и значений‑переменных (values.yaml), которые подставляются в шаблоны. В отличие от отдельных манифестов различных объектов, которые разворачиваются по одному готовому файлу через kubectl, чарты устанавливаются «набором» или «релизом». Релиз можно обновлять или откатывать, а при удалении ресурсы из Kubernetes также удаляются все сразу.

Для приложения написали манифест, поднимающий statefulset. Statefulset подходит нам потому, что его поды имеют предсказуемые названия, сохраняют идентичность при перезапуске и при изменении топологии кластера поднимаются, удаляются или перезапускаются одна за одной, позволяя сообщениям перетекать из брокера в брокер. Также необходимы манифесты для сервисов — service для доступа к подам, headless service для обнаружения подов в кластере брокеров.

apiVersion: v1 kind: Service metadata: name: artemis-svc namespace: my_namespace spec: ports: - name: console port: 8161 protocol: TCP targetPort: 8161 - name: data port: 61616 protocol: TCP targetPort: 61616 - name: jgroups-7800 port: 7800 protocol: TCP targetPort: 7800 - name: jgroups-7900 port: 7900 protocol: TCP targetPort: 7900 publishNotReadyAddresses: true selector: app: artemis-app type: ClusterIP

В сервисах объявляем порты:

  • console — для доступа к UI‑интерфейсу;
  • data — для TCP‑подключения к акцепторам приложения;
  • prometheus — для сбора метрик;
  • jgroups — для межкластерного общения.

Так как у нас уже были роли и плейбуки Ansible для развёртывания Artemis на ВМ, то для большинства конфигурационных файлов требовалось сделать перевод из Jinja2-формата в Helm template, и дописать шаблоны для недостающих файлов. В итоге у нас получился следующий список файлов с конфигурациями, которые мы монтируем через configmap в /app/broker/etc:

etc/ |-- _address-settings.tpl |-- _addresses.tpl |-- _artemis_profile.tpl |-- _audit_metamodel.tpl |-- _audit_properties.tpl |-- _bootstrap.tpl |-- _broker.tpl |-- _cert_roles.tpl |-- _cert_users.tpl |-- _jgroups-ping.tpl |-- _jolokia-access.tpl |-- _keycloak.tpl |-- _logback.tpl |-- _login.tpl |-- _management.tpl |-- _plugins_configs.tpl |-- _resource-limit-settings.tpl |-- _security-settings.tpl `-- _vault.tpl

Кластеризация через Jgroups

Важной частью конфигураций является настройка кластеризации. На ВМ ноды брокера мы объединяли в кластер, объявляя статичные коннекторы в разделе <cluster-connections> в broker.xml:

<connectors> <!-- Connector used to be announced through cluster connections and notifications --> <connector name="artemis">tcp://10.20.30.40:61616?sslEnabled=true;enabledProtocols=TLSv1.2,TLSv1.3</connector> <connector name="node0">tcp://10.20.30.41:61616?sslEnabled=true;enabledProtocols=TLSv1.2,TLSv1.3</connector> </connectors> <cluster-connections> <cluster-connection name="my-cluster"> <reconnect-attempts>-1</reconnect-attempts> <connector-ref>artemis</connector-ref> <message-load-balancing>ON_DEMAND</message-load-balancing> <max-hops>1</max-hops> <static-connectors allow-direct-connections-only="false"> <connector-ref>node0</connector-ref> </static-connectors> </cluster-connection> </cluster-connections> <ha-policy> <live-only> <scale-down> <connectors> <connector-ref>node0</connector-ref> </connectors> </scale-down> </live-only> </ha-policy>

В облаке же объявлять кластер таким образом было бы неудобно. Поэтому мы настроили механизм Jgroups, доступный в Artemis «из коробки». Jgroups — стек протоколов, позволяющий реализовывать кластеризацию для Java‑приложений. Настройки брокера стали выглядеть так:

<connectors> <!-- Connector used to be announced through cluster connections and notifications --> <connector name="cluster">tcp://${POD_IP}:61617?sslEnabled=false;enabledProtocols=TLSv1.2,TLSv1.3</connector> <connector name="artemis">tcp://${POD_IP}:61616?sslEnabled=true;enabledProtocols=TLSv1.2,TLSv1.3</connector> </connectors> <acceptors> <acceptor name="cluster">tcp://0.0.0.0:61617?protocols=CORE,AMQP,MQTT,STOMP;amqpCredits=1000;amqpDuplicateDetection=true;amqpLowCredits=300;amqpMinLargeMessageSize=102400;supportAdvisory=false;suppressInternalManagementObjects=false;tcpReceiveBufferSize=1048576;tcpSendBufferSize=1048576;useEpoll=true;sslEnabled=false</acceptor> <acceptor name="artemis">tcp://0.0.0.0:61616?protocols=CORE,AMQP,MQTT,STOMP;amqpCredits=1000;amqpDuplicateDetection=true;amqpLowCredits=300;amqpMinLargeMessageSize=102400;supportAdvisory=false;suppressInternalManagementObjects=false;tcpReceiveBufferSize=1048576;tcpSendBufferSize=1048576;useEpoll=true;sslEnabled=true;enabledProtocols=TLSv1.2,TLSv1.3;keyStorePath=/app/artemis/broker/vault/crt.pem;keyStoreType=PEM;trustStorePath=/app/artemis/broker/vault/ca.pem;trustStoreType=PEM;verifyHost=false;needClientAuth=true</acceptor> </acceptors> <broadcast-groups> <broadcast-group name="my-broadcast-group"> <jgroups-file>jgroups-ping.xml</jgroups-file> <jgroups-channel>activemq_broadcast_channel</jgroups-channel> <connector-ref>cluster</connector-ref> </broadcast-group> </broadcast-groups> <discovery-groups> <discovery-group name="my-discovery-group"> <jgroups-file>jgroups-ping.xml</jgroups-file> <jgroups-channel>activemq_broadcast_channel</jgroups-channel> <refresh-timeout>10000</refresh-timeout> </discovery-group> </discovery-groups> <cluster-connections> <cluster-connection name="my-cluster"> <discovery-group-ref discovery-group-name="my-discovery-group"/> <connector-ref>cluster</connector-ref> <max-hops>1</max-hops> <message-load-balancing>ON_DEMAND</message-load-balancing> <reconnect-attempts>-1</reconnect-attempts> </cluster-connection> </cluster-connections> <ha-policy> <live-only> <scale-down> <discovery-group-ref discovery-group-name="my-discovery-group"/> </scale-down> </live-only> </ha-policy>

Каждый брокер теперь имел отдельный акцептор и коннектор, предназначенный для общения между нодами кластера. Объявили broadcast- и discovery-группы для работы Jgroups, которые указаны в cluster-connections и ha-policy. Сам же Jgroups-стек описали в файле jgroups-ping.xml:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:org:jgroups" xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd" > <TCP bind_addr="127.0.0.1" bind_port="7800" external_addr="${POD_IP}" external_port="7800" port_range="0" thread_pool.min_threads="0" thread_pool.max_threads="200" thread_pool.keep_alive_time="30000"/> <dns.DNS_PING dns_query="${DNS_QUERY}" dns_record_type="${DNS_RECORD_TYPE:A}" /> <MERGE3 min_interval="10000" max_interval="30000"/> <FD_SOCK2 port_range="0" /> <FD_ALL3 timeout="40000" interval="5000" /> <VERIFY_SUSPECT2 timeout="1500" /> <pbcast.NAKACK2 use_mcast_xmit="false" /> <pbcast.STABLE desired_avg_gossip="50000" max_bytes="4M"/> <pbcast.GMS print_local_addr="true" join_timeout="2000" max_join_attempts="2" print_physical_addrs="true" print_view_details="true"/> <UFC max_credits="2M" min_threshold="0.4"/> <MFC max_credits="2M" min_threshold="0.4"/> <FRAG2 frag_size="60K" /> </config>

Мы не будем подробно описывать каждый протокол с его особенностями, это можно найти в документации Jgroups. Остановимся на основных моментах, которые используются в этом проекте.

Нас интересует блок TCP — в нём мы объявляем адреса и порты, на которых будет работать Jgroups. Сам приклад работает на 127.0.0.1 внутри пода и стандартном Jgroups‑порте 7800. Также необходимо указать «внешний» адрес — адрес пода, в котором размещено наше приложение, порт при этом остаётся неизменным.

Ранее в манифесте сервиса мы объявляли, что для Jgroups необходимы два порта: 7800 и 7900, но в конфигурации об этом не написано. Дело в том, что порт 7900 используется для протокола FD_SOCK2, указанного в стеке. Значение порта получаем из bind_port + offset, и обычно это 7800 + 100.

Второй интересующий нас блок — dns.DNS_PING. Он отвечает за обнаружение узлов кластера. Здесь мы указываем dns_query, совпадающий с headless‑service. Помимо DNS_PING существуют и другие методы обнаружения. Например, JDBC_PING и S3_PING, которые позволяют обращаться к внешнему источнику информации для обнаружения, к базе данных или бакету; или AWS_PING и AZURE_PING, которые обращаются к ресурсам публичного облака, где располагается приложение.

Можно выбрать механизм, подходящий под задуманную архитектуру, а нам достаточно было DNS_PING. Протоколы оставшегося стека не нуждаются в конфигурировании относительно приложения, оставим их как есть. Но при желании их параметры можно настроить, исходя из своих потребностей.

c1ee5c07120d49434d48bb57da8406cf.png

В итоге механизм обнаружения состоит из следующих шагов:

  1. При запуске узел брокера обращается по DNS_PING к DNS‑серверу. Брокер запрашивает dns_query, в котором прописан headless‑service statefulset-а.
  2. DNS‑сервер смотрит поды, подходящие под dns_query.
  3. DNS‑сервер возвращает список адресов узлу брокера.
  4. Узел брокера рассылает приглашения для вступления в кластер другим узлам из полученного списка. Идёт обмен кластерным паролем. Тут включаются в работу нижестоящие протоколы из стека:
  • MERGE3 — протокол для обнаружения подгрупп, возникающих при разделении и восстановлении сети.
  • FD_SOCK2 и FD_ALL3 — используются для обнаружения сбоев. FD_SOCK2 отслеживает работоспособность членов кластера через TCP‑соединения, а FD_ALL3 использует heartbeat.
  • VERIFY_SUSPECT2 — проверяет и подтверждает неактивность участника кластера.
  • pbcast.NAKACK2 — обеспечивает надёжную доставку сообщений с использованием механизма отрицательного подтверждения (NAK). Он обрабатывает повторные передачи отсутствующих сообщений, чтобы гарантировать получение сообщений всеми участниками.
  • pbcast.STABLE — вычисляет, какие широковещательные сообщения были доставлены всем участникам кластера, и отправляет события STABLE в стек. Это позволяет NAKACK2 удалять сообщения, которые видели все участники.
  • Узел брокера получает ответ, GMS‑протокол (Group Membership Service) его обрабатывает. Между узлами кластера вычисляется новая топология, и узлы объединяются.

Протоколы UFC и MFC используют кредитную систему для контроля потока сообщений и предотвращения перегрузок.

FRAG‑протокол фрагментирует сообщения размером больше указанного размера и собирает их на принимающей стороне.

Сайдкары Vault-Agent и Istio

В нашей архитектуре Artemis сконфигурирован на работу с mTLS. Помимо использования сертификатов для установления защищённого соединения они также используются для аутентификации клиентов. Брокер поддерживает работу с JKS keystore/truststore и, с недавнего времени, с PEM keystore/truststore.

Приложению необходимы пароли для JKS и для кластерного соединения. Чтобы не хранить секреты в конфигурационных файлах (даже в зашифрованном виде) и не использовать объекты типа Secret в K8s для паролей и keystore/truststore, мы используем Vault‑agent.

Через annotation в statefulset включается сайдкар и объявляются секреты, которые необходимо взять из хранилища и записать в файловую систему. Ниже приведены примеры запроса к PKI engine для выпуска PEM‑сертификата и обращению к KV‑хранилищу за cluster_password‑секретом (мы также немного доработали Artemis, чтобы он умел читать cluster_password из файла).

vault.hashicorp.com/agent-init-first: 'true' vault.hashicorp.com/agent-set-security-context: 'true' vault.hashicorp.com/agent-pre-populate: 'false' vault.hashicorp.com/agent-inject-secret-cluster.pass: 'true' vault.hashicorp.com/secret-volume-path-cluster.pass: /app/artemis/broker/vault vault.hashicorp.com/namespace: MY_VAULT_NAMESPACE vault.hashicorp.com/role: MY_ROLE vault.hashicorp.com/agent-inject: 'true' vault.hashicorp.com/agent-limits-cpu: 100m vault.hashicorp.com/agent-requests-cpu: 100m vault.hashicorp.com/secret-volume-path-crt.pem: /app/artemis/broker/vault vault.hashicorp.com/agent-inject-secret-crt.pem: 'true' vault.hashicorp.com/agent-inject-template-crt.pem: | {%- raw %} {{- with secret "PKI/issue/MY_ROLE" "common_name=my_artemis_app.my_domain" "format=pem" "ttl=20h" "private_key_format=pkcs8" -}} {{ .Data.private_key }} {{ .Data.certificate }} {{- end }} {%- endraw %} vault.hashicorp.com/agent-inject-template-cluster.pass: | {%- raw %} {{- with secret "PATH/TO/MY/KV/cloud_artemis" -}} {{ index .Data "cluster_password" }} {{- end }} {%- endraw %}

Когда под стартует, контейнер с прикладом ждёт, пока сайдкар Vault‑agent не создаст в файловой системе необходимые секреты по указанному пути. Таким образом мы получаем для приложения необходимые пароли, keystore и truststore для настройки TLS на сервере. Vault‑agent установлен не только на поде с приложением, но и на граничных шлюзах, что позволяет использовать Vault для получения сертификатов при интеграции с внешними системами.

Маршрутизация трафика внутри namespace и настройки mTLS

Стараясь не забывать и про сетевую безопасность, про которую нам заботливо напоминают коллеги всевозможными стандартами и проверками, мы обращаемся к любимому Istio. Рассказывать, как работает Istio, в этой статье мы не будем, пройдёмся лишь по моментам, актуальным для нашего проекта.

Нулевой шаг — включить Peer Authentication в режим mtls: strict, чтобы внутри namespace ходил только TLS‑трафик.

Далее пойдём по пути от «пользователя/приложения». Для приложения развёрнут Ingress, в который ходят пользователи для подключения к UI по console‑порту или приложения для отправки сообщений по data‑порту:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/ssl-passthrough: "true" name: artemis-istio-ingress namespace: my_namespace spec: rules: - host: ui-artemis-istio-ingress.my_cluster http: paths: - backend: service: name: artemis-ingressgateway-svc port: number: 8161 path: / pathType: Prefix - host: data-artemis-istio-ingress.my_cluster http: paths: - backend: service: name: artemis-ingressgateway-svc port: number: 61616 path: / pathType: Prefix tls: - hosts: - ui-artemis-istio-ingress.my_cluster - data-artemis-istio-ingress.my_cluster

Попадая на Ingress‑controller, трафик переводится на бэкенд, которым является сервис нашего поднятого Ingress‑шлюза. И, так как аутентификация пользователей осуществляется в приложении, мы пропускаем SSL‑трафик дальше, не прерывая его.

apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: artemis-ingressgateway namespace: my_namespace spec: selector: app: artemis-ingressgateway istio: artemis-ingressgateway servers: - hosts: - ui-artemis-istio-ingress.my_cluster port: name: tls-console number: 8161 protocol: tls tls: mode: PASSTHROUGH - hosts: - data-artemis-istio-ingress.my_cluster port: name: tls-data number: 61616 protocol: tls tls: mode: PASSTHROUGH --- apiVersion: v1 kind: Service metadata: name: artemis-ingressgateway-svc namespace: my_namespace ports: - name: tls-console port: 8161 protocol: TCP targetPort: 8161 - name: tls-data port: 61616 protocol: TCP targetPort: 61616 selector: app: artemis-ingressgateway istio: artemis-ingressgateway sessionAffinity: None type: ClusterIP

Дальше трафик регулируется через VirtualService и перенаправляется со шлюза на сервис приложения:

apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: artemis-ingress-vs namespace: my_namespace spec: exportTo: - . gateways: - artemis-ingressgateway hosts: - ui-artemis-istio-ingress.my_cluser - data-artemis-istio-ingress.my_cluster tls: - match: - gateways: - artemis-ingressgateway port: 8161 sniHosts: - ui-artemis-istio-ingress.my_cluster route: - destination: host: artemis-svc port: number: 8161 - match: - gateways: - artemis-ingressgateway port: 61616 sniHosts: - data-artemis-istio-ingress.my_cluster route: - destination: host: artemis-svc port: number: 61616

По пути «в сторону приложения» на трафик не накладывается никаких DestinationRule, так как ранее мы указали ssl-passthrough.

Как ходит трафик из приложения? Разберём на примере обращения в Vault, который находится вне нашего K8s. Чтобы сервис Istio знал, куда слать трафик, направление которого уходит за пределы кластера, необходимо определить ServiceEntry:

apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: name: vault-8443-service-entry spec: exportTo: - . hosts: - my.vault.host location: MESH_EXTERNAL ports: - name: http-vault number: 8443 protocol: https resolution: DNS

Объявляем Egress-шлюз и сервис шлюза:

apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: scripts-egressgateway spec: selector: app: artemis-egressgateway istio: artemis-egressgateway servers: - hosts: - my.vault.host port: name: tls-vault-9444 number: 9444 protocol: TLS tls: mode: PASSTHROUGH --- apiVersion: v1 kind: Service metadata: name: artemis-egressgateway-svc spec: ports: - name: status-port port: 15021 protocol: TCP targetPort: 15021 - name: tls-vault-9444 port: 9444 protocol: TCP targetPort: 9444 selector: app: artemis-egressgateway istio: artemis-egressgateway sessionAffinity: None type: ClusterIP

Направляем трафик через VirtualService:

apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: artemis-vault-vs spec: exportTo: - . gateways: - artemis-egressgateway - mesh hosts: - my.vault.host tcp: - match: - gateways: - mesh port: 8443 route: - destination: host: artemis-egressgateway-svc port: number: 9444 - match: - gateways: - artemis-egressgateway port: 9444 sniHosts: - my.vault.host route: - destination: host: my.vault.host port: number: 8443

Так как трафик идёт из приложения в Vault уже с использованием TLS, настроенным в Vault-agent, никаких дополнительных DestinationRule ставить не надо.

С трафиком, который ходит в приложение и из приложения, разобрались, перейдём к самому кластеру приложения. Если вы подумали, что после кластеризации через Jgroups всё самое неприятное позади, спешим вас переубедить: трафик внутри кластера тоже необходимо перевести в TLS.

У нас было два варианта: настраивать SSL непосредственно через Jgroups или пустить всё через Istio. Раз всё остальное ходит через Istio, то и тут мы решили не мудрить, включили режим отладки и пошли разбираться.

Первая проблема, с которой мы столкнулись, открыв журналы, — все запросы на обнаружение узлов уходили в BlackHoleCluster. Мы задали ServiceEntry для нашего headless‑service, и трафик стал доходить до DNS‑сервиса и возвращать список узлов кластера.

apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: name: artemis-headless spec: exportTo: - . hosts: - artemis-hdls-svc location: MESH_INTERNAL ports: - name: jgroups-7800 number: 7800 protocol: TCP - name: jgroups-7900 number: 7900 protocol: TCP resolution: NONE workloadSelector: labels: app: artemis-app

Но аналогичная проблема появлялась при общении узлов между собой. Когда брокер, получив список хостов, начинал рассылать приглашения о вступлении в кластер, нас снова засасывало в чёрные дыры. Объявляем ещё один ServiceEntry, на этот раз для хостов кластера. Так как мы заранее не знаем, какой адрес достанется поду при развёртывании или масштабировании, то в манифесте указываем любой адрес (0.0.0.0/0), но с Jgroups‑портами и с data‑портом для работы приклада и пересылки сообщений между узлами кластера.

apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: name: artemis-cluster spec: addresses: - 0.0.0.0/0 exportTo: - . hosts: - artemis.hosts location: MESH_INTERNAL ports: - name: cluster number: 61617 protocol: TCP - name: jgroups-7800 number: 7800 protocol: TCP - name: jgroups-7900 number: 7900 protocol: TCP resolution: NONE workloadSelector: labels: app: artemis-app

Следующая ошибка — NR filter_chain_not_found. Она возникала из‑за того, что у нас стоит peerAutherntication mtls:strict, и трафик, который ходит в рамках процессов по кластеризации Jgroups, не покрыт TLS. Настраиваем DestinationRule на mTLS с сертификатами Istio для портов кластера:

apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: artemis-clustering-dr spec: exportTo: - . host: artemis.hosts trafficPolicy: portLevelSettings: - port: number: 61617 tls: mode: ISTIO_MUTUAL - port: number: 7800 tls: mode: ISTIO_MUTUAL - port: number: 7900 tls: mode: ISTIO_MUTUAL workloadSelector: matchLabels: app: artemis-app

Открываем журналы Istio и видим, что трафик начал ходить по необходимым портам:

info Envoy proxy is ready "- - -" 0 - - - "-" 192 0 7747 - "-" "-" "-" "-" "172.21.10.42:7800" outbound|7800|| artemis-hdls-svc 172.21.1.146:59028 172.21.10.42:7800 172.21.1.146:35715 - - "- - -" 0 - - - "-" 1854 1527 40980 - "-" "-" "-" "-" "127.0.0.1:7800" inbound|7800|| 127.0.0.1:42266 172.21.1.146:7800 172.21.10.179:46534 outbound_.7800_._. artemis-hdls-svc -

Итоги и векторы развития

Мы получили:

  • полностью функциональный кластер брокеров Artemis в Kubernetes без использования оператора;
  • бесценный опыт настройки и отладки Istio;
  • немного седых волос.

Текущая реализация кластера ограничивается работой в неперсистентном режиме — сообщения хранятся только в памяти брокера и не записываются на диск. Поэтому нашим следующим шагом будет настройка персистентного кластера с хранилищами в PersistentVolume на диске или S3. Такая доработка позволит перенести шифрование сообщений по модели «encryption at rest», которая уже реализована на ВМ.

Ещё одной зоной исследования и улучшений является производительность. На момент написания статьи конечные результаты нагрузочного тестирования, которые были бы релевантны по отношению к ВМ, ещё не получены. Но и так очевидно, что производительность кластера в облаке будет меньше, чем на ВМ, из‑за дополнительных процессов с трафиком и работы в контейнерах.

Также для увеличения отказоустойчивости мы планируем развёртывать мультикластер, растянутый между несколькими ЦОДами. И с помощью того же Istio будем обрабатывать падения узлов и переключаться на рабочие ноды.

Все эти разработки мы проводим в рамках продукта Platform V Synapse Messaging, который входит в состав Platform V Synapse — комплекса облачных продуктов для интеграции и оркестрации микросервисов. Он позволяет импортозаместить любые корпоративные сервисные шины, обеспечивает обработку данных для бизнес‑решений в реальном времени и интегрирует технологии в единый производственный процесс.