Привет, Хабр! Меня зовут Артем Безруков, я 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 — комплекса облачных продуктов для интеграции и оркестрации микросервисов. Он позволяет импортозаместить любые корпоративные сервисные шины, обеспечивает обработку данных для бизнес‑решений в реальном времени и интегрирует технологии в единый производственный процесс.
ссылка на оригинал статьи https://habr.com/ru/articles/849362/
Добавить комментарий