Keycloakを冗長構成で動かしてみる

# 今日やること

[Keycloak by OpenStandia Advent Calendar 2017](https://qiita.com/advent-calendar/2017/keycloak-by-openstandia) 16日目の今日は Keycloakを冗長構成(クラスタ構成)にして動かしてみます。Keycloakを冗長構成にする手順は Keycloak Documentation の [Clustering](http://www.keycloak.org/docs/latest/server_installation/index.html#_clustering) を参考にしました。手順は下記の通りです。

1. 動作モード(operation mode)の選択
1. 外部共有データベースの設定
1. ロードバランサーの設定
1. マルチキャストネットワークの設定
これに加えて **「AWS EC2でのKeycloak冗長構成」** についても触れます。そして最後に動作確認をする流れで進めますので、宜しくお願い致します。 :smiley:

# Keycloakの冗長構成

## なぜ冗長構成にするのか

冗長構成にすることで以下のことが可能になります。

* サーバの負荷の分散することができます。
* セッションレプリケーションにより、片方のサーバが停止した場合でも別のサーバでセッション情報を保持することで処理を継続させることができます。

## Keycloak冗長構成の概要
<!-- ![Keycloak冗長構成.png](https://qiita-image-store.s3.amazonaws.com/0/207022/79b8ed7b-c316-4620-4e1d-6a797d883031.png) -->
![Keycloak冗長構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43869/e0e1122d-534c-fb36-2d2b-17aaa2c4869b.png)

クライアントから接続するとロードバランサーを経由して各ノードに振り分けられる構成です。**ノード間の通信は「JGroupsサブシステム」、セッションレプリケーションは「Infinispanサブシステム」が使われています。**

## JGroups
JGroupsは、ノード間の通信機能を提供します。通信には「UDP」「TCP」を選択できます。KeycloakのデフォルトはUDPマルチキャスト通信が設定されています。UDPマルチキャスト通信は各ノードにマルチキャスト通信情報（マルチキャストIPアドレスとポート）を合わせるだけで自動的に冗長構成されます。

## Infinispan

Infinispanは、キャッシュ機能を提供します。キャッシュの同期方式は一部のノードに同期する「Distributedモード」と全てのノードに同期する「Replicatedモード」があります。Keycloakのデフォルト設定は「Distributedモード」です。
「Replicatedモード」は耐障害性には優れてますが、台数が多くなると性能が低下するので注意が必要です。一方で、「Distributedモード」は一部のノードにしか同期しないのでキャッシュデータを保持していたノードが停止するとキャッシュデータを失ってしまいます。キャッシュデータを保持するために、同期するノード数を調整するなどの工夫が必要です。「Distributedモード」の設定については、[サーバキャッシュの設定](https://qiita.com/yoonis/items/4f4a9df0f6f8e858bd4a#%E3%82%B5%E3%83%BC%E3%83%90%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%AE%E8%A8%AD%E5%AE%9A)で詳しく説明します。

# Keycloakを冗長構成にしてみる

Keycloakを冗長構成に構築してみます。構築手順の中での決め事をいくつか書きます。

* Keycloakのホームディレクトリは「$KEYCLOAK_HOME」とします。
* DBサーバは「MySQL」を使用します。
* Keycloakサーバの設定変更箇所は、冗長構成する全てのサーバに反映する必要があります。

## 事前準備

冗長構成にするサーバを用意します。今回は４つのサーバを用意しました。Keycloakのサーバ構築と初期セットアップに関しましては、Keycloak by OpenStandia 2日目の [Keycloakのセットアップ](https://qiita.com/tamura__246/items/13fc301e9409fef77bf3) を確認してください。「DB」「Apache」のサーバ構築手順はここでは省略します。

|サーバ|FQDN|説明|バージョン|
|:---|:---|:---|:---|
|keycloak01|keycloak01.example.com|keycloak #1|3.3.0.CR2|
|keycloak02|keycloak02.example.com|keycloak #2|3.3.0.CR2|
|DB|keycloakdb.example.com|DB(MySQL)|5.7.17|
|Apache|keycloak.example.com|ロードバランサー|2.4.6|

## 動作モード(operation mode)の選択

はじめに、Keycloakの動作モード(operation mode)を選択します。動作モードの選択で「データベースの構成」「キャッシュの構成」「サーバの起動方法」などが影響しますので最初に決めておきます。動作モードは全部で３種類あります。今回はこの中から「Standalone Clustered Mode」を選択して構築します。

|動作モード|構成|起動スクリプト|設定ファイル|
|:---|:---|:---|:---|
|Standalone Mode|シングル構成|standalone.sh|standalone.xml|
|Standalone Clustered Mode|冗長構成|standalone.sh|standalone-ha.xml|
|Domain Clustered Mode|冗長構成|domain.sh|host-master.xml ,<br> host-slave.xml|

動作モード別に起動方法が異なりますので、機能コマンドを確認してみます。

* Standalone Mode

「Standalone Mode」の場合は、起動オプション`--server-config`を省略しても問題ありません。[Keycloakのセットアップ](https://qiita.com/tamura__246/items/13fc301e9409fef77bf3) などでのオプション無しの起動は 「Standalone Mode」で起動したことになります。

```bash
$ $KEYCLOAK_HOME/bin/standalone.sh --server-config=standalone.xml
```

* Standalone Clustered Mode

「Standalone Clustered Mode」の場合は、起動オプション`--server-config`にHA用設定ファイル（standalone-ha.xml）を指定します。

```bash
$ $KEYCLOAK_HOME/bin/standalone.sh --server-config=standalone-ha.xml
```

* Domain Mode

「Domain Mode」は上記２つと起動スクリプト、起動オプションが異なります。まだ「master」「slave」の考え方があり、「master」と「slave」で設定ファイルも異なります。

```bash
// masterの場合
$ $KEYCLOAK_HOME/bin/domain.sh --host-config=host-master.xml
// slaveの場合
$ $KEYCLOAK_HOME/bin/domain.sh --host-config=host-slave.xml
```

## 外部共有データベースの設定

Keycloakサーバの共有データベースを作成します。今回DBサーバはシングル構成を用意しました。無論、冗長構成にすることもできます。又、冗長構成せずに [AWS RDS](https://aws.amazon.com/jp/rds/) を利用することで「可用性と耐久性」を満たすこともできます。実際に仕事でKeycloak＋AWS RDSで構築した経験がありますので、AWS RDSの件は別でまた投稿できればと思っています。
では、外部共有データベースの設定に戻りまして事前に用意したDBサーバにKeycloakの共有データベースを設定します。

* 共有データベース(keycloak)を作成

```sql
mysql> CREATE DATABASE keycloakdb;
Query OK, 1 row affected (0.00 sec)
```

* 接続ユーザ(keycloakuser)を作成して権限を付与

```sql
// ユーザ作成
mysql> CREATE USER 'keycloakuser'@'%' IDENTIFIED BY 'password';
Query OK, 0 rows affected (0.00 sec)
// 権限付与
mysql> GRANT ALL ON `keycloakdb`.* TO 'keycloakuser'@'%';
Query OK, 0 rows affected (0.00 sec)
// 変更反映
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
```

DBサーバ側の準備はこれでOKです。ここからはDB接続に必要なKeycloak側の設定を変更します。修正対象ファイルは「動作モード(operation mode)」によって異なります。今回は「Standalone Clustered Mode」を選択しているので「standalone-ha.xml」を修正します。

* （必要に応じて）オリジナルファイルのバックアップ

```bash
$ cd $KEYCLOAK_HOME/standalone/configuration
$ cp -pf standalone-ha.xml standalone-ha.xml.org
```

* データベース設定（datasource）を変更

datasource(KeycloakDS)の接続情報を今回用意したDBに接続するように変更します。「MySQL」に接続するので、MySQLのdriver定義も追加します。

```diff:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
 <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
- <connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
- <driver>h2</driver>
+ <connection-url>jdbc:mysql://keycloakdb.example.com:3306/keycloakdb?characterEncoding=utf8&amp;useSSL=false</connection-url>
+ <driver>mysql</driver>
 <security>
- <user-name>sa</user-name>
- <password>sa</password>
+ <user-name>keycloakuser</user-name>
+ <password>password</password>
 </security>
 </datasource>
 ･･････
 <drivers>
 <driver name="h2" module="com.h2database.h2">
 <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
 </driver>
+ <driver name="mysql" module="com.mysql">
+ <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
+ </driver>
 </drivers>
```

* 「MySQL」のモジュール追加

KeycloakのデフォルトではMySQLのJDBCドライバーは用意されていません。そのため、WildFlyの機能でモジュールとして登録する必要があります。

```bash
// MySQL用のモジュールディレクトリ作成
$ mkdir -p $KEYCLOAK_HOME/modules/system/layers/keycloak/com/mysql/main
// MySQLの connector をダウンロード(バージョンは任意です)
$ cd $KEYCLOAK_HOME/modules/system/layers/keycloak/com/mysql/main
$ curl -L -O http://central.maven.org/maven2/mysql/mysql-connector-java/5.1.41/mysql-connector-java-5.1.41.jar
// module.xml を新規作成
$ vi module.xml
```

```xml:$KEYCLOAK_HOME/modules/system/layers/keycloak/com/mysql/main/module.xml
<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.3" name="com.mysql">
 <resources>
 <resource-root path="mysql-connector-java-5.1.41.jar"/>
 </resources>
 <dependencies>
 <module name="javax.api"/>
 <module name="javax.transaction.api"/>
 </dependencies>
</module>
```

## ロードバランサーの設定

ロードバランサーの設定はKeycloak用に特別に設定する項目はありません。Apache、nginx、 [AWS ELB](https://aws.amazon.com/jp/elasticloadbalancing/) を使って動かしてみましたが、今回はApacheを使用しました。
説明しておきたいことは、**（できれば）「HTTPS」通信のロードバランサーを構築することです。理由は、Keycloakのデフォルト設定だと「SSL required」項目により、SSL通信が必須になるからです。**

SSL required (Keycloak Documentation の [Setting Up a Load Balancer or Proxy](http://www.keycloak.org/docs/latest/server_installation/index.html#_setting-up-a-load-balancer-or-proxy) より)

> SSL required - if the SSL required is set to external (the default) it should require SSL for all external requests

「external」の状態で「HTTP」通信すると「https required」エラーとなり接続できません。「HTTP」で接続できるように [Admin CLI](http://www.keycloak.org/docs/latest/server_admin/index.html#admin-cli) コマンドで設定を変更する方法もありますが、特に理由がなければ「HTTPS」通信のロードバランサーを設定することをお勧めします。

* （参考）「HTTP」通信のため、Admin CLI コマンドで「SSL required」設定を変更する方法

```bash
$ cd $KEYCLOAK_HOME/bin/
// 管理者ID（ここではadmin）で認証情報を設定する
$ kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin
// SSL requiredを「NONE」に変更
$ kcadm.sh update realms/{レルム名} -s sslRequired=NONE
```

ロードバランサーを「HTTPS」通信に設定した場合、Keycloakの「http-listener」設定も合わせて修正する必要があります。変更する項目は「proxy-address-forwarding」でデフォルトは未設定(false)です。この項目が「false」だと、Keycloakでリダイレクトが発生したタイミングでURLの「HTTPS」が「HTTP」に戻ってしまいますので、「true」に変更します。

```diff:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
 <subsystem xmlns="urn:jboss:domain:undertow:4.0">
 <buffer-cache name="default"/>
 <server name="default-server">
 <ajp-listener name="ajp" socket-binding="ajp"/>
- <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
+ <http-listener name="default" socket-binding="http" proxy-address-forwarding="true" redirect-socket="https" enable-http2="true"/>
 <https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
　　　　 ......
 </server>
 <servlet-container name="default">
 <jsp-config/>
　　　　 ......
 </subsystem>
```

ロードバランサにApache、nginxを利用する場合は、以下のようにHTTPヘッダーに「X-Forwarded-Proto "https"」を追加する必要があります。[AWSのELBを利用する場合](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-proto)は「X-Forwarded-Proto」が自動的に付与されるため、以下の設定は不要です。

* apache

```
RequestHeader set X-Forwarded-Proto "https"
```

* nginx

```
proxy_set_header X-Forwarded-Proto https;
```

## マルチキャストネットワークの設定

KeycloakのHA設定ファイル（standalone-ha.xml）はデフォルトでUDPのマルチキャスト通信するように設定されています。今回はマルチキャストネットワークの設定は変更をせず、Keycloakのデフォルト設定のままで動かします。以下設定ファイルの中にトランスポートプロトコルの種類の指定箇所や、マルチキャストネットワークのIPアドレス、ポート情報の定義箇所にコメントを入れましたので、参考にしてください。

```xml:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
 <subsystem xmlns="urn:jboss:domain:jdr:1.0"/>
 <subsystem xmlns="urn:jboss:domain:jgroups:4.0">
 <channels default="ee">
 <!-- トランスポートプロトコルは UDP。-->
 <channel name="ee" stack="udp"/>
 </channels>
 <stacks>
 <stack name="udp">
 <!-- socket-binding情報はjgroups-udpの定義を使用。-->
 <transport type="UDP" socket-binding="jgroups-udp"/>
 <protocol type="PING"/>
 <protocol type="MERGE3"/>
 <protocol type="FD_SOCK"/>
 <protocol type="FD_ALL"/>
 <protocol type="VERIFY_SUSPECT"/>
 <protocol type="pbcast.NAKACK2"/>
 <protocol type="UNICAST3"/>
 <protocol type="pbcast.STABLE"/>
 <protocol type="pbcast.GMS"/>
 <protocol type="UFC"/>
 <protocol type="MFC"/>
 <protocol type="FRAG2"/>
 </stack>
 ...
 </stacks>
 ...
 </subsystem>
 ...
 <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
 ...
 <socket-binding name="jgroups-mping" interface="private" port="0" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
 <socket-binding name="jgroups-tcp" interface="private" port="7600"/>
 <!-- jgroups-udpの定義-->
 <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="modcluster" port="0" multicast-address="224.0.1.105" multicast-port="23364"/>
 ...
 </socket-binding-group>
```

## サーバキャッシュの設定

Infinispanの説明で少し触れましたが、キャッシュの同期方式は「Replicatedモード」「Distributedモード」があります。「Replicatedモード」を選択すると、キャッシュデータを全てのノードに同期するので一部のノードが停止してもキャッシュデータは維持されます。（もちろん、全てのノードが停止した場合は失います。）
注意すべきは「Distributedモード」です。一部のノードにしかキャッシュデータを保持しませんので、キャッシュデータを保持していたノードが停止してしまうと、他のノードが起動中であってもキャッシュデータを失って再ログインを要求されます。
standalone-ha.xmlの「Distributedモード」の設定を見てみます。

```xml:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
<subsystem xmlns="urn:jboss:domain:infinispan:4.0">
 <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
 <transport lock-timeout="60000"/>
 <local-cache name="realms">
 <eviction max-entries="10000" strategy="LRU"/>
 </local-cache>
 <local-cache name="users">
 <eviction max-entries="10000" strategy="LRU"/>
 </local-cache>
 <distributed-cache name="sessions" mode="SYNC" owners="1"/>
 <distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
 <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
 <distributed-cache name="authorization" mode="SYNC" owners="1"/>
 <replicated-cache name="work" mode="SYNC"/>
 <local-cache name="keys">
 <eviction max-entries="1000" strategy="LRU"/>
 <expiration max-idle="3600000"/>
 </local-cache>
 </cache-container>
```

`distributed-cache`の種類の説明

* sessions : ユーザのセッション情報をキャッシュします。
* offlineSessions： アプリケーションのセッション情報をキャッシュします。
* loginFailures： ログインに失敗した情報をキャッシュします。
* authorization： 認可ポリシーをキャッシュします。

`distributed-cache`の定義の中に「owners」の設定があります。「owners」はキャッシュデータを保持するノード数で、キャッシュデータを保持しているノードを「owner」といいます。「owner」はセッションIDが発行されるタイミングでほぼランダム[^1]で決まります。
「owners」のデフォルト設定は "1" です。これが「Distributedモード」にした時の注意すべきポイントで、2台構成をデフォルト設定で動かした場合、キャッシュデータが保持される確率は50%しかないということです。**確実にキャッシュデータを維持するためには「owners」を2に変更するか、同期方式を「Replicatedモード」に変更する必要があります** （個人的には冗長構成用の設定なのでownersのデフォルトは"2"で良いと思いますが...）
2台構成で「owners="2"」にするのは結局全てのノードにキャッシュデータを保持することになるので、「Replicatedモード」に変更するのとほぼ同じですが、3台以上になれば「owners="2"」にするだけでも「owner」である2台が同時に停止しない限りキャッシュデータは維持できるので、高い確率でキャッシュデータを継続させることができます。
構築するシステムに合わせて、「Distributedモード」でowner数を調整するか、「Replicatedモード」を選択するかを設計する必要があると思います。キャッシュデータを失った時、再ログインを許容できるのであれば「owners="1"」のままでも良いと思います。

今回はどちらのノードが停止しても、再ログインせずに継続してサービス利用可能なシステムを想定して、以下の「Distributedモード」＆「owners="2"」に設定を変更して動かします。

```diff:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
<subsystem xmlns="urn:jboss:domain:infinispan:4.0">
 <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
 <transport lock-timeout="60000"/>
 <local-cache name="realms">
 <eviction max-entries="10000" strategy="LRU"/>
 </local-cache>
 <local-cache name="users">
 <eviction max-entries="10000" strategy="LRU"/>
 </local-cache>
- <distributed-cache name="sessions" mode="SYNC" owners="1"/>
- <distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
- <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
- <distributed-cache name="authorization" mode="SYNC" owners="1"/>
+ <distributed-cache name="sessions" mode="SYNC" owners="2"/>
+ <distributed-cache name="offlineSessions" mode="SYNC" owners="2"/>
+ <distributed-cache name="loginFailures" mode="SYNC" owners="2"/>
+ <distributed-cache name="authorization" mode="SYNC" owners="2"/>
 <replicated-cache name="work" mode="SYNC"/>
 <local-cache name="keys">
 <eviction max-entries="1000" strategy="LRU"/>
 <expiration max-idle="3600000"/>
 </local-cache>
 </cache-container>
```

[^1]: ログイン時に`session id`として`UUID.randomUUID().toString()`によりランダムな文字列を生成し、`session id`をキーにコンシステントハッシュ法に基づきキャッシュトポロジー segments のインデックス(0～79) を算出し `owner`ノードを決定する。

# AWS EC2でのKeycloak冗長構成
AWSのEC2上でKeycloakを冗長構成にする場合UDPマルチキャスト通信だと正常に動きません。:cry:（Keycloak自体は起動しますが、冗長構成の相手サーバが見つからずシングル構成で起動してしまいます。）

## なぜ動かないの？
なぜなら、**AWSのEC2はUDPのマルチキャストをサポートしていないからです**。ですので、これはKeycloakだけでなくUDPマルチキャストで冗長構成するサービスはAWS EC2上で同じことが起きます。

## 動くようにするには
JGroupsの設定を「TCP」通信に変更します。「TCP」通信に変更すると、ノード間通信の為のノード情報(ノード名、IPアドレスなど)を格納する場所が必要になります。格納方式は以下の２つ。

* JDBC_PING : データベースにJGroups用のテーブルを作成して各ノード情報を格納する方法
* S3_PING : AWSのS3にバケットを用意して各ノード情報を格納する方法
「S3_PING」方式は別途、AWS S3 バゲットを用意しないといけない反面、「JDBC_PING」方式はKeycloakの共有データベースをそのまま利用できるので、今回は 「JDBC_PING」を選択します。

## JGroupsの設定を「TCP」に変更（JDBC_PING方式）

* JGroupsのモジュールがDB(MySQL)を参照できるように修正

```diff:$KEYCLOAK_HOME/modules/system/layers/base/org/jgroups/main/module.xml
<module xmlns="urn:jboss:module:1.5" name="org.jgroups">
 <resources>
 <resource-root path="jgroups-3.6.13.Final.jar"/>
 </resources>
 <dependencies>
 <module name="javax.api"/>
 <module name="org.jgroups.azure"/>
+ <module name="com.mysql"/>
 </dependencies>
</module>
```

* standalone-ha.xmlのJGroups設定を変更

```diff:$KEYCLOAK_HOME/standalone/configuration/standalone-ha.xml
 <subsystem xmlns="urn:jboss:domain:jgroups:5.0">
 <channels default="ee">
 <!-- JGroupsのトランスポートプロトコルをTCPに変更 -->
- <channel name="ee" stack="udp"/>
+ <channel name="ee" stack="tcp"/>
 </channels>
 <stacks>
 <stack name="udp">
 <transport type="UDP" socket-binding="jgroups-udp" />
 <protocol type="PING"/>
 ......
 </stack>
 ......
 <!-- TCP設定をJDBC_PING向けに修正 -->
 <stack name="tcp">
- <transport type="TCP" socket-binding="jgroups-tcp"/>
+ <transport type="TCP" socket-binding="jgroups-tcp">
+ <property name="external_addr">
+ ${env.EXTERNAL_IP}
+ </property>
+ </transport>
 <socket-protocol type="MPING" socket-binding="jgroups-mping"/>
+ <protocol type="JDBC_PING">
+ <property name="connection_driver">
+ com.mysql.jdbc.Driver
+ </property>
+ <property name="connection_url">
+ jdbc:mysql://keycloakdb.example.com:3306/keycloakdb?characterEncoding=utf8&amp;useSSL=false
+ </property>
+ <property name="connection_username">
+ keycloakuser
+ </property>
+ <property name="connection_password">
+ password
+ </property>
+ <property name="initialize_sql">
+ CREATE TABLE IF NOT EXISTS JGROUPSPING (
+ own_addr VARCHAR(200) NOT NULL,
+ cluster_name VARCHAR(200) NOT NULL,
+ ping_data BLOB DEFAULT NULL,
+ PRIMARY KEY (own_addr, cluster_name)
+ )
 </property>
 </protocol>
 <protocol type="MERGE3"/>
 <protocol type="FD_SOCK"/>
 <protocol type="FD_ALL"/>
 <protocol type="VERIFY_SUSPECT"/>
 <protocol type="pbcast.NAKACK2"/>
 <protocol type="UNICAST3"/>
 <protocol type="pbcast.STABLE"/>
 <protocol type="pbcast.GMS"/>
 <protocol type="MFC"/>
 <protocol type="FRAG2"/>
 </stack>
 </stacks>
 </subsystem>
 ......
 <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
 ......
 <socket-binding name="jgroups-mping" interface="private" port="0" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
 <!-- TCPのJGroupsポートを private → public に変更 -->
- <socket-binding name="jgroups-tcp" interface="private" port="7600"/>
+ <socket-binding name="jgroups-tcp" interface="public" port="7600"/>
 <socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>
 ......
 </socket-binding-group>
```

上記設定の中の `${env.EXTERNAL_IP}` にはKeycloakサーバ自身のIPアドレスを設定します。このIPはノード間の通信時に使われるので localhost や 127.0.0.1 は使用できません。ちなみにAWS上では以下のコマンドで自身のIPアドレスを取ることができます。standalone-ha.xmlでは環境変数が使えるのでコマンドで環境変数に設定して置いてstandalone-ha.xmlには${env.EXTERNAL_IP}と定義するだけでOKです。 :+1:

```bash
export EXTERNAL_IP=$(curl -s 169.254.169.254/latest/meta-data/local-ipv4)
```

## JDBC_PING使用時の注意点

JDBC_PINGは、Keycloak起動時に自分自身のノード情報(ノード名やIPなど) を DB(JGROUPSPING) に登録します。このDB(JGROUPSPING)の情報を元にJGROUPのメンバーを特定することが可能になります。
(サービス停止などで) JGROUPメンバーから外れるとDB(JGROUPSPING)からノード情報を削除しますが、正常なサービス停止ではない場合(Dockerコンテナからの停止やkillなどでの強制終了)は、レコードが削除されずに残ってしまう場合があります。（以下、これをゾンビレコードと言います）
ゾンビレコードが残ってしまうと、次回Keycloak起動時に存在しないゾンビノードに対して接続を試みます。ゾンビレコードが増えればその分、接続を試みる回数も増えてKeycloakの起動も遅くなります。
解消方法は、JGROUPの `clear_table_on_view_change` オプションを `true` に設定します。このオプションを有効にすることで毎回DB(JGROUPSPING)をクリアします。
しかし、公式の http://jgroups.org/manual/#_jdbc_ping の説明文をみると、
> If clear_table_on_view_change is set to true, then the coordinator clears the table after a view change (instead of only removing the crashed members), and everybody re-inserts its own information. This attribute can be set to true if automatic removal of zombies is desired. However, it is costly, therefore if no zombies ever occur (e.g. because processes are never killed with kill -9), or zombies are removed by a system admin, then it should be set to false.

このオプションは処理コストがかかるデメリットも存在するので、システム要件に合わせて設定することをお勧めします。

※ 「S3_PING(FILE_PING)」方式もゾンビファイルが残るという同現象が発生しますので、「S3_PING(FILE_PING)」の場合は、`remove_old_coords_on_view_change` `remove_all_files_on_view_change` オプションを有効にすることで解消します。

# 動かしてみる
全てのサーバ(Keycloak01, Keycloak02, DB, Apache)を起動します。Keycloakの起動は [Bind Addresses](http://www.keycloak.org/docs/3.2/server_installation/topics/network/bind-address.html) を起動オプションとして渡す必要があります。

Keycloak01 起動コマンド例

```bash
$ $KEYCLOAK_HOME/bin/standalone.sh --server-config=standalone-ha.xml -b keycloak01.example.com -Djboss.node.name=keycloak01.example.com -Djboss.bind.address.private=keycloak01.example.com
```

* --server-configはHA構成の「standalone-ha.xml」を設定
* -Djboss.node.name はノード名(ホスト名)を設定
* -b, -Djboss.bind.address.private はノード名(ホスト名)を設定

## 起動時にKeycloakのサーバログを確認する

正常に冗長構成できている場合は、Keycloak起動時のserver.logに以下のJGroupsログが出力されます。2台のKeycloakサーバがグループ化されていることが確認できます。

```txt:$KEYCLOAK_HOME/standalone/log/server.log
19:08:55,198 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel server: [keycloak01.example.com|3] (2) [keycloak01.example.com, keycloak02.example.com]
19:08:55,201 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel keycloak: [keycloak01.example.com|3] (2) [keycloak01.example.com, keycloak02.example.com]
19:08:55,201 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel web: [keycloak01.example.com|3] (2) [keycloak01.example.com, keycloak02.example.com]
19:08:55,204 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel ejb: [keycloak01.example.com|3] (2) [keycloak01.example.com, keycloak02.example.com]
19:08:55,205 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel hibernate: [keycloak01.example.com|3] (2) [keycloak01.example.com, keycloak02.example.com]
```

## フェイルオーバーを確認する

keycloak01/keycloak02 どちらに振られているかを確認するため、以下のようにロードバランサーのアクセスログに%{BALANCER_WORKER_ROUTE}を出力するように設定しました。

```
Header add Set-Cookie "BIGipServerKEYCLOAKSSLWEB_POOL=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<Proxy balancer://keycloak>
 BalancerMember http://keycloak01.example.com:8080 route=01 ttl=1
 BalancerMember http://keycloak02.example.com:8080 route=02 ttl=1
</Proxy>
```

アクセスログは以下のように出力されます。以下の例は「"BIGipServerKEYCLOAKSSLWEB_POOL=.01"」と出力されているので、keycloak01に振られているのが分かります。keycloak02の場合は「"BIGipServerKEYCLOAKSSLWEB_POOL=.02"」と出力されます。

```
[27/Oct/2017:19:19:38 +0900] 0 5327 "GET /auth/resources/3.3.0.cr2/admin/keycloak/templates/kc-tabs-realm.html HTTP/1.1" 200 2045 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
```

では、フェイルオーバーの動作を確認します。keycloak01, keycloak02両方起動中の状態で、
Keycloakの管理コンソールにログインします。

![2017-10-27_191953.png](https://qiita-image-store.s3.amazonaws.com/0/206135/09cd1c35-51ad-fc2a-f56d-8a8d9ad28297.png)

ログイン後TOP画面が表示されます。

![2017-10-27_192033.png](https://qiita-image-store.s3.amazonaws.com/0/206135/2ee8f869-df3c-4fdd-a00a-32dc53a9b7b1.png)
この時のロードバランサーのアクセスログを確認します。

```
......
[27/Oct/2017:19:19:38 +0900] 0 5327 "GET /auth/resources/3.3.0.cr2/admin/keycloak/templates/kc-tabs-realm.html HTTP/1.1" 200 2045 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:19:38 +0900] 0 8847 "GET /auth/resources/3.3.0.cr2/admin/keycloak/templates/kc-menu.html HTTP/1.1" 200 5433 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:19:38 +0900] 0 8501 "GET /auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/fonts/OpenSans-Semibold-webfont.woff HTTP/1.1" 200 22908 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/css/patternfly.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:19:38 +0900] 0 11755 "GET /auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/fonts/OpenSans-Bold-webfont.woff HTTP/1.1" 200 22432 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/css/patternfly.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:19:38 +0900] 0 6179 "GET /auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/fonts/OpenSans-Light-webfont.woff HTTP/1.1" 200 22248 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/resources/3.3.0.cr2/admin/keycloak/lib/patternfly/css/patternfly.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
......
```

"BIGipServerKEYCLOAKSSLWEB_POOL=.01" からKeycloak01に振られているのが分かります。

Keycloak01サーバを停止します。

```:keycloak01停止時、keycloak02のserver.log
......
19:21:46,918 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel server: [keycloak02.example.com|4] (1) [keycloak02.example.com]
19:21:46,921 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel keycloak: [keycloak02.example.com|4] (1) [keycloak02.example.com]
19:21:47,050 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel web: [keycloak02.example.com|4] (1) [keycloak02.example.com]
19:21:47,067 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel hibernate: [keycloak02.example.com|4] (1) [keycloak02.example.com]
19:21:47,076 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel ejb: [keycloak02.example.com|4] (1) [keycloak02.example.com]
......
```

server.logからKeycloak01が冗長構成から外れているのが分かります。
先ほどのTOP画面表示状態でclientsメニューをクリックします。
![2017-10-27_192347.png](https://qiita-image-store.s3.amazonaws.com/0/206135/980184d3-a670-57f2-a8b2-bf2f5d2d529c.png)
この時のロードバランサーのアクセスログを確認します。

```
......
 [27/Oct/2017:19:22:48 +0900] 0 495625 "GET /auth/admin/realms/master HTTP/1.1" 200 2527 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
 [27/Oct/2017:19:22:48 +0900] 0 11489 "GET /auth/resources/3.3.0.cr2/admin/keycloak/templates/kc-paging.html HTTP/1.1" 200 1155 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
 [27/Oct/2017:19:22:48 +0900] 0 373314 "GET /auth/admin/realms/master/clients?viewableOnly=true HTTP/1.1" 200 13749 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
......
```

"BIGipServerKEYCLOAKSSLWEB_POOL=.02" からKeycloak02に振られているのが分かります。
ここでKeycloak01を起動して、もう一度画面を操作します。今回はRealmメニューをクリックしました。
![2017-10-27_192524.png](https://qiita-image-store.s3.amazonaws.com/0/206135/1c399fe5-918a-6b61-5763-528e0102907b.png)
この時のロードバランサーのアクセスログを確認します。

```
......
[27/Oct/2017:19:24:35 +0900] 0 94542 "GET /auth/admin/realms/master HTTP/1.1" 200 2527 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:24:35 +0900] 0 123872 "GET /auth/admin/realms/master/clients HTTP/1.1" 200 13749 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:24:35 +0900] 0 204240 "GET /auth/admin/realms/master/clients/0057898b-43f9-4ced-9cf4-ff3deeca9f0d HTTP/1.1" 200 2644 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:24:36 +0900] 0 5693 "GET /auth/resources/3.3.0.cr2/admin/keycloak/templates/kc-tabs-client.html HTTP/1.1" 200 4513 "BIGipServerKEYCLOAKSSLWEB_POOL=.02" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
......
```

"BIGipServerKEYCLOAKSSLWEB_POOL=.02" からまだKeycloak02に振られているのが分かります。
今度は、Keycloak02を停止します。

```:keycloak02停止時、keycloak01のserver.log
......
19:25:17,772 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel server: [keycloak01.example.com|6] (1) [keycloak01.example.com]
19:25:17,785 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel keycloak: [keycloak01.example.com|6] (1) [keycloak01.example.com]
19:25:17,925 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel web: [keycloak01.example.com|6] (1) [keycloak01.example.com]
19:25:17,971 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel ejb: [keycloak01.example.com|6] (1) [keycloak01.example.com]
19:25:17,986 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (thread-2) ISPN000094: Received new cluster view for channel hibernate: [keycloak01.example.com|6] (1) [keycloak01.example.com]
......
```

server.logからKeycloak02が冗長構成から外れているのが分かります。
ここでもう一度画面を操作します。今回もRealmメニューをクリックしました。
![2017-10-27_192629.png](https://qiita-image-store.s3.amazonaws.com/0/206135/0e6b5817-d9b1-4be5-4cf9-3b77f7059290.png)
この時のロードバランサーのアクセスログを確認します。

```
......
[27/Oct/2017:19:25:38 +0900] 0 567203 "GET /auth/admin/realms/master HTTP/1.1" 200 2527 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
[27/Oct/2017:19:25:38 +0900] 0 573475 "GET /auth/admin/realms HTTP/1.1" 200 2529 "BIGipServerKEYCLOAKSSLWEB_POOL=.01" "https://keycloak.example.com/auth/admin/master/console/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0"
......
```

"BIGipServerKEYCLOAKSSLWEB_POOL=.01" で再度Keycloak01に振り直されているのが分かります。
Keycloakの冗長構成の動作確認は以上です。

# 最後に
Keycloakを冗長構成で動かしてみました。冗長構成にする方法が他のSSO製品(OpenAM)に比べてシンプルで簡単だと感じました。3台以上の冗長構成にしたい場合も、今日の手順でKeycloakの設定を同じく3台以上設定するだけで台数を増やすことができます。Keycloakの冗長構成について、また機会があれば下記のパターンも書いてみたいと思いました。

* AWSでELB,RDSを利用したKeycloakの冗長構成
* DockerでKeycloakの冗長構成

それでは、今日の記事は以上です。ありがとうございました :bow:
(追記）
→ AWS、Dockerを利用して冗長構成する方法は、Keycloak by OpenStandia Advent Calendar 2017 の 21日目 [インフラ管理不要なコンテナ環境のAWS FargateでKeycloakを動かしてみる](https://qiita.com/wadahiro/items/0837729e7c57becbfd06) で紹介されました:star:

# 参考資料
 - [NRI OpenStandia Keycloak](http://openstandia.jp/oss_info/keycloak/index.html)
 - [NRI OpenStandia Keycloak日本語ドキュメント](http://keycloak-documentation.openstandia.jp/)
 - [Keycloak Documentation](http://www.keycloak.org/documentation.html)
 - [Clustering](http://www.keycloak.org/docs/latest/server_installation/index.html#_clustering)
 - [JGroups Subsystem](https://docs.jboss.org/author/display/WFLY10/JGroups+Subsystem)
 - [Transport](http://www.jgroups.org/manual/html/protlist.html#Transport)
 - [Infinispan](http://infinispan.org/)
 - 
[![mark.png](https://github.com/k-tamura/cocha-icons/raw/master/mark.png)](https://github.com/k-tamura/cocha-icons)