LabHub

ブログ

Keycloak Kubernetes HA 運用 — Infinispan クラスタリングと無停止デプロイ

한국어English日本語中文

はじめに

SSO サーバーは組織のすべてのサービスが依存する単一の入り口です。Keycloak が落ちればログインが止まり、ログインが止まれば事実上の全社障害です。だからこそ Keycloak 運用の中心課題は常に高可用性(HA)でした。

幸い、2026 年の Keycloak 26.x は HA 運用の難易度を大きく下げました。persistent user sessions がデフォルトになったことで「再起動すると全ユーザーがログアウト」という問題が消え、26.6 の zero-downtime rolling patch によりパッチアップグレード時の無停止デプロイが公式にサポートされます。本記事では Kubernetes 上で Keycloak HA クラスタを構築・運用する全工程を扱います。

デプロイ方式の選択 — Operator vs Helm

Kubernetes に Keycloak を載せる代表的な方法は、公式 Operator とコミュニティ Helm チャート(主に Bitnami または codecentric)です。

項目Keycloak Operator (公式)Helm チャート (コミュニティ)
メンテナンス主体Keycloak プロジェクト公式コミュニティ/ベンダー
抽象化レベルKeycloak CR で宣言values.yaml で詳細制御
26.6 rolling patch 自動化サポート (update strategy)手動構成が必要
realm importKeycloakRealmImport CR初期化スクリプト
カスタムイメージサポート (推奨パターン)サポート
細かい Pod 制御限定的 (podTemplate で補完)自由
推奨対象標準構成、運用自動化重視非標準トポロジ、既存 Helm パイプライン

新規構築なら公式 Operator を推奨します。バージョンアップグレードの自動化と 26.6 の無停止パッチ戦略が Operator に組み込まれているからです。Operator のインストールは次のとおりです。

kubectl create namespace keycloak
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/kubernetes.yml

Keycloak CR の実践例です。

apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
  name: keycloak
  namespace: keycloak
spec:
  instances: 3
  image: registry.example.com/idp/keycloak-custom:26.6.2
  startOptimized: true
  db:
    vendor: postgres
    host: keycloak-db.database.svc.cluster.local
    port: 5432
    database: keycloak
    usernameSecret:
      name: keycloak-db-secret
      key: username
    passwordSecret:
      name: keycloak-db-secret
      key: password
    poolMinSize: 10
    poolInitialSize: 10
    poolMaxSize: 30
  hostname:
    hostname: sso.example.com
    strict: true
  http:
    httpEnabled: true
  proxy:
    headers: xforwarded
  additionalOptions:
    - name: log-console-output
      value: json
    - name: event-metrics-user-enabled
      value: "true"
  resources:
    requests:
      cpu: "1"
      memory: 1500Mi
    limits:
      memory: 3Gi
  update:
    strategy: Auto

Infinispan キャッシュ構造 — セッションと認証状態の保管庫

Keycloak クラスタの心臓部は組み込みの Infinispan キャッシュです。ノード間の状態共有はすべてここで行われます。主要キャッシュを分類すると次のとおりです。

キャッシュ名種類用途26+ のデフォルト動作
realms, userslocalDB エンティティの読み取りキャッシュノードごとにローカル、invalidation メッセージで同期
authorizationlocal認可ポリシーキャッシュノードごとにローカル
sessions, clientSessionsdistributedログインセッションDB 永続化 + キャッシュ
offlineSessionsdistributedオフラインセッションDB 永続化 + キャッシュ
authenticationSessionsdistributed進行中の認証 (ログインフォーム段階)クラスタ分散
loginFailuresdistributedブルートフォースカウンタクラスタ分散
workreplicatedノード間 invalidation の伝播全ノード複製
actionTokensdistributedメールリンクなどワンタイムトークンクラスタ分散

構造を図で見ると次のようになります。

        +-----------------+   +-----------------+   +-----------------+
        |  Keycloak Pod 1 |   |  Keycloak Pod 2 |   |  Keycloak Pod 3 |
        |                 |   |                 |   |                 |
        | local: realms,  |   | local: realms,  |   | local: realms,  |
        |        users    |   |        users    |   |        users    |
        |                 |   |                 |   |                 |
        | distributed:    |   | distributed:    |   | distributed:    |
        |  sessions(o2)  <----> sessions(o2)   <----> sessions(o2)    |
        |  authSessions   |   |  authSessions   |   |  authSessions   |
        |                 |   |                 |   |                 |
        | replicated:     |   | replicated:     |   | replicated:     |
        |  work          <----> work           <----> work            |
        +--------+--------+   +--------+--------+   +--------+--------+
                 |                     |                     |
                 +----------+----------+----------+----------+
                            |   JGroups (gossip)  |
                            v                     v
                     +-------------+      +--------------+
                     | PostgreSQL  |      | DNS headless |
                     | (セッション  |      | service      |
                     |  永続化)     |      | (DNS_PING)   |
                     +-------------+      +--------------+

distributed キャッシュはデフォルトで owners 数が 2 で、一つのエントリが二つのノードに複製されます。つまりノードが一台落ちてもセッションデータは生きています。同時に二台失うとキャッシュ上のデータは失われる可能性がありますが、26 からはセッションが DB にも永続化されるため復旧が可能です。

JGroups DNS_PING — Kubernetes でのノードディスカバリ

Infinispan のクラスタメンバーシップは JGroups が担当します。Kubernetes ではマルチキャストが塞がれているため、DNS ベースのディスカバリ(DNS_PING)を使用します。動作原理はシンプルです。

  1. headless Service がすべての Keycloak Pod の IP を DNS A レコードとして公開
  2. 各ノードが起動時にその DNS 名を照会してピア一覧を取得
  3. JGroups が 7800 ポートでクラスタを形成

Operator を使えば自動構成されますが、手動で構成する場合は次のとおりです。

apiVersion: v1
kind: Service
metadata:
  name: keycloak-discovery
  namespace: keycloak
spec:
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: keycloak
  ports:
    - name: jgroups
      port: 7800
      targetPort: 7800
# Keycloak 起動オプション (StatefulSet/Deployment の環境変数として)
KC_CACHE=ispn
KC_CACHE_STACK=kubernetes
JAVA_OPTS_APPEND=-Djgroups.dns.query=keycloak-discovery.keycloak.svc.cluster.local

publishNotReadyAddresses を有効にする理由は、Pod が readiness 前の段階でもクラスタに参加しなければ、起動中のセッションリバランシングが正常に動作しないためです。クラスタ形成の確認はログで行います。

kubectl logs -n keycloak keycloak-0 | grep "ISPN000094"
# ISPN000094: Received new cluster view ... (3) [keycloak-0-..., keycloak-1-..., keycloak-2-...]

26.x からは JGroups トラフィックの TLS 暗号化がデフォルトで有効になり(Operator デプロイ時)、ノード間のセッションデータが平文で流れることはありません。

Persistent User Sessions — 26 のゲームチェンジャー

Keycloak 24 までオンラインセッションは純粋なインメモリ(Infinispan)でした。全体再起動や複数ノードの同時障害ですべてのユーザーがログアウトされる構造でした。Keycloak 26 から persistent-user-sessions 機能がデフォルトで有効になり、次のことが変わりました。

代償は DB 書き込み負荷の増加です。ログイン/ログアウト/refresh のたびに DB 書き込みが発生するため、ログイン集中シナリオ(朝 9 時の出勤時間)の DB IOPS を見積もりに反映する必要があります。無効化は可能ですが(features から除外)、26 の運用モデルは永続セッションを前提に設計されているため、特別な理由がなければデフォルトの維持を推奨します。

DB 選択とコネクションプール

項目推奨理由
DB エンジンPostgreSQL 15+公式パフォーマンステストの基準、Aurora PostgreSQL 検証済み
分離レベルREAD COMMITTEDデフォルト、変更不要
プールサイズノードあたり max 30 前後過大なプールは DB 負荷を増やすだけ
HAPatroni / RDS Multi-AZ / AuroraDB が SPOF にならないように
プール見積もりピーク同時リクエスト基準ログイン TPS x 平均クエリ数を考慮

コネクションプールの公式は単純ではありませんが、経験則として「ログイン 100 TPS あたりノードごとにプール 10-15」から始め、モニタリング(agroal メトリクス)で調整します。プールが枯渇するとログイン遅延ではなくログイン失敗に直結するため、db-pool 関連メトリクスにアラートを設定すべきです。

# Keycloak CR 内のコネクションプール設定部分
  db:
    poolMinSize: 10
    poolInitialSize: 10
    poolMaxSize: 30
  additionalOptions:
    - name: transaction-xa-enabled
      value: "false"

Sticky Session は必要か

結論から: 26 基準で必須ではありませんが、依然として有益です。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: keycloak
  namespace: keycloak
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "KC_ROUTE"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
spec:
  ingressClassName: nginx
  rules:
    - host: sso.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: keycloak-service
                port:
                  number: 8080
  tls:
    - hosts: [sso.example.com]
      secretName: sso-tls

proxy-buffer-size の拡大は実務必須の Tips です。Keycloak のレスポンスヘッダー(特にトークンを含むリダイレクト)がデフォルトバッファを超えて 502 が発生するケースがよくあります。

Multi-Site (Cross-DC) Active-Active

26.x の公式 multi-site アーキテクチャは二サイトの Active-Active をサポートします。中核となる構成要素は次のとおりです。

        Site A (eu-west-1)                  Site B (eu-central-1)
   +------------------------+         +------------------------+
   |  Keycloak (3 pods)     |         |  Keycloak (3 pods)     |
   |        |               |         |        |               |
   |  Infinispan (external) | <-----> |  Infinispan (external) |
   |  cross-site replication|  RELAY2 |  cross-site replication|
   +-----------+------------+         +-----------+------------+
               |                                  |
               +----------------+-----------------+
                                |
                     +----------v-----------+
                     |  Aurora Global DB    |
                     |  (writer: Site A)    |
                     +----------------------+
                                ^
               +----------------+----------------+
               |     Global LB (Route53 /        |
               |     ヘルスチェックベース failover)|
               +---------------------------------+

multi-site は運用の複雑さが非常に高いため、RTO/RPO 要件が本当に必要な場合にのみ導入し、その前に単一リージョン複数 AZ + 堅牢なバックアップ/リストア手順で十分かをまず検討するのがよいでしょう。詳細は公式 HA ガイドを参照してください。

26.6 Zero-Downtime Rolling Patch

26.6 以前は、どのバージョンアップグレードでもキャッシュプロトコル非互換の可能性のため、クラスタ全体を落として上げ直す recreate 戦略が基本でした。26.6 からはパッチリリース間(例: 26.6.0 から 26.6.2)の互換性が保証され、rolling update が公式サポートされます。

# Keycloak CR
spec:
  update:
    strategy: Auto   # 互換性を自動判定: 可能なら rolling、不可なら recreate

Auto 戦略の動作は次のとおりです。

  1. Operator が新しいイメージで update-compatibility 検査ジョブを実行
  2. キャッシュ/設定が互換なら Pod を一台ずつ入れ替え(無停止)
  3. 非互換なら recreate(全体再起動、persistent sessions でログインは維持)

手動で互換性を検査することもできます。

# 既存バージョンでメタデータを生成
bin/kc.sh update-compatibility metadata --file=/tmp/metadata.json

# 新バージョンで検査
bin/kc.sh update-compatibility check --file=/tmp/metadata.json
echo $?   # 0 なら rolling 可能

リソースサイジングと JVM チューニング

公式サイジングガイドに基づく出発点は次のとおりです。

負荷指標1 vCPU あたりの処理量(概算)備考
パスワードログイン毎秒 15 回前後ハッシュコスト(argon2)に大きく左右
client credentials グラント毎秒 120 回前後最も軽い処理
refresh token毎秒 120 回前後DB 書き込みを含む
メモリ(非ヒープ含む)Pod あたり 1.25-3Giセッション数より realm/クライアント数の影響大

JVM メモリは 26 からコンテナメモリベースの比率算定がデフォルトです(デフォルトヒープ 70%)。明示的に制御するには:

  additionalOptions: []
  # または環境変数で
  # JAVA_OPTS_KC_HEAP: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50"
  resources:
    requests:
      cpu: "1"
      memory: 1500Mi
    limits:
      memory: 3Gi

CPU limit は設定しないのが一般的な推奨です(スロットリングによる遅延スパイクの防止)。メモリ limit は OOMKill 防止のため、ヒープ+メタスペース+ネイティブの合算より余裕を持たせます。

ヘルスチェックと Startup Probe

Keycloak は管理ポート(デフォルト 9000)で health エンドポイントを提供します。

# Deployment/StatefulSet を直接構成する場合
livenessProbe:
  httpGet:
    path: /health/live
    port: 9000
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /health/ready
    port: 9000
  periodSeconds: 10
  failureThreshold: 3
startupProbe:
  httpGet:
    path: /health/started
    port: 9000
  periodSeconds: 5
  failureThreshold: 60   # 最大 5 分の起動猶予

さらに PodDisruptionBudget と topologySpreadConstraints は HA の基本です。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: keycloak-pdb
  namespace: keycloak
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: keycloak

障害シナリオ別の対応

シナリオ 1: ノード 1 台ダウン

シナリオ 2: DB 瞬断 (failover 30 秒)

シナリオ 3: スプリットブレイン (ネットワークパーティション)

シナリオ 4: 全体再起動 (災害復旧)

# 定期 realm export (CronJob での自動化を推奨)
bin/kc.sh export --dir /tmp/export --realm production --users different_files

シナリオ 5: ログイン集中 (出勤時間スパイク)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: keycloak-hpa
  namespace: keycloak
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: keycloak
  minReplicas: 3
  maxReplicas: 8
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

おわりに

Keycloak 26 時代の HA 運用は「Infinispan をなんとかなだめる作業」から「DB を中心に据えたごく普通のステートフルサービス運用」へとシンプルになりました。persistent user sessions と zero-downtime rolling patch がその転換点です。それでも JGroups ディスカバリ、コネクションプール見積もり、プローブチューニングといった基本は依然として運用者の仕事です。本記事の YAML 例を出発点として、必ず自分の環境の負荷テストで数値を検証してください。

次の記事では、Keycloak の機能そのものを拡張する SPI 開発(カスタム Authenticator、EventListener)を扱います。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます