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

от автора

Привет, Хабр! Меня зовут Артем Безруков, я 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-чартов

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

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

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

  1. При запуске узел брокера обращается по DNS_PING к DNS‑серверу. Брокер запрашивает dns_query, в котором прописан headless‑service statefulset-а.

  2. DNS‑сервер смотрит поды, подходящие под dns_query.

  3. DNS‑сервер возвращает список адресов узлу брокера.

  4. Узел брокера рассылает приглашения для вступления в кластер другим узлам из полученного списка. Идёт обмен кластерным паролем. Тут включаются в работу нижестоящие протоколы из стека:

    1. MERGE3 — протокол для обнаружения подгрупп, возникающих при разделении и восстановлении сети.

    2. FD_SOCK2 и FD_ALL3 — используются для обнаружения сбоев. FD_SOCK2 отслеживает работоспособность членов кластера через TCP‑соединения, а FD_ALL3 использует heartbeat.

    3. VERIFY_SUSPECT2 — проверяет и подтверждает неактивность участника кластера.

    4. pbcast.NAKACK2 — обеспечивает надёжную доставку сообщений с использованием механизма отрицательного подтверждения (NAK). Он обрабатывает повторные передачи отсутствующих сообщений, чтобы гарантировать получение сообщений всеми участниками.

    5. pbcast.STABLE — вычисляет, какие широковещательные сообщения были доставлены всем участникам кластера, и отправляет события STABLE в стек. Это позволяет NAKACK2 удалять сообщения, которые видели все участники.

    6. Узел брокера получает ответ, 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-app [Thread-2 (ActiveMQ-server-org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl$6@6b69761b)] INFO  org.apache.activemq.artemis.core.server  - AMQ221027: Bridge ClusterConnectionBridge@6d8264f3 [name=$.artemis.internal.sf.my-cluster.b49f0fb1-53d4-11ef-8504-568fdad344ae, queue=QueueImpl[name=$.artemis.internal.sf.my-cluster.b49f0fb1-53d4-11ef-8504-568fdad344ae, postOffice=PostOfficeImpl [server=ActiveMQServerImpl::name=artemis-statefulset-0], temp=false]@13e456bc targetConnector=ServerLocatorImpl (identity=(Cluster-connection-bridge::ClusterConnectionBridge@6d8264f3 [name=$.artemis.internal.sf.my-cluster.b49f0fb1-53d4-11ef-8504-568fdad344ae, queue=QueueImpl[name=$.artemis.internal.sf.my-cluster.b49f0fb1-53d4-11ef-8504-568fdad344ae, postOffice=PostOfficeImpl [server=ActiveMQServerImpl::name=artemis-statefulset-0], temp=false]@13e456bc targetConnector=ServerLocatorImpl [initialConnectors=[TransportConfiguration(name=cluster, factory=org-apache-activemq-artemis-core-remoting-impl-netty-NettyConnectorFactory)?enabledProtocols=TLSv1-2,TLSv1-3&port=61617&sslEnabled=false&host=172-21-10-42&verifyHost=false], discoveryGroupConfiguration=null]]::ClusterConnectionImpl@1887326180[nodeUUID=97d07d4c-53d4-11ef-8aab-5e95e30bd562, connector=TransportConfiguration(name=cluster, factory=org-apache-activemq-artemis-core-remoting-impl-netty-NettyConnectorFactory)?enabledProtocols=TLSv1-2,TLSv1-3&port=61617&sslEnabled=false&host=172-21-1-146&verifyHost=false, address=, server=ActiveMQServerImpl::name=artemis-statefulset-0])) [initialConnectors=[TransportConfiguration(name=cluster, factory=org-apache-activemq-artemis-core-remoting-impl-netty-NettyConnectorFactory)?enabledProtocols=TLSv1-2,TLSv1-3&port=61617&sslEnabled=false&host=172-21-10-42&verifyHost=false], discoveryGroupConfiguration=null]] is connected

В UI Artemis топология кластера обновилась, и связь образовалась между aкцепторами двух узлов.

После перезапуска одного из подов кластера (.42) в журналах опять можно заметить трафик по Jgroups‑портам в процессе изменения топологии и коммуникацию с новой подой (.179) по data‑порту aкцептора.

"- - -" 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 - "- - -" 0 - - - "-" 1281 1292 3226 - "-" "-" "-" "-" "127.0.0.1:7900" inbound|7900|| 127.0.0.1:46662 172.21.1.146:7900 172.21.10.179:52084 outbound_.7900_._. artemis-hdls-svc - "- - -" 0 - - - "-" 3347 4241 38730 - "-" "-" "-" "-" "127.0.0.1:7800" inbound|7800|| 127.0.0.1:46792 172.21.1.146:7800 172.21.10.179:48670 outbound_.7800_._. artemis-hdls-svc - "- - -" 0 - - - "-" 1213 1211 1527 - "-" "-" "-" "-" "127.0.0.1:61617" inbound|61617|| 127.0.0.1:59074 172.21.1.146:61617 172.21.10.179:40322 outbound_.61617_._.artemis.hosts - "- - -" 0 - - - "-" 1213 1211 787 - "-" "-" "-" "-" "127.0.0.1:61617" inbound|61617|| 127.0.0.1:59088 172.21.1.146:61617 172.21.10.179:40350 outbound_.61617_._.artemis.hosts - "- - -" 0 - - - "-" 1213 1211 1527 - "-" "-" "-" "-" "127.0.0.1:61617" inbound|61617|| 127.0.0.1:59086 172.21.1.146:61617 172.21.10.179:40336 outbound_.61617_._.artemis.hosts -

Чтобы при перезапуске под узлы Artemis успевали обмениваться сообщениями в очередях, обязательно надо добавить аннотацию для сайдкара Istio, которая задаёт ожидание завершения сетевых соединений перед завершением работы.

proxy.istio.io/config:|   proxyMetadata:     EXIT_ON_ZERO_ACTIVE_CONNECTIONS: 'true'

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

Мы получили:

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

  • бесценный опыт настройки и отладки Istio;

  • немного седых волос.

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

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/849362/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *