在 Kubernetes 上以 HA 模式运行 Keycloak

在 Kubernetes 上以 HA 模式运行 Keycloak

TL博士:会有Keycloak这个开源门禁系统的介绍,分析内部结构,配置细节。

简介和主要思想

在本文中,我们将了解在 Kubernetes 上部署 Keycloak 集群时要记住的基本思想。

如果您想了解更多关于Keycloak的信息,请参考文末的链接。 为了更加沉浸于实践,你可以学习 我们的存储库 具有实现本文主要思想的模块(启动指南在那里,本文将提供设备和设置的概述, 约译者).

Keycloak 是一个用 Java 编写的综合系统,构建在应用程序服务器之上 野蝇。 简而言之,它是一个授权框架,为应用程序用户提供联合和 SSO(单点登录)功能。

我们邀请您阅读官方 现场 или 维基百科 以便详细了解。

启动Keycloak

Keycloak 需要两个持久数据源才能运行:

  • 用于存储已建立数据的数据库,例如用户信息
  • Datagrid缓存,用于缓存数据库中的数据,以及存储一些短暂且频繁变化的元数据,例如用户会话。 实施的 英飞凌,通常比数据库快得多。 但无论如何,Infinispan 中保存的数据都是短暂的——并且在集群重新启动时不需要将其保存在任何地方。

Keycloak 有四种不同的工作模式:

  • 正常 - 唯一一个进程,通过文件配置 独立文件
  • 规则簇 (高可用性选项)-所有进程必须使用相同的配置,并且必须手动同步。 设置存储在文件中 独立-ha.xml,此外您还需要对数据库和负载均衡器进行共享访问。
  • 域集群 - 随着集群的增长,以正常模式启动集群很快就会成为例行公事且无聊的任务,因为每次配置更改时,都必须在每个集群节点上进行所有更改。 域操作模式通过设置一些共享存储位置并发布配置来解决此问题。 这些设置存储在文件中 域名.xml
  • 数据中心之间的复制 — 如果您想在多个数据中心的集群中运行 Keycloak,通常是在不同的地理位置。 在此选项中,每个数据中心将拥有自己的 Keycloak 服务器集群。

在本文中,我们将详细考虑第二种选择,即 规则簇,我们还将稍微讨论一下数据中心之间的复制主题,因为在 Kubernetes 中运行这两个选项是有意义的。 幸运的是,在 Kubernetes 中同步几个 pod(Keycloak 节点)的设置是没有问题的,所以 域簇 做起来不会很难。

另请注意这个词 本文的其余部分仅适用于一组协同工作的 Keycloak 节点,无需提及 Kubernetes 集群。

常规Keycloak簇

要在此模式下运行 Keycloak,您需要:

  • 配置外部共享数据库
  • 安装负载均衡器
  • 拥有支持 IP 多播的内部网络

我们不会讨论设置外部数据库,因为这不是本文的目的。 假设某个地方有一个正在工作的数据库 - 并且我们有一个到它的连接点。 我们只需将此数据添加到我们的环境变量中即可。

为了更好地了解 Keycloak 在故障转移 (HA) 集群中的工作原理,了解它在多大程度上依赖于 Wildfly 的集群功能非常重要。

Wildfly 使用多个子系统,其中一些用作负载平衡器,一些用于容错。 负载均衡器在集群节点过载时确保应用程序可用性,而容错器即使在某些集群节点发生故障时也确保应用程序可用性。 其中一些子系统:

  • mod_cluster:与 Apache 一起作为 HTTP 负载均衡器工作,默认依赖 TCP 多播来查找主机。 可以用外部平衡器代替。

  • infinispan:使用 JGroups 通道作为传输层的分布式缓存。 此外,它还可以使用 HotRod 协议与外部 Infinispan 集群通信以同步缓存内容。

  • jgroups:基于JGroups通道为高可用服务提供群组通信支持。 命名管道允许集群中的应用程序实例连接成组,以便通信具有可靠性、有序性和对故障的敏感性等属性。

负载均衡器

在 Kubernetes 集群中安装平衡器作为入口控制器时,请务必记住以下事项:

Keycloak假设通过HTTP连接到认证服务器的客户端的远程地址是客户端计算机的真实IP地址。 平衡器和入口设置应正确设置 HTTP 标头 X-Forwarded-For и X-Forwarded-Proto,并保存原来的标题 HOST。 最新版本 ingress-nginx (>0.22.0) 默认情况下禁用此功能

激活标志 proxy-address-forwarding 通过设置环境变量 PROXY_ADDRESS_FORWARDING в true 让 Keycloak 知道它在代理后面工作。

您还需要启用 粘性会话 在入口处。 Keycloak 使用分布式 Infinispan 缓存来存储与当前身份验证会话和用户会话相关的数据。 默认情况下,缓存由单个所有者运行,换句话说,该特定会话存储在集群中的某个节点上,其他节点如果需要访问该会话,则必须远程查询它。

具体来说,与文档相反,附加名为 cookie 的会话对我们来说不起作用 AUTH_SESSION_ID。 Keycloak 有一个重定向循环,因此我们建议为粘性会话选择不同的 cookie 名称。

Keycloak 还附加了首先响应的节点的名称 AUTH_SESSION_ID,并且由于高可用版本中的每个节点都使用相同的数据库,因此每个节点 必须有 用于管理事务的单独且唯一的节点标识符。 建议放入 JAVA_OPTS 选项 jboss.node.name и jboss.tx.node.id 每个节点都是唯一的 - 例如,您可以输入 pod 的名称。 如果您输入 pod 名称,请不要忘记 jboss 变量的 23 个字符限制,因此最好使用 StatefulSet 而不是 Deployment。

另一个 rake - 如果 pod 被删除或重新启动,其缓存就会丢失。 考虑到这一点,值得将所有缓存的缓存所有者数量设置为至少两个,以便保留缓存的副本。 解决方案是运行 野蝇脚本 启动pod时,将其放入目录中 /opt/jboss/startup-scripts 在容器中:

脚本内容

embed-server --server-config=standalone-ha.xml --std-out=echo
batch

echo * Setting CACHE_OWNERS to "${env.CACHE_OWNERS}" in all cache-containers

/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})

run-batch
stop-embedded-server

然后设置环境变量的值 CACHE_OWNERS 到所需要的。

支持 IP 多播的专用网络

如果您使用 Wea​​venet 作为 CNI,多播将立即起作用 - 并且您的 Keycloak 节点一启动就会看到彼此。

如果您的 Kubernetes 集群中没有 ip 多播支持,您可以将 JGroups 配置为与其他协议一起查找节点。

第一个选项是使用 KUBE_DNS它使用 headless service 要查找 Keycloak 节点,您只需向 JGroups 传递将用于查找节点的服务的名称即可。

另一种选择是使用该方法 KUBE_PING,配合API搜索节点(需要配置 serviceAccount 有权利 list и get,然后配置 pod 以使用它 serviceAccount).

JGroups查找节点的方式是通过设置环境变量来配置的 JGROUPS_DISCOVERY_PROTOCOL и JGROUPS_DISCOVERY_PROPERTIES。 为 KUBE_PING 您需要通过询问来选择 Pod namespace и labels.

️ 如果您使用多播并在一个 Kubernetes 集群中运行两个或多个 Keycloak 集群(假设一个在命名空间中) production, 第二 - staging) - 一个 Keycloak 集群的节点可以加入另一集群。 确保通过设置变量为每个集群使用唯一的多播地址jboss.default.multicast.address и jboss.modcluster.multicast.address в JAVA_OPTS.

数据中心之间的复制

在 Kubernetes 上以 HA 模式运行 Keycloak

链接

Keycloak为Keycloak节点组成的Keycloack集群所在的每个数据中心使用多个独立的Infinispan缓存集群。 但不同数据中心的Keycloak节点之间没有区别。

Keycloak 节点使用外部 Java 数据网格(Infinispan 服务器)进行数据中心之间的通信。 通信按照协议进行 Infinispan 热棒.

Infinispan 缓存必须配置以下属性 remoteStore,以便数据可以远程存储(在另一个数据中心, 约译者)缓存。 JDG服务器之间有独立的infinispan集群,以便现场数据存储在JDG1上 site1 将在现场复制到JDG2 site2.

最后,接收 JDG 服务器通过客户端连接通知其集群的 Keycloak 服务器,这是 HotRod 协议的一个功能。 Keycloak 节点开启 site2 更新其 Infinispan 缓存,并且特定用户会话也可以在 Keycloak 节点上使用 site2.

对于某些缓存,也可以不进行备份并完全避免通过Infinispan服务器写入数据。 为此,您需要删除该设置 remote-store 特定的 Infinispan 缓存(在文件中 独立-ha.xml),之后是一些具体的 replicated-cache Infinispan 服务器端也不再需要。

设置缓存

Keycloak中有两种类型的缓存:

  • 当地的。 它位于数据库旁边,用于减少数据库的负载,并减少响应延迟。 这种类型的缓存存储领域、客户端、角色和用户元数据。 即使缓存是 Keycloak 集群的一部分,这种类型的缓存也不会被复制。 如果缓存中的条目发生更改,则有关更改的消息将发送到集群中的其余服务器,之后该条目将从缓存中排除。 参见说明 work 有关该过程的更详细说明,请参阅下文。

  • 复制。 处理用户会话、脱机令牌,还监视登录错误以检测密码网络钓鱼尝试和其他攻击。 这些缓存中存储的数据是临时的,仅存储在 RAM 中,但可以跨集群复制。

Infinispan 缓存

会议 - Keycloak 中的一个概念,单独的缓存称为 authenticationSessions,用于存储特定用户的数据。 来自这些缓存的请求通常是浏览器和 Keycloak 服务器需要的,而不是应用程序需要的。 这就是对粘性会话的依赖发挥作用的地方,即使在主动-主动模式下,此类缓存本身也不需要复制。

行动代币。 另一个概念,通常用于各种场景,例如,用户必须通过邮件异步执行某些操作。 例如,在手术过程中 forget password 高速缓存 actionTokens 用于跟踪关联令牌的元数据 - 例如,令牌已被使用且无法再次激活。 这种类型的缓存通常需要在数据中心之间复制。

存储数据的缓存和老化 旨在减轻数据库的负载。 这种缓存提高了性能,但也带来了一个明显的问题。 如果一台 Keycloak 服务器更新数据,则必须通知其他服务器,以便它们更新缓存中的数据。 Keycloak使用本地缓存 realms, users и authorization 用于缓存数据库中的数据。

还有一个单独的缓存 work,在所有数据中心复制。 它本身不存储数据库中的任何数据,而是用于向数据中心之间的集群节点发送有关数据老化的消息。 换句话说,一旦数据更新,Keycloak节点就会向其数据中心内的其他节点以及其他数据中心内的节点发送消息。 各节点收到该消息后,清除本地缓存中相应的数据。

用户会话。 带有名称的缓存 sessions, clientSessions, offlineSessions и offlineClientSessions,通常在数据中心之间复制,并用于存储有关用户在浏览器中处于活动状态时活动的用户会话的数据。 这些缓存与处理来自最终用户的 HTTP 请求的应用程序一起工作,因此它们与粘性会话相关联,并且必须在数据中心之间进行复制。

暴力保护。 缓存 loginFailures 用于跟踪登录错误数据,例如用户输入错误密码的次数。 复制此缓存是管理员的责任。 但为了准确计算,值得激活数据中心之间的复制。 但另一方面,如果不复制这些数据,则会提高性能,并且如果出现此问题,复制可能不会被激活。

部署 Infinispan 集群时,您需要将缓存定义添加到设置文件中:

<replicated-cache-configuration name="keycloak-sessions" mode="ASYNC" start="EAGER" batching="false">
</replicated-cache-configuration>

<replicated-cache name="work" configuration="keycloak-sessions" />
<replicated-cache name="sessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineSessions" configuration="keycloak-sessions" />
<replicated-cache name="actionTokens" configuration="keycloak-sessions" />
<replicated-cache name="loginFailures" configuration="keycloak-sessions" />
<replicated-cache name="clientSessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineClientSessions" configuration="keycloak-sessions" />

您必须在启动 Keycloak 集群之前配置并启动 Infinispan 集群

然后你需要配置 remoteStore 用于Keycloak缓存。 为此,一个脚本就足够了,其操作与上一个类似,用于设置变量 CACHE_OWNERS,需要将其保存到文件并放入目录中 /opt/jboss/startup-scripts:

脚本内容

embed-server --server-config=standalone-ha.xml --std-out=echo
batch

echo *** Update infinispan subsystem ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)

echo ** Add remote socket binding to infinispan server **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})

echo ** Update replicated-cache work element **
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=work, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)

/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache sessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=sessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache offlineSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=offlineSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache clientSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=clientSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache offlineClientSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=offlineClientSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache loginFailures element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=loginFailures, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache actionTokens element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    cache=actionTokens, 
    remote-servers=["remote-cache"], 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache authenticationSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)

echo *** Update undertow subsystem ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)

run-batch
stop-embedded-server

不要忘记安装 JAVA_OPTS 对于运行 HotRod 的 Keycloak 节点: remote.cache.host, remote.cache.port 和服务名称 jboss.site.name.

链接和附加文档

该文章由员工翻译并为 Habr 准备 斯莱姆培训中心 — 来自实践专家(Kubernetes、DevOps、Docker、Ansible、Ceph、SRE)的强化课程、视频课程和企业培训

来源: habr.com

添加评论