Кластеризация Keycloak

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

Здесь я описываю весь опыт, которые постиг в процесс кластеризации.

В данной статье описывается геораспределенное кластерное решение

Общая схема распределения серверов следующая.

KeycloakAPP — это узлы кластера Keycloak

KeycloakDB — узлы кластера Patroni

DC1 Reverse и DC2 Reverse — реверс-прокси, расположенные в разных ДЦ

Мы ставим Keycloak на основе Quarkus, поэтому он у нас будет работать как служба.

Базовая подготовка

Необходимо развернуть кластер PostgreSQL. Развертывание кластера PostgreSQL в данной статье не рассматривается, так как статей на эту тему в интернете много.

Также необходимо создать БД для keycloak и пользователя, под которым Keycloak будет подключаться к БД.

Установка Keycloak

На все узлы серверов приложений Keycloak необходимо установить Java, Haproxy и загрузить архив с исполняемыми файлами Keycloak c сайта https://www.keycloak.org/downloads

Haproxy нам потребуется для подключения серверов приложений к БД

Распаковываем архив в директорию /opt/keycloak-$version и делаем симлинк /opt/keycloak на эту директорию, чтобы в дальнейшем легче было обновлять.

Создаем unit-file для сервиса /etc/systemd/system/keycloak.service

[Unit]
Description=Keycloak Identity Provider
Requires=network.target
After=syslog.target network.target

[Service]
Type=idle
User=root
Group=root
LimitNOFILE=102642
ExecStart="/opt/keycloak"/bin/kc.sh start --https-port=8080 --log=console,file
[Install]
WantedBy=multi-user.target

Выполняем команду systemctl daemon-reload

Настройка Haproxy

Создаем конфигурационный файл /etc/haproxy/haproxy.cfg и вносим в него следующий текст.

global
    maxconn 100000
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

defaults
    mode               tcp
    log                global
    retries            2
    timeout queue      5s
    timeout connect    5s
    timeout client     60m
    timeout server     60m
    timeout check      15s

listen stats
    mode http
    bind *:7000
    stats enable
    stats uri /stats
    stats auth login:pass

listen master
    bind 0.0.0.0:5432
    maxconn 10000
    option tcplog
    option httpchk OPTIONS /primary
    http-check expect status 200
    default-server inter 3s fastinter 1s fall 3 rise 4 on-marked-down shutdown-sessions
server db1 192.168.0.1:5432 check port 8008
server db2 192.168.10.1:5432 check port 8008
server db3 10.172.54.32:5432 check port 8008


listen replicas
    bind 0.0.0.0:5001
    maxconn 10000
    option tcplog
        option httpchk OPTIONS /replica?lag=100MB
        balance roundrobin
    http-check expect status 200
    default-server inter 3s fastinter 2s fall 3 rise 2 on-marked-down shutdown-sessions
server db1 192.168.0.1:5432 check port 8008
server db2 192.168.10.1:5432 check port 8008
server db3 10.172.54.32:5432 check port 8008

Haproxy необходимо сконфигурировать таким образом на каждом узле приложений.

Суть в том, что Haproxy сам определяет при таком конфиге, кто мастер и запросы отправляет на него, вне зависимости от того, кто стал мастером.

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

При установке Haproxy на сам сервер приложений мы ничего не теряем, если у нас упадет один или два сервера приложений.

Настройка Keycloak

Редактируем конфигурационный файл /opt/keycloak/conf/keycloak.conf и вносим в него следующие строки

# Database
db=postgres
db-username=Пользователь БД
db-password=Пароль пользователя БД
db-url=jdbc:postgresql://127.0.0.1/Название БД

# Metrics
health-enabled=true
metrics-enabled=true
http-metrics-histograms-enabled=true
cache-metrics-histograms-enabled=true
http-metrics-slos=5,10,25,50,250,500,1000,2500,5000,10000
# Log
log=console,file
log-format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n
log-level=info,org.keycloak.truststore:debug,org.keycloak.events:debug,org.infinispan:info
log-file=/var/log/keycloak.log

# HTTP
http-enabled=true
proxy-headers=xforwarded
hostname=FQDN keycloak

Как видим, здесь мы говорим кейлоку подключаться на localhost, на котором у нас слушает Haproxy и он будет перенаправлять трафик к серверам БД

Настройка кластера Keycloak

Для настройки кластера Keycloak нам необходимо на каждом сервере создать в директории /opt/keycloak/conf/cache-ispn.xml и внести в него следующий код.

В комментариях даны пояснения по тем параметрам, которые необходимо менять.

<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates
  ~ and other contributors as indicated by the @author tags.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~ http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"
        xmlns="urn:infinispan:config:15.0">

<jgroups>
    <stacks>
    <stack name="keycloak-stack" extends="tcp">
        <TCP 
        bind_addr="${jgroups.bind.address,jgroups.tcp.address:192.168.0.2}" 
        bind_port="${jgroups.bind.port,jgroups.tcp.port:7600}" 
        thread_naming_pattern="pl"
        send_buf_size="640k"
        sock_conn_timeout="300"
        bundler_type="transfer-queue"
        thread_pool.min_threads="${jgroups.thread_pool.min_threads:0}"
        thread_pool.max_threads="${jgroups.thread_pool.max_threads:200}"
        thread_pool.keep_alive_time="60000"
        />
      <TCPPING initial_hosts="${jgroups.tcpping.initial_hosts:192.168.0.2[7600],192.168.0.3[7600],192.168.0.4[7600],192.168.0.5[7600]}" 
          stack.combine="REPLACE" stack.position="MPING"/>
    <FD_ALL3 stack.combine="COMBINE" interval="1000"  timeout="4000"/>
      </stack>
  </stacks>
</jgroups>

    <cache-container name="keycloak">
        <transport lock-timeout="60000"/>
         <transport cluster="${infinispan.cluster.name:keycloak-cluster}"
             stack="keycloak-stack"
             node-name="${infinispan.node.name:KeycloakNode01}"/>
        <local-cache name="realms" simple-cache="true">
            <encoding>
                <key media-type="application/x-java-object"/>
                <value media-type="application/x-java-object"/>
            </encoding>
            <memory max-count="10000"/>
        </local-cache>
        <local-cache name="users" simple-cache="true">
            <encoding>
                <key media-type="application/x-java-object"/>
                <value media-type="application/x-java-object"/>
            </encoding>
            <memory max-count="10000"/>
        </local-cache>
        <distributed-cache name="sessions" owners="2">
            <expiration lifespan="-1"/>
            <memory max-count="10000"/>
        </distributed-cache>
        <distributed-cache name="authenticationSessions" owners="2">
            <expiration lifespan="-1"/>
        </distributed-cache>
        <distributed-cache name="offlineSessions" owners="2">
            <expiration lifespan="-1"/>
            <memory max-count="10000"/>
        </distributed-cache>
        <distributed-cache name="clientSessions" owners="2">
            <expiration lifespan="-1"/>
            <memory max-count="10000"/>
        </distributed-cache>
        <distributed-cache name="offlineClientSessions" owners="2">
            <expiration lifespan="-1"/>
            <memory max-count="10000"/>
        </distributed-cache>
        <distributed-cache name="loginFailures" owners="2">
            <expiration lifespan="-1"/>
        </distributed-cache>
        <local-cache name="authorization" simple-cache="true">
            <encoding>
                <key media-type="application/x-java-object"/>
                <value media-type="application/x-java-object"/>
            </encoding>
            <memory max-count="10000"/>
        </local-cache>
        <replicated-cache name="work">
            <expiration lifespan="-1"/>
        </replicated-cache>
        <local-cache name="keys" simple-cache="true">
            <encoding>
                <key media-type="application/x-java-object"/>
                <value media-type="application/x-java-object"/>
            </encoding>
            <expiration max-idle="3600000"/>
            <memory max-count="1000"/>
        </local-cache>
        <distributed-cache name="actionTokens" owners="2">
            <encoding>
                <key media-type="application/x-java-object"/>
                <value media-type="application/x-java-object"/>
            </encoding>
            <expiration max-idle="-1" lifespan="-1" interval="300000"/>
            <memory max-count="-1"/>
        </distributed-cache>
    </cache-container>
</infinispan>

<stack name=»keycloak-stack» extends=»tcp»> — Название вашего стека, задается по желанию

bind_addr=»${jgroups.bind.address,jgroups.tcp.address:192.168.0.2}» — Указывается адрес текущего сервера Keycloak.

bind_port=»${jgroups.bind.port,jgroups.tcp.port:7600}» — порт, который слушает Keycloak, менять необязательно, но если хочется, то можно.

initial_hosts=»${jgroups.tcpping.initial_hosts:192.168.0.2[7600],192.168.0.3[7600],192.168.0.4[7600],192.168.0.5[7600]}» — Список серверов,которые участвуют в кластере

<FD_ALL3 stack.combine=»COMBINE» interval=»1000″ timeout=»4000″/> — interval определяет, как часто узлы опрашивают друг друга, timeout — через сколько миллисекунд сервер считается отказавшим и выкидывается из кластера.

<transport cluster=»${infinispan.cluster.name:keycloak-cluster}» — Название вашего кластера Keycloak

stack=»keycloak-stack» — Стэк, к которому привязываются параметры репликации распределенных кэшей. Должен совпадать с названием стэка, которое задается параметром stack name

node-name=»${infinispan.node.name:KeycloakNode01}» — Название ноды Keycloak, должно быть уникальным в пределах кластера, разрешается любое понятное название

У каждого распределенного кэша есть параметр owners=»X», где X > 1 и X =< N, где N количество узлов кластера

Остальные параметры на начальном этапе можно не трогать и уже править по мере необходимости.

В текущей конфигурации Keycloak выдерживает 6000 авторизаций за 2 минуты и не падает.

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

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

Если запускать с интервалом в 2 минуты, то первый запущенный сервер станет координатором, и все остальные сервера привяжутся к нему.

Настройка балансировщика для Keycloak

В качестве балансировщика я использую Nginx, поэтому приведу пример для него.

upstream keycloak {
    ip_hash;
    server 192.168.0.2:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.3:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.4:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.5:8080 max_fails=3 fail_timeout=10s;
    keepalive 4096;
}

upstream keycloak_health {
    ip_hash;
    server 192.168.0.2:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.3:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.4:8080 max_fails=3 fail_timeout=10s;
    server 192.168.0.5:8080 max_fails=3 fail_timeout=10s;
    keepalive 4096;
}


server {
    server_name keycloak_fqdn;
    include snippets/redirect2https;
}

server {
    server_name keycloak_fqdn;

    access_log /var/log/nginx/keycloak_fqdn.access.log main;
    error_log /var/log/nginx/keycloak_fqdn.error.log;

    include snippets/https;
    include ssl/ssl.cfg;
    include snippets/big-header;

    location / {
        proxy_pass http://keycloak;
        include snippets/proxy;
        include snippets/HAConfig;
    }

    location ~* /health {
        allow 192.168.0.0/24;
        deny all;
        include snippets/streamproxy;
        include snippets/HAConfig;
        proxy_pass http://keycloak_health;
    }

    location /admin/ {
        allow 192.168.0.0/24;
        deny all;
        include snippets/proxy;
        proxy_pass http://keycloak;
        include snippets/HAConfig;
    }

    location /realms/master/ {
        allow 192.168.0.0/24;
        deny all;
        include snippets/proxy;
        include snippets/HAConfig;
        proxy_pass http://keycloak;
    }

}

Параметр ip_hash нужен, чтобы запросы от одного пользователя шли на конкретный сервер Keycloak, поскольку кэши синхронизируются не так быстро, как идут запросы и есть риск получить 403 из-за несовпадения токенов, поскольку запрос на аутентификацию пришел на первый сервер, а запрос на получение токена на второй, а второй сервер еще не знает, что клиент запросил аутентификацию

В HAConfig указаны параметры, чтобы Nginx перенаправил запрос на другой сервер, если первый не ответил, при этом не отображая пользователю ошибку

proxy_connect_timeout 3s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 non_idempotent;
proxy_next_upstream_timeout 15s;
proxy_next_upstream_tries 2;

В snippets/proxy указаны дополнительные параметры

proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Remote-Port $remote_port;
proxy_set_header X-Url-Scheme $scheme;

Необходимо, чтобы параметры

proxy_http_version 1.1;
proxy_set_header Connection "";

обязательно присутствовали, если на реверс-прокси у вас есть сайты, использующие веб-сокеты.

В snippets/big-header указываются параметры для хэдеров, поскольку некоторые сайты при авторизации могут быть выставлять огромные куки для авторизации.

proxy_buffer_size          256k;
proxy_buffers              8 256k;
proxy_busy_buffers_size    256k;
large_client_header_buffers 4 64k;

Нюансы работы кластера Keycloak

В кластере Keycloak все узлы равнозначны, но всегда есть координатор, который управляет работой кластера.

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

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

grep "Received new cluster view for channel keycloak-cluster" /var/log/keycloak.log | tail -n1 | sed -E 's/.*\[(.*)\].*/\1/' | cut -d',' -f1 | xargs

Команда выдаст имя текущего координатора кластера, его и необходимо перезапускать в последнюю очередь.

Оставьте комментарий