Привет, Хабр! Меня зовут Артем Безруков, я 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
. Протоколы оставшегося стека не нуждаются в конфигурировании относительно приложения, оставим их как есть. Но при желании их параметры можно настроить, исходя из своих потребностей.
В итоге механизм обнаружения состоит из следующих шагов:
При запуске узел брокера обращается по
DNS_PING
к DNS‑серверу. Брокер запрашиваетdns_query
, в котором прописан headless‑service statefulset-а.DNS‑сервер смотрит поды, подходящие под
dns_query
.DNS‑сервер возвращает список адресов узлу брокера.
-
Узел брокера рассылает приглашения для вступления в кластер другим узлам из полученного списка. Идёт обмен кластерным паролем. Тут включаются в работу нижестоящие протоколы из стека:
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-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 — комплекса облачных продуктов для интеграции и оркестрации микросервисов. Он позволяет импортозаместить любые корпоративные сервисные шины, обеспечивает обработку данных для бизнес‑решений в реальном времени и интегрирует технологии в единый производственный процесс.