Keycloak の クラスタリングで利用されるプロトコル

株式会社 日立製作所　茂木昂士

[日立グループOSS Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hitachi-oss) 1日目は、近年注目されているSingle Sign OnのOSSである[Keycloak](https://www.keycloak.org)のクラスタリングで使われているプロトコルについてです。

# Keycloak の クラスタリング

Keycloak では、JGroups, Infinispan というライブラリを利用してクラスタリングを実現しています。これらのライブラリは Keycloak のベースとなる Wildfly で利用されていますが、利用される Wildfly のバージョンが上がってきていることで様々な機能が追加されています。

筆者が、[Keycloak 超入門 (4) : Keycloak のクラスタ環境を構成してみよう](http://www.atmarkit.co.jp/ait/articles/1801/31/news019.html) という記事で Keycloak のクラスタ構成について解説しましたが、記事執筆時点と比べてバージョンが上がり、機能が増えてきて様々な選択肢が取れるようになっています。

今回は Keycloak のクラスタ機能のなかでも、 JGroups の Discovery プロトコルに絞って解説します。

# JGroups の Discovery プロトコル

JGroups はクラスタのメンバーとなるホストを見つけるために、Discovery プロトコルというものを利用しています。JGroups では、この Discovery プロトコルとして様々なプロトコルを提供しており、環境や要件に合わせて適切なプロトコルを選ぶことができるようになっています。

ここでは、いくつかのプロトコルについて簡単に説明していきます。なお、より詳しく知りたい場合は[JGroups のマニュアル (6.4 Initial membership Discovery)](http://www.jgroups.org/manual4/index.html#DiscoveryProtocols)を参照してください。

## PING

`PING`は IP マルチキャストを利用してクラスタのメンバーを見つけるプロトコルです。Keycloak のデフォルトでは`PING`が使われています。IP マルチキャストが利用可能な環境であれば、特別な設定もなく利用することができます。ですが、IaaS 等の環境ではマルチキャストが使えない環境も多いため注意が必要です。

## TCPPING

`TCPPING`は TCP 通信を使ってクラスタメンバーを見つけるプロトコルです。下記のように`initial_hosts`というパラメータで各ホストのアドレスを指定します。

```xml
        <protocol type="TCPPING">
            <property name="initial_hosts">192.168.10.1[7600],192.168.10.2[7600]</property>
        </protocol>
```

クラスタのメンバーが固定されているため、クラスタホスト数の増減がある状況や、IP アドレスが変わるような環境では利用しづらいですが、それ以外の環境では設定がシンプルであるため問題が起こりづらいプロトコルです。

## JDBC_PING

`JDBC_PING`はデータベースを利用してクラスタのメンバーを見つけるプロトコルです。クラスタの各メンバーが自身の情報をデータベースに書き込み、その情報をもとにクラスタを構築します。

ほとんどの環境では共有のデータベースを利用するので、このプロトコルが使われることが一般的のようです。

## DNS_PING

`DNS_PING`は DNS の A レコードや SVC レコードを利用してクラスタのメンバーを見つけるプロトコルです。もともとは Kubernetes や OpenShift などの環境向けに作られたのですが、DNS Discovery が利用できる環境であればどこでも利用できます。

## KUBE_PING

`KUBE_PING`は Kubernetes の API を利用してクラスタのメンバーを見つけるプロトコルです。`KUBE_PING`は JGroups Extra というプロジェクトで提供されているものですが、Keycloak では 4.4.0.Final から同梱されるようになっています。

`DNS_PING`との大きな違いは、Kubernetes の API を利用するために追加の権限(View)が必要になる点です。そのため、[Keycloak の公式](https://github.com/jboss-dockerfiles/keycloak/tree/master/server#openshift-example-with-dnsdns_ping)でも特別な権限のいらない`DNS_PING`の利用が推奨されています。

今回はこれらのプロトコルのうち、`DNS_PING` を試してみたいと思います。

# DNS_PING の設定

今回は OKD 3.11 上で設定を行います。Keycloak Docker イメージの GitHub リポジトリ([https://github.com/jboss-dockerfiles/keycloak](https://github.com/jboss-dockerfiles/keycloak))の`openshift-examples`というディレクトリにテンプレート用の Json ファイル(keycloak-https.json)があるのでこれを利用します。

## テンプレートのデプロイ

`keycloak-https.json`ファイルを取得し、下記のコマンドを実行します。

```console
# oc new-app -p NAMESPACE=`oc project -q` -f keycloak-https.json
```

これで Keycloak の DeploymentConfig が作成され、Pod が作成されます。クラスタの確認をするため`oc scale --replicas=3 dc keycloak`として、スケールします。

一見うまくいっているように見えますが、実はクラスタが構築されておらずそれぞれの Pod が独自に動いています。これには下記二つの理由があります。

### 問題 1: Headless Service が構築されていない

JGroups ドキュメントの`DNS_PING`の部分に Kubernetes/OpenShift では Headless Service が必要と書かれています。[Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services)とは clusterIP の値を持たない Service で、名前解決をすると Service のアドレスではなく、各 Pod のアドレスが得られるというものです。ですが今回のテンプレートには Headless Service を定義している部分がありません。

### 問題 2: Protocol Stack が udp で動いている

Keycloak の Docker イメージでは、JGroups で利用する Protocol Stack のデフォルトが udp になっています。ですが、udp のままだとクラスタ構築時にタイムアウトしていまい Keycloak が起動しません。

これらの問題を修正していきます。

## Docker イメージの作成

まずは Docker のイメージを修正していきます。Keycloak の Docker イメージは、`JGROUPS_DISCOVERY_PROTOCOL`で指定した値と同名の jboss-cli ファイルがあればそれを実行し、なければ`default.cli`というファイルを実行して Discovery プロトコルの設定を行います。なので、`dns_ping.cli`というファイルを作成し、下記の内容を記載します。

```jboss-cli:dns_ping.cli
embed-server --server-config=standalone-ha.xml --std-out=echo
batch

/subsystem=jgroups/channel=ee:write-attribute(name="stack", value="tcp")

/subsystem=jgroups/stack=tcp/protocol=MPING:remove()
/subsystem=jgroups/stack=tcp/protocol=dns.DNS_PING:add(add-index=0, properties=$keycloak_jgroups_discovery_protocol_properties)

run-batch
stop-embedded-server
```

ここでは、下記の処理を指定ます。

- Protocol Stack を tcpに変更
- Discovery Protocol に dns.DNS_PING を設定

そして、上記のファイルを追加した Keycloak の Docker イメージを作成します。

```dockerfile:Dockerfile
FROM jboss/keycloak-openshift:latest
ADD dns_ping.cli /opt/jboss/tools/cli/jgroups/discovery/
```

作成した Docker イメージは OKD の Docker レジストリに Push しておきます。

## Headless Service の作成

次に Headless Service を作成します。下記のように`clusterIP: None`という設定を入れて Service を作成します。

```yaml:keycloak-discovery.yaml
apiVersion: v1
kind: Service
metadata:
  name: keycloak-discovery
spec:
  clusterIP: None
  ports:
    - name: http
      port: 8080
      protocol: TCP
      targetPort: 8080
  selector:
    deploymentconfig: keycloak
```

Service の作成が完了すると、下記のように`CLUSTER-IP`の値が`None`という値になっている`keycloak-discvery`というサービスが作成されます。

```console
# oc get svc keycloak-discovery
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
keycloak-discovery   ClusterIP   None         <none>        8080/TCP   2m
```

## DeploymentConfig の修正

最後に DeploymentConfig の内容を修正していきます。修正する内容は下記の通りです。

- Pod に利用するイメージを自身で作成したものに変更
- `JGROUPS_DISCOVERY_POROTOCOL`の値を`dns.DNS_PING`から`dns_ping`に変更
- `JGROUPS_DISCOVERY_PROPERTIES`の値を`keycloak-discovery.keycloak.svc.cluster.local`に変更

上記の変更をが反映されると、Keycloak のクラスターが正常に動作するようになります。正常に起動していると各 Pod のログに下記のようなログが出てクラスターが構築されていることが確認できます。

```log
INFO  [org.infinispan.CLUSTER] (thread-11,ejb,keycloak-2-sgns7) ISPN000094: Received new cluster view for channel ejb: [keycloak-2-sgns7|2] (3) [keycloak-2-sgns7, keycloak-2-69ksp, keycloak-2-z7l9b]
...
```

これで DNS_PING の動作が確認できました。

# まとめ

Keycloak や Wildfly は、Kubernetes/OpenShift 上で簡単に Cluster を組むことができるようになってきていますが、今回はまだ発展途上な感じがしました。これからどんどん使われていくことによって、よりよいものに発展していくでしょう。