background
前任者が残した keycloak が kubernetes の上で動いていました。セッションを共有できないため 1 pod で運用しています。pod を止めずに kubernetes version up ができるように冗長化しました。
issue
デフォルト設定のkeycloakはセッションをメモリに保存します。そのため keycloak を 2 pods 以上起動すると、pod 間でのセッション不整合が起こります。あなたが keycloakにログインしたとき、 pod-A に接続されていたとします。ログイン後の画面遷移で表示されるのが pod-B だった場合、pod-Bはあなたのセッション情報をメモリに持っていないため、まだログイン前のユーザだとしてログイン画面を表示します。こんな感じで、二回に一回は思った画面が表示するけど・・みたいな謎の挙動をするようになります。
Workaround
podsへのアクセスを振り分けるLoad balancerでsticky sessionを利用すれば、あなたのリクエストを随時pod-Aに振り分けるため一見問題なくなったように見えます。しかし、pod-Aがなんらかの原因でstopした場合、あなたはpod-Bを利用することになります。pod-Bのメモリにはあなたのセッションがありませんから、ログインからやり直すことになります。これは冗長化というよりは、cold standby があるようなものです。
分散キャッシュ
pod A, B が同じメモリを持てば問題は解決します。それが keycloak では分散キャッシュ(公式には Distributed Caches)と呼ばれる機能です。
keycloak はデフォルトで infinispan を使った分散キャッシュに対応しています。infinispan は Redis, etcd, memcached のような高速なインメモリストレージです。kubernetesに対応している点、Javaの組み込みライブラリとして利用できる点が特徴(のよう)です。
*著者は keycloak もろくに理解していませんし infinispan を目にするのはこれが初めてです
注意点として、infinispanは基本的に UDP で動作しますが kubernetes network では UDP が使えないので TCP で動作するように設定を変える必要があります。この後のセクションで説明します。
How to set up Keycloak x Infinispan
Docker Image
Versionは最新の 23.0.7 を利用しました。これ自体に inifispan に対応した keycloak が入ってます。
Docker image
FROM quay.io/keycloak/keycloak:23.0.7-0
*infinispanは独立したprocessとして稼働するわけではないようです。keycloakのプロセス内でinfinispanの組み込み関数を利用してinfinispanを動作させているみたい。
keycloakが古すぎる(特にv19以前のwildflyの方)はupgradeしましょう。こちらに解説があります。
Deployement
環境変数でkeycloakのinfinispanを有効にします。
- KC_CACHE
-
ispn
にすることで infinispan を使います。
-
- KC_CACHE_STACK
-
kubernetes
にすることで inifispan を k8s 用の動作モードにします。- おそらく UDP ではなく TCP で動くようにする、また cluster nodesのdiscoveryを k8s の DNS を使ってするようにするんだと思います
-
- JAVA_OPTS_APPEND
- これが重要です。infinispan の node discoveryに使う FQDN を
jgroups.dns.query
に指定します。 k8s の特殊なリソースである headless-service のFQDNを指定する必要があります。後述します。
- これが重要です。infinispan の node discoveryに使う FQDN を
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: keycloak
name: keycloak
namespace: keycloak
spec:
replicas: 2
selector:
matchLabels:
app: keycloak
strategy:
rollingUpdate:
maxSurge: 2
maxUnavailable: 0
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:23.0.7-0
command:
- /opt/keycloak/bin/kc.sh
- --verbose
- start
env:
- name: TZ
value: Asia/Tokyo
# Infinispan 分散キャッシュ設定 -->
- name: KC_CACHE
value: ispn
- name: KC_CACHE_STACK
value: kubernetes
## **重要** ↓↓↓ これをserviceと一致する名前にすること。後述。
- name: JAVA_OPTS_APPEND
value: -Djgroups.dns.query=keycloak-infinispan-headless-service.keycloak.svc.cluster.local
# <--
# 初期設定
- name: KEYCLOAK_ADMIN
value: admin
# DB 設定
- name: KC_DB
value: mysql
- name: KC_DB_URL
value: jdbc:mysql://my-mysql.example.com:3306/keycloak
- name: KC_DB_USERNAME
value: keycloak
# Load balancer で TLS termination している時に必要な設定
- name: KC_PROXY
value: edge
- name: KC_PROXY_ADDRESS_FORWARDING
value: "true"
- name: KC_HOSTNAME_STRICT
value: "false"
- name: KC_HOSTNAME_STRICT_HTTPS
value: "false"
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_HTTP_PORT
value: "8080"
envFrom:
- secretRef:
name: keycloak-secret # KC_DB_PASSWORD を読み込む
ports:
- containerPort: 8080
name: http
readinessProbe:
httpGet:
path: /realms/master
port: 8080
Services
Infinispan用の headless service
これが大事です。普通の k8s serviceは通信をpodに繋げるためにありますが、これは違います。infinispanに、ns内に存在するpodを教えてあげるためだけに作ります。
---
apiVersion: v1
kind: Service
metadata:
labels:
app: keycloak
name: keycloak-infinispan-headless-service # <---- JAVA_OPTS_APPENDに入れるprefixと一致すること。
namespace: keycloak
spec:
clusterIP: None # <--------- 絶対 `None`。省略禁止。
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: keycloak
大事なポイント
-
name: keycloak-infinispan-headless-service
- k8sではserviceの名前を使ってFQDNを作り名前解決に利用します。
-
<service-name>
.<namespace-name>
.svc.cluster.local というルールです - そのFQDNを deployment の
JAVA_OPTS_APPEND
に入れます
-
clusterIP: None
- ここでやりたいのは、
keycloak-infinispan-headless-service.keycloak.svc.cluster.local
に名前解決をすると 全ての pod の IP address(A record) を返却する DNS サーバを作ることです。 - 設定で言うと
dnsAllPods: true
みたいなサービスを作りたいわけです(そんなオプションはありませんが) -
clusterIP: None
にすると、そういうサービスを作ることができます。これをheadless service
と呼びます。 - 詳しくはこちらの記事に書きました
- ここでやりたいのは、
通信用の service & ingress
これはごく普通のものです。podの8080に通信を繋げます。 clusterIP: None
がないので、この service は名前解決を依頼された時にpodのIP addressesではなく service のIPを返します。そのため、このserviceは infinispan の cluster node discovery には使えません。
---
apiVersion: v1
kind: Service
metadata:
labels:
app: keycloak
name: keycloak
namespace: keycloak
spec:
type: ClusterIP
ports:
- name: http
port: 8080
selector:
app: keycloak
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/backend-protocol: HTTP
nginx.ingress.kubernetes.io/configuration-snippet: |
if ($http_x_forwarded_proto = 'http') {
return 302 https://$host$request_uri;
}
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
name: keycloak
namespace: keycloak
spec:
rules:
- host: my-keycloak.example.com
http:
paths:
- backend:
service:
name: keycloak
port:
number: 8080
path: /
pathType: Prefix
起動
1 つ目の pod log
keycloakを起動して、infinispan の接続に成功すると keycloak-cache-init
がついた log が出ます。意味は調べていませんが、成功例として記録します
Starting user marshaller
この log を起点として infinispan の処理が始まるようです。
(keycloak-cache-init) server listening on *.57800
infinispanが起動したようです。
no members discovered after 2003 ms: creating cluster as coordinator
既存の infinispan clusterが見つからないため、自身が coordinator (おそらくmaster nodeのようなもの) として起動したようです。
Received new cluster view for channel ISPN:
clusterの情報が表示されます。よく見るとこの文言が複数回logに出ています。これはclusterのnodesが変わった時に出るようです。読み方は後述。
Unable to persist Infinispan internal caches as no global state enabled
これは気にしなくていいエラーです。podが全部死んでもインメモリのsessionを保存しておきたいときにpersist cacheを使いますが、今回は不要なので使っていません。(その場合はログインし直してもらえばok)
2024-03-04 12:57:45,352 WARN [org.infinispan.PERSISTENCE] (keycloak-cache-init) ISPN000554: jboss-marshalling is deprecated and planned for removal
2024-03-04 12:57:45,508 INFO [org.infinispan.CONTAINER] (keycloak-cache-init) ISPN000556: Starting user marshaller 'org.infinispan.jboss.marshalling.core.JBossUserMarshaller'
2024-03-04 12:57:46,364 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000078: Starting JGroups channel `ISPN` with stack `kubernetes`
2024-03-04 12:57:46,367 INFO [org.jgroups.JChannel] (keycloak-cache-init) local_addr: 7da7c155-16be-45b9-b3e0-d64de166f6c0, name: keycloak-7c7d6ddd84-2lk9h-42677
2024-03-04 12:57:46,388 INFO [org.jgroups.protocols.FD_SOCK2] (keycloak-cache-init) server listening on *.57800
2024-03-04 12:57:48,404 INFO [org.jgroups.protocols.pbcast.GMS] (keycloak-cache-init) keycloak-7c7d6ddd84-2lk9h-42677: no members discovered after 2003 ms: creating cluster as coordinator
2024-03-04 12:57:48,420 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000094: Received new cluster view for channel ISPN: [keycloak-7c7d6ddd84-2lk9h-42677|0] (1) [keycloak-7c7d6ddd84-2lk9h-42677]
2024-03-04 12:57:48,582 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000079: Channel `ISPN` local address is `keycloak-7c7d6ddd84-2lk9h-42677`, physical addresses are `[172.18.3.53:7800]`
2024-03-04 12:57:48,597 WARN [org.infinispan.CONFIG] (keycloak-cache-init) ISPN000569: Unable to persist Infinispan internal caches as no global state enabled
...
2024-03-04 12:59:54,798 INFO [org.infinispan.CLUSTER] (jgroups-16,keycloak-7c7d6ddd84-2lk9h-42677) ISPN000094: Received new cluster view for channel ISPN: [keycloak-7c7d6ddd84-2lk9h-42677|4] (1) [keycloak-7c7d6ddd84-2lk9h-42677]
2024-03-04 13:00:18,029 INFO [org.infinispan.CLUSTER] (jgroups-16,keycloak-7c7d6ddd84-2lk9h-42677) ISPN000094: Received new cluster view for channel ISPN: [keycloak-7c7d6ddd84-2lk9h-42677|5] (2) [keycloak-7c7d6ddd84-2lk9h-42677, keycloak-7c7d6ddd84-b2l59-34175]
2 つめの pod log
後から clusterにjoinする pod は少しlogが変わります。
server listening on *.57800
ここまでは同じです。
Received new cluster view for channel ISPN:
その後、 no members...
が出ずにすぐに cluster 情報を recieveしています。これで joinが完了しています。
2024-03-04 13:00:16,977 WARN [org.infinispan.PERSISTENCE] (keycloak-cache-init) ISPN000554: jboss-marshalling is deprecated and planned for removal
2024-03-04 13:00:17,092 INFO [org.infinispan.CONTAINER] (keycloak-cache-init) ISPN000556: Starting user marshaller 'org.infinispan.jboss.marshalling.core.JBossUserMarshaller'
2024-03-04 13:00:17,793 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000078: Starting JGroups channel `ISPN` with stack `kubernetes`
2024-03-04 13:00:17,798 INFO [org.jgroups.JChannel] (keycloak-cache-init) local_addr: c8eef3e0-3c27-496f-b6d2-b225981142c9, name: keycloak-7c7d6ddd84-b2l59-34175
2024-03-04 13:00:17,815 INFO [org.jgroups.protocols.FD_SOCK2] (keycloak-cache-init) server listening on *.57800
2024-03-04 13:00:18,109 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000094: Received new cluster view for channel ISPN: [keycloak-7c7d6ddd84-2lk9h-42677|5] (2) [keycloak-7c7d6ddd84-2lk9h-42677, keycloak-7c7d6ddd84-b2l59-34175]
2024-03-04 13:00:18,313 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000079: Channel `ISPN` local address is `keycloak-7c7d6ddd84-b2l59-34175`, physical addresses are `[172.18.5.221:7800]`
2024-03-04 13:00:18,390 WARN [org.infinispan.CONFIG] (keycloak-cache-init) ISPN000569: Unable to persist Infinispan internal caches as no global state enabled
Received new cluster view for channel ISPN:
の読み方
眺めただけでなんとなくlogの意味がわかった気がします。
[keycloak-7c7d6ddd84-2lk9h-42677|0] (1) [keycloak-7c7d6ddd84-2lk9h-42677]
この部分はおそらくこう言う意味だと想像します。
[
cluster-name
|state-version
] (number of nodes
) [node-name1
,node-name2
...]
- cluster-name
- 最初にcoordinatorになったpodのhostnameが入ると想像します。
- state-version
- nodeの増減など何か変化があるたびにincrementされるような気がします
- number of nodes
- そのclusterで稼働しているnodeの数
- node-name1, 2...
- clusterに参加しているnodeのhostname list
- pod-A を起動
- pod-A:
[keycloak-7c7d6ddd84-2lk9h-42677|0] (1) [keycloak-7c7d6ddd84-2lk9h-42677]
- nodeがpod-A自身の一台しかいません。
- pod-A:
- pod-Bを起動
- pod-B:
[keycloak-7c7d6ddd84-2lk9h-42677|1] (2) [keycloak-7c7d6ddd84-2lk9h-42677, keycloak-7c7d6ddd84-b2l59-34175]
- pod-Bのnodeが増えて、合計
(2)
台になりました。
- pod-Bのnodeが増えて、合計
- pod-B:
- pod-A がpod-Bの増加を認識
- pod-A:
[keycloak-7c7d6ddd84-2lk9h-42677|1] (2) [keycloak-7c7d6ddd84-2lk9h-42677, keycloak-7c7d6ddd84-b2l59-34175]
- このログは "2." と全く同じ内容です。
- pod-A:
- pod-B を削除すると、pod-Aがnode減少を認識する
- pod-A:
[keycloak-7c7d6ddd84-2lk9h-42677|2] (1) [keycloak-7c7d6ddd84-2lk9h-42677]
- 合計
(1)
台になりました。
- 合計
- pod-A:
上手く動かない場合
headless serviceの設定不足でハマりましたが、特にエラーログは出ませんでした。それぞれのpodが独自にclusterを構築して、一見すると動作しているように見えるためです。Received new cluster view for channel ISPN
で (2)
以上の値が出ない、または異なる cluster-name が表示されていれば、分散キャッシュは動いていないと思っていいと思います。
headless-serviceの名前解決ができていない場合は keycloak podにdebug containerをつけて、 host keycloak-infinispan-headless-service.keycloak.svc.cluster.local
を叩いてみましょう。podのAレコードが返ってきなければそこが原因です。
infinispanが動く場合は、dig/nslookup/hostなどの名前解決でこのように全てのpodが返却されます。
e.g. 5 podsある場合
[app-5dfd9989bc-6fl2q] $ host your-headless-service.your-namespace.svc.cluster.local
your-headless-service.your-namespace.svc.cluster.local has address 172.18.5.211
your-headless-service.your-namespace.svc.cluster.local has address 172.18.4.253
your-headless-service.your-namespace.svc.cluster.local has address 172.18.3.35
your-headless-service.your-namespace.svc.cluster.local has address 172.18.5.212
your-headless-service.your-namespace.svc.cluster.local has address 172.18.3.36
$ dig a +short keycloak-infinispan-headless-service.keycloak.svc.cluster.local
172.18.5.211
172.18.4.253
172.18.3.35
172.18.5.212
172.18.3.36
よもやま話
headless-serviceの意味がわからない頃に一度挫折したんですがやっとできた感じです。
https://qiita.com/uturned0/items/e9256c48ccba6f588d79
その頃、tcpdumpでadverting してるpacketを探してたんですが全然パケットが見つからなくて困ってました。7800/tcpだったと思うんですがそこで起動してるはずなのに全然packet飛んでる気配がなかった。あれは今思えば、infinispanがdnsを見て特定のip addressだけに飛ばす仕組みだったからなんでしょうね。てっきり VRRP の multicast みたい に network broadcast address とかで discovery すると思ってたので k8s がそれをサポートしていないことも勉強になったし、無駄な通信を省いてる昨今は進化してるなぁと感想を抱きました
そして主にsecurity対策だったと言う学びもありました
時間があれば inifinispanのadvertisingなどを観測したかったんですが今回はここまで。とりあえず動いてよかった。
k8s勉強しないとなぁ。こうやって一つずつ覚えていきましょう。