Настраиваем отказоустойчивый Keycloak с Infinispan в Kubernetes

от автора

В этой статье мы поделимся опытом развертывания в кластере Kubernetes устойчивой и масштабируемой инсталляции популярного решения для обеспечения «единого входа» (SSO) — Keycloak в связке с Infinispan (для кэширования пользовательских метаданных).

Keycloak и область применения

Keycloak – проект с открытым исходным кодом компании Red Hat, предназначенный для управления аутентификацией и авторизацией в приложениях, функционирующих на серверах приложений WildFly, JBoss EAP, JBoss AS и прочих web-серверах. Keycloak упрощает реализацию защиты приложений, предоставляя им бэкенд авторизации практически без дополнительного кода. За подробной информацией о том, как это осуществляется, можно обратиться к этому руководству.

Как правило, Keycloak устанавливается на отдельный виртуальный или выделенный сервер приложений WildFly. Пользователи однократно аутентифицируются с помощью Keycloak для всех приложений, интегрированных с данным решением. Таким образом, после входа в Keycloak пользователям не нужно снова входить в систему для доступа к другому приложению. Аналогично происходит и с выходом.

Для хранения своих данных Keycloak поддерживает работу с рядом наиболее популярных реляционных систем управления базами данных (РСУБД): Oracle, MS SQL, MySQL, PostgreSQL. В нашем случае использовалась CockroachDB — современная распределенная СУБД (изначально Open Source, а впоследствии — под BSL), которая обеспечивает согласованность данных, масштабируемость и устойчивость к авариям. Одной из её приятных особенностей является совместимость с PostgreSQL на уровне протокола.

Кроме того, в своей работе Keycloak активно использует кэширование: кэшируются пользовательские сессии, авторизационные и аутентификационные токены, успешные и неуспешные попытки авторизации. По умолчанию для хранения всего этого используется Infinispan. На ней мы остановимся подробнее.

Infinispan

Infinispan — это масштабируемая, высокодоступная платформа для хранения данных типа ключ-значение, написанная на Java и распространяемая под свободной лицензией (Apache License 2.0). Основная область применения Infinispan — распределенный кэш, но также её применяют как KV-хранилище в базах данных типа NoSQL.

Платформа поддерживает два способа запуска: развертывание в качестве отдельно-стоящего сервера / кластера серверов и использование в виде встроенной библиотеки для расширения функций основного приложения.

KC в конфигурации по умолчанию использует встроенный кэш Infinispan. Он позволяет настраивать распределенные кэши, чтобы репликация и перекаты данных осуществлялись без простоя. Таким образом, даже если мы полностью отключим сам KC, а потом поднимем его обратно, авторизованных пользователей это не затронет.

Сам IS хранит всё в памяти, а на случай переполнения (или полного отключения IS) можно настроить сбрасывание его данных в БД. В нашем случае эту функцию выполняет CockroachDB.

Постановка задачи

Клиент уже использовал KC как бэкенд авторизации своего приложения, но переживал за устойчивость решения и сохранность кэшей при авариях / развертываниях. Поэтому перед нами стояли две задачи:

  1. Обеспечить надежность/устойчивость к авариям, высокую доступность.

  2. Сохранить пользовательские данные (сессии, токены) при потенциальном переполнении памяти.

Описание инфраструктуры и архитектуры решения

Изначально KC был запущен в 1 реплике и настройками кэширования по умолчанию, т.е. использовался встроенный Infinispan, который все держал в памяти. Источником данных был кластер CockroachDB.

Для обеспечения надежности потребовалось развернуть несколько реплик KC. Keycloak позволяет это сделать, используя несколько механизмов автообнаружения. В первой итерации мы сделали 3 реплики KC, использующих IS в качестве модуля/плагина:

К сожалению, IS, используемый как модуль, предоставлял недостаточно возможностей для настройки поведения кэшей (кол-во записей, объем занимаемой памяти, алгоритмы вытеснения в постоянное хранилище) и предлагал только файловую систему как постоянное хранилище для данных.

Поэтому на следующей итерации мы развернули отдельный кластер Infinispan и отключили встроенный модуль IS в настройках Keycloak:

Решение было развернуто в кластере Kubernetes. Keycloak и Infinispan запущены в одном namespace по 3 реплики. За основу для такой инсталляции был взят этот Helm-чарт. CockroachDB разворачивалась в отдельном пространстве имен и использовалась совместно с другими компонентами клиентского приложения.

Практическая реализация

Полные примеры Helm-шаблонов доступны в нашем репозитории flant/examples.

1. Keycloak

КС поддерживает несколько режимов запуска: standalone, standalone-ha, domain cluster, DC replication. Режим standalone-ha является идеальным вариантом для запуска в Kubernetes, потому что легко добавлять/удалять реплики, общий конфиг-файл хранится в ConfigMap, правильно выбранная стратегия развертывания обеспечивает доступность узлов при обновлении ПО.

Хотя для KC не требуется постоянного файлового хранилища (PV/PVC) и можно было выбрать тип Deployment, мы используем StatefulSet. Это делается для того, чтобы задавать имя узлов в Java-переменной jboss.node.name при настройке обнаружения узлов на основе DNS_PING. Длина этой переменной должна быть меньше 23 символов.

Для настройки KC используются:

  • переменные окружения, которые задают режимы работы KC (standalone, standalone-ha и т.д.);

  • конфигурационный файл /opt/jboss/keycloak/standalone/configuration/standalone-ha.xml, который позволяет выполнить максимально полную и точную настройку Keycloak;

  • переменные JAVA_OPTS, определяющие поведение Java-приложения. 

По умолчанию KC запускается со standalone.xml — этот конфиг сильно отличается от HA-версии. Для получения нужной нам конфигурации добавим в values.yaml:

# Additional environment variables for Keycloak extraEnv: | …    - name: JGROUPS_DISCOVERY_PROTOCOL      value: "dns.DNS_PING"    - name: JGROUPS_DISCOVERY_PROPERTIES      value: "dns_query={{ template "keycloak.fullname". }}-headless.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}"    - name: JGROUPS_DISCOVERY_QUERY      value: "{{ template "keycloak.fullname". }}-headless.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}"

После первого запуска можно достать из pod’а c KC нужный конфиг и на его основе подготовить .helm/templates/keycloak-cm.yaml:

$ kubectl -n keycloak cp keycloak-0:/opt/jboss/keycloak/standalone/configuration/standalone-ha.xml /tmp/standalone-ha.xml

После получения файла переменные JGROUPS_DISCOVERY_PROTOCOL и JGROUPS_DISCOVERY_PROPERTIES можно переименовать или удалить, чтобы KC не пытался создавать этот файл при каждом повторном деплое.

Устанавливаем JAVA_OPTS в .helm/values.yaml:

java:   _default: "-server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED -Djava.awt.headless=true -Djboss.default.jgroups.stack=kubernetes -Djboss.node.name=${POD_NAME} -Djboss.tx.node.id=${POD_NAME} -Djboss.site.name=${POD_NAMESPACE} -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled -Dkeycloak.profile.feature.token_exchange=enabled -Djboss.default.multicast.address=230.0.0.5 -Djboss.modcluster.multicast.address=224.0.1.106 -Djboss.as.management.blocking.timeout=3600"

 Для корректной работы DNS_PING указываем:

-Djboss.node.name=${POD_NAME}, -Djboss.tx.node.id=${POD_NAME} -Djboss.site.name=${POD_NAMESPACE} и -Djboss.default.multicast.address=230.0.0.5 -Djboss.modcluster.multicast.address=224.0.1.106 

Все остальные манипуляции проводим с .helm/templates/keycloak-cm.yaml.

Подключение базы:

            <subsystem xmlns="urn:jboss:domain:datasources:6.0">                 <datasources>                     <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" use-ccm="true">                         <connection-url>jdbc:postgresql://${env.DB_ADDR:postgres}/${env.DB_DATABASE:keycloak}${env.JDBC_PARAMS:}</connection-url>                         <driver>postgresql</driver>                         <pool>                             <flush-strategy>IdleConnections</flush-strategy>                         </pool>                         <security>                             <user-name>${env.DB_USER:keycloak}</user-name>                             <password>${env.DB_PASSWORD:password}</password>                         </security>                         <validation>                             <check-valid-connection-sql>SELECT 1</check-valid-connection-sql>                             <background-validation>true</background-validation>                             <background-validation-millis>60000</background-validation-millis>                         </validation>                     </datasource>                     <drivers>                         <driver name="postgresql" module="org.postgresql.jdbc">                             <xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>                         </driver>                     </drivers>                 </datasources>             </subsystem>             <subsystem xmlns="urn:jboss:domain:ee:5.0">             …                  <default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/KeycloakDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>             </subsystem>

Настройки кэшей:

           <subsystem xmlns="urn:jboss:domain:infinispan:11.0">                 <cache-container name="keycloak" module="org.keycloak.keycloak-model-infinispan">                     <transport lock-timeout="60000"/>                      <local-cache name="realms">                               <heap-memory size="10000"/>                     </local-cache>                         <!-- В локальном кэше храним users, authorization и keys - аналогично realms -->                     <replicated-cache name="work"/>                                          <distributed-cache name="authenticationSessions" owners="${env.CACHE_OWNERS_AUTH_SESSIONS_COUNT:1}">                       <remote-store cache="authenticationSessions" remote-servers="remote-cache" passivation="false" preload="false" purge="false" shared="true">                         <property name="rawValues">true</property>                         <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>                       </remote-store>                     </distributed-cache>                       <!-- В отдельно стоящем IS - sessions,  offlineSessions, clientSessions, offlineClientSessions, loginFailures и actionTokens -->                        <!-- Для actionTokens устанавливаем owners = env.CACHE_OWNERS_AUTH_SESSIONS_COUNT (>=2) - для их сохранности в момент редеплоя -->                 </cache-container>             </subsystem>

Настройки JGROUPS и DNS_PING:

            <subsystem xmlns="urn:jboss:domain:jgroups:8.0">                 <channels default="ee">                               <channel name="ee" stack="tcp" cluster="ejb"/>                 </channels>                 <stacks>                      <stack name="udp">                         <transport type="UDP" socket-binding="jgroups-udp"/>                         <protocol type="dns.DNS_PING">                             <property name="dns_query">${env.JGROUPS_DISCOVERY_QUERY}</property>                         </protocol>                         ...                     </stack>                     <stack name="tcp">                         <transport type="TCP" socket-binding="jgroups-tcp"/>                         <protocol type="dns.DNS_PING">                             <property name="dns_query">${env.JGROUPS_DISCOVERY_QUERY}</property>                         </protocol>                         ...                     </stack>                 </stacks>             </subsystem>         <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">             <socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>             <socket-binding name="http" port="${jboss.http.port:8080}"/>             <socket-binding name="https" port="${jboss.https.port:8443}"/>             <socket-binding name="jgroups-mping" interface="private" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>             <socket-binding name="jgroups-tcp" interface="private" port="7600"/>             <socket-binding name="jgroups-tcp-fd" interface="private" port="57600"/>             <socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>             <socket-binding name="jgroups-udp-fd" interface="private" port="54200"/>             <socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>             <socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>             <socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>             <socket-binding name="txn-recovery-environment" port="4712"/>             <socket-binding name="txn-status-manager" port="4713"/>         </socket-binding-group>

Наконец, подключаем внешний Infinispan:

        <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">            …             <outbound-socket-binding name="remote-cache">                 <remote-destination host="${env.INFINISPAN_SERVER}" port="11222"/>             </outbound-socket-binding>            …         </socket-binding-group>

Подготовленный XML-файл монтируем в контейнер из ConfigMap’а .helm/templates/keycloak-cm.yaml

apiVersion: apps/v1 kind: StatefulSet metadata:   name: keycloak-stand spec:   serviceName: keycloak-stand-headless   template:     spec:       containers:         image: registry.host/keycloak         name: keycloak         volumeMounts:         - mountPath: /opt/jboss/keycloak/standalone/configuration/standalone-ha.xml           name: standalone           subPath: standalone.xml       volumes:       - configMap:           defaultMode: 438           name: keycloak-stand-standalone         name: standalone

2. Infinispan

Настройка Infinispan гораздо легче, чем KC, поскольку шаги с генерацией нужного конфиг-файла отсутствуют.

Достаем конфиг по умолчанию /opt/infinispan/server/conf/infinispan.xml из Docker-образа infinispan/server:12.0 и на его основе готовим .helm/templates/infinispan-cm.yaml.

Первым делом настраиваем auto-discovery. Для этого устанавливаем уже знакомые нам переменные окружения в .helm/templates/infinispan-sts.yaml:

        env: {{- include "envs" . | indent 8 }}         - name: POD_IP           valueFrom:             fieldRef:               fieldPath: status.podIP         - name: JGROUPS_DISCOVERY_PROTOCOL           value: "dns.DNS_PING"         - name: JGROUPS_DISCOVERY_PROPERTIES           value: dns_query={{ ( printf "infinispan-headless.keycloak-%s.svc.cluster.local" .Values.global.env ) }}

… и добавляем секцию jgroups в XML-конфиг:

        <jgroups>             <stack name="image-tcp" extends="tcp">                 <TCP bind_addr="${env.POD_IP}" bind_port="${jgroups.bind.port,jgroups.tcp.port:7800}" enable_diagnostics="false"/>                 <dns.DNS_PING dns_address="" dns_query="${env.INFINISPAN_SERVER}" dns_record_type="A" stack.combine="REPLACE" stack.position="MPING"/>             </stack>             <stack name="image-udp" extends="udp">                 <UDP enable_diagnostics="false" port_range="0" />                 <dns.DNS_PING dns_address="" dns_query="${env.INFINISPAN_SERVER}" dns_record_type="A" stack.combine="REPLACE" stack.position="PING"/>                 <FD_SOCK client_bind_port="57600" start_port="57600"/>             </stack>         </jgroups>

Для корректной работы Infinispan c CockroachDB нам пришлось пересобрать образ Infinispan, добавив в него новую версию SQL-драйвера PostgreSQL. Для сборки использовалась утилита werf с таким простым werf.yaml:

--- image: infinispan from: infinispan/server:12.0 git: - add: /jar/postgresql-42.2.19.jar   to: /opt/infinispan/server/lib/postgresql-42.2.19.jar shell:   setup: |     chown -R 185:root /opt/infinispan/server/lib/

Добавим в XML-конфиг секцию <data-source>:

            <data-sources>               <data-source name="ds" jndi-name="jdbc/datasource" statistics="true">                   <connection-factory driver="org.postgresql.Driver" username="${env.DB_USER:keycloak}" password="${env.DB_PASSWORD:password}" url="jdbc:postgresql://${env.DB_ADDR:postgres}:${env.DB_PORT:26257}/${env.DB_DATABASE:keycloak}${env.JDBC_PARAMS_IS:}" new-connection-sql="SELECT 1" transaction-isolation="READ_COMMITTED">                     <connection-property name="name">value</connection-property>                   </connection-factory>                   <connection-pool initial-size="1" max-size="10"  min-size="3" background-validation="1000" idle-removal="1" blocking-timeout="1000" leak-detection="10000"/>               </data-source>             </data-sources>

В Infinispan мы должны описать те кэши, которые в KC были созданы с типом distributed-cache. Например, offlineSessions:

            <distributed-cache name="offlineSessions" owners="${env.CACHE_OWNERS_COUNT:1}" xmlns:jdbc="urn:infinispan:config:store:jdbc:12.0">                <persistence passivation="false">                    <jdbc:string-keyed-jdbc-store fetch-state="false" shared="true" preload="false">                        <jdbc:data-source jndi-url="jdbc/datasource"/>                        <jdbc:string-keyed-table drop-on-exit="false" create-on-start="true" prefix="ispn">                            <jdbc:id-column name="id" type="VARCHAR(255)"/>                            <jdbc:data-column name="datum" type="BYTEA"/>                            <jdbc:timestamp-column name="version" type="BIGINT"/>                            <jdbc:segment-column name="S" type="INT"/>                        </jdbc:string-keyed-table>                    </jdbc:string-keyed-jdbc-store>                </persistence>             </distributed-cache>

Таким же образом настраиваем и остальные кэши.

Подключение XML-конфига происходит аналогично тому, что мы рассматривали Keycloak.

На этом настройка Keycloak и Infinispan закончена. Повторюсь, что полные листинги доступны на GitHub: flant/examples.

Заключение

Использование Kubernetes в качестве фундамента позволило легко масштабировать решение, добавляя по мере необходимости или узлы Keycloak для обработки входящих запросов, или узлы Infinispan для увеличения емкости кэшей.

С момента сдачи данной работы клиенту прошло 2 месяца. Каких-либо жалоб и недостатков за этот период не выявлено. Поэтому можно считать, что поставленные цели достигнуты: мы получили устойчивое, масштабируемое решение для обеспечения SSO.

P.S.

Читайте также в нашем блоге:

ссылка на оригинал статьи https://habr.com/ru/company/flant/blog/567626/


Комментарии

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

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