Keycloak 16 -> 22 に Upgrade した話

# Background

[Keycloak に Duo を連携する](https://qiita.com/uturned0/items/8f45ce492ec59680f9f2) ために Keycloak の ver up が必要になりました。その keycloak を導入した前任者は、辞める直前に *「keycloakのversion upできなかったんですよねぇ…」* と意味深にこぼしていました。その理由を思い知った話です。

# Keycloak は 2 つある

Keycloak は Java で実装されていますがそのフレームワークが変わるとともに大きな変更が発生しました。古いのが WildFly(ex. JBoss Application Server), 新しいのが Quarkus です。

**WildFly**: version ? - 20 まで
**Quarkus**: version 17 - latest 

Version 17 - 19 の間は 2 つの distribution が存在していたことになります。 

言い換えます。

　　**同じバージョンでも全く別物があるんだよっっ！！！！**




Quarkusへの移行は **breaking changes** を伴うもので、今回はこれを乗り越えたお話です。詳細な移行ガイドはこちら。

https://www.keycloak.org/migration/migrating-to-quarkus

Keycloak version history

https://www.keycloak.org/docs/latest/release_notes/index.html

# Docker image の変遷
以前は Docker hub の `jboss/keycloak` イメージが主流でしたが、v16.1.1 を最後に更新が止まりました。

> https://hub.docker.com/r/jboss/keycloak/tags

現在は Redhat の docker registry `quay.io/keycloak/keycloak` で配布されています。

> https://quay.io/repository/keycloak/keycloak

ここには `-legacy` と名のつくimagesがあり、これは WildFly版 を示しています。

しかし、 `17.0.0-legacy` から `19.0.3-legacy` までしかありません。
つまりdocker imageをサクッと使う場合、利用できるのは ver 19 が最後ということになります。

`jboss/keycloak:16.1.1` -> `quay.io/keycloak/keycloak:19.0.3-legacy` への移行は簡単でした。imageを差し替えるだけでした。(envもDBもそのままで動きます)

# Quarkusに移行しよう

情報が少なくてめっちゃ大変でしたが無事に移行したのでまとめます。

## Quarkusへの移行に必要な事
1. docker imageを `quay.io` のものを使うようにする
2. 環境変数を **全て** 差し替える
3. DBは何もしなくても動いた
4. Keycloakに連動してるサービスの設定を変更する
    1. なんと Keycloak URL が変わるので client id 持ってるサービスは全部変更が必須です。（ただし設定で回避できます）


# 移行方法

## Dockerfile

as-is

```yaml
FROM jboss/keycloak:16.1.1
```

to-be

official doc では build stageを分けるように言っているのでその通りに従ってみました。

https://www.keycloak.org/server/containers


```yaml
#################################################
# Builder
#################################################
ARG KEYCLOAK_VERSION=22.0.1-2
FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION as builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true

# Configure a database vendor
ENV KC_DB=mysql

WORKDIR /opt/keycloak
COPY DuoUniversalKeycloakAuthenticator-1.0.7_22.0.1wd.jar  /opt/keycloak/providers

RUN /opt/keycloak/bin/kc.sh build

#################################################
# Runner
#################################################
ARG KEYCLOAK_VERSION=22.0.1-2
FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION
COPY --from=builder /opt/keycloak/ /opt/keycloak/

EXPOSE 8080
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]
```

*Builderの注釈
ここで入れているENVは全て不要ですが、入れておくとRunnerを違うenvでstartしたときにwarningが出るようになります。

*Runnerの注釈
httpしか使わない設定にしてます。imageにENVを埋め込んだり、起動オプションを追加したい場合はここで入れていいです。私はk8s + 環境変数で管理したかったので変更していません。

起動オプションはここにまとめがあります。

https://www.keycloak.org/server/all-config


## Example of docker-compose.yaml

一例です。環境変数は起動オプションのマニュアルで項目一つずつをexpandすると書いてあります。

> https://www.keycloak.org/server/all-config


```yaml
version: '3.1'

services:
  keycloak:
    build:
      context: .
    environments:
      - KC_DB=mysql
      - KC_DB_URL=jdbc:mysql://mysql.local:3306/keycloak
      - KC_DB_USERNAME=root
      - KC_DB_PASSWORD=example
      - KC_HOSTNAME=my-keycloak-ui.example.com
      - KC_HTTP_PORT=8080
      - KC_HTTP_ENABLED=true
      - KC_HOSTNAME_STRICT=false
      - KC_HOSTNAME_STRICT_HTTPS=false
      - KC_HEALTH_ENABLED=true
      - KC_METRICS_ENABLED=true
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
      - PROXY_ADDRESS_FORWARDING="true"
    ports:
      - 8080:8080
    command: /opt/keycloak/bin/kc.sh start-dev
```


## Environments
### Database
as-is

```yaml
# MySQL connection
DB_ADDR: mysql.local
DB_DATABASE: keycloak
DB_USER: root
DB_PASSWORD: example
DB_PORT: 3306

# Keycloak admin
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
```

to-be

環境変数の名前が全て変わりました。prefixとして `KC_` がついています。
この環境変数をまとめた資料はこれです。ぱっと見は起動オプションに見えますが、expandすると環境変数が見えます。消えたものもあるので注意です。

https://www.keycloak.org/server/all-config


```yaml
KC_DB: "mysql"

# MySQL connection
KC_DB_URL: "jdbc:mysql://mysql.local:3306/keycloak"
KC_DB_USERNAME: root
KC_DB_PASSWORD: example

# Keycloak admin
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
```

DB部分を分けて書く形式だと、おそらく以下でも動きます（試してません）。KC_DB_URL_HOST etc と KC_DB_URLの併用はできません。

```yaml
KC_DB: "mysql"

# MySQL connection
KC_DB_URL_HOST: mysql.local
KC_DB_URL_DATABASE: keycloak
KC_DB_USERNAME: root
KC_DB_PASSWORD: example
KC_DB_URL_PORT: 3306
```


### Environments for network

HTTPを利用する場合、Load balancerでSSL offloadingする場合の設定が変わりました。 起動するけど、アクセスすると `Content-Security-Policy` のエラーで永遠に loading gif が流れたりして大ハマりしました。

browser -> (httpS) -> LB -> (http) -> Keycloak の例

```yaml
### Required for SSL-termination by LB --->
## LB が SSL Termination している場合のモード = edge
KC_PROXY: "edge"
KC_PROXY_ADDRESS_FORWARDING: "true"

# LB で SSL Termination してる場合は HTTPS を無効化
KC_HOSTNAME_STRICT: "false"
KC_HOSTNAME_STRICT_HTTPS: "false"
### <---

# LB で SSL Terminationしてる場合は HTTP 必須
KC_HTTP_ENABLED: "true"
KC_HTTP_PORT: "8080"
```

`KC_PROXY: "edge"` によってfrontendの HTTP Headerにつく Content-Security-Policy が `self` だけから keycloak URL も追加されたものになり、admin consoleを開けるようになります。これがわからなくて時間を溶かしました。横山さん、助けてくれてありがとう。


LB や Reverseproxyを使う場合、ここら辺の設定は **必須** です。ver 16 までのように簡単には動きません。詳細はマニュアルを見てください。AWS EC2 の場合など、さまざまなパターンがあります。
https://www.keycloak.org/server/reverseproxy


### Environments for redundancy

QuarkusのKeycloakはinfinispanを使って冗長化する仕組みが入っているみたいです。冗長化をしようとしましたがうまく行かず今回は諦めました。
追記：成功しました https://qiita.com/uturned0/items/37d2ce64d04a3f3a5dd2

infinispanは通常UDPを使ってcluster discoveryや通信をしますが、kubernetesではUDPが使えないのでTCPにするなどの対策が必要だそうです。


```yaml
### infinispan 分散キャッシュを有効にする
KC_CACHE: "ispn"
# k8sはUDP multicastが使えないため、TCPPINGを使う
# https://www.keycloak.org/server/caching
KC_CACHE_STACK: "kubernetes"
# pod内からk8s serviceの名前解決ができるドメインを指定
# `<service-name>.<namespace-name>.svc.cluster.local`
JAVA_OPTS: "-Djgroups.dns.query=keycloak-service.keycloak.svc.cluster.local"
```

上記の設定で keycloak は走るのですが、infinispanの通信を観測できませんでした。ここで JAVA_OPTS を空にして走らせると infinispanのdns.queryがないというエラーとともに現在の設定内容が表示されます。そこには 7800/tcp とあったのでそこら辺をtcpdumpしたんですが全く観測できず。sessionも共有されていないためか、Duoからのredirectでエラーが起こりました。時間なく諦めました。

追記： k8sのheadless serviceが必要でした、成功しました。

https://qiita.com/uturned0/items/37d2ce64d04a3f3a5dd2


docs
https://www.keycloak.org/server/caching


# How to debug
keycloak podの中で kc.sh を使って状態を確認できます。 `/opt/keycloak/` が keycloakのroot dirとなっていて、confなどもそこにあります。

```
# show config
/opt/keycloak/bin/kc.sh show-config

# database import/export
/opt/keycloak/bin/kc.sh tools

...
```

## show-configの例

手元のdev環境の例です。duo pluginが入ったりしてるので初期状態とは違います。
環境変数で上書きされてるものには `KcEnvVarConfigSource` と表示されるのが優しい。

```rb
bash-5.1$ /opt/keycloak/bin/kc.sh show-config
Current Mode: development
Current Configuration:
    kc.cache =  local (PersistedConfigSource)
    kc.config.built =  true (SysPropConfigSource)
    kc.db =  mysql (KcEnvVarConfigSource)
    kc.db-password =  ******* (KcEnvVarConfigSource)
    kc.db-url =  jdbc:mysql://your-mysql.local:3306/keycloak (KcEnvVarConfigSource)
    kc.db-username =  keycloak (KcEnvVarConfigSource)
    kc.db.password =  keycloak (KcEnvVarConfigSource)
    kc.db.url =  jdbc:mysql://your-mysql.local:3306/keycloak (KcEnvVarConfigSource)
    kc.db.username =  keycloak (KcEnvVarConfigSource)
    kc.health-enabled =  true (KcEnvVarConfigSource)
    kc.health.enabled =  true (KcEnvVarConfigSource)
    kc.hostname-strict =  false (KcEnvVarConfigSource)
    kc.hostname-strict-https =  false (KcEnvVarConfigSource)
    kc.hostname-url =  https://your-keycloak.example.com (KcEnvVarConfigSource)
    kc.hostname.strict =  false (KcEnvVarConfigSource)
    kc.hostname.strict.https =  false (KcEnvVarConfigSource)
    kc.hostname.url =  https://your-keycloak.example.com (KcEnvVarConfigSource)
    kc.http-enabled =  true (KcEnvVarConfigSource)
    kc.http-port =  8080 (KcEnvVarConfigSource)
    kc.http.enabled =  true (KcEnvVarConfigSource)
    kc.http.port =  8080 (KcEnvVarConfigSource)
    kc.log-console-output =  default (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.log-file =  ${kc.home.dir:default}${file.separator}data${file.separator}log${file.separator}keycloak.log (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.metrics-enabled =  true (KcEnvVarConfigSource)
    kc.metrics.enabled =  true (KcEnvVarConfigSource)
    kc.provider.file.DuoUniversalKeycloakAuthenticator-1.0.7_22.0.1wd.jar.last-modified =  1702558543000 (PersistedConfigSource)
    kc.proxy =  edge (KcEnvVarConfigSource)
    kc.proxy.address.forwarding =  true (KcEnvVarConfigSource)
    kc.spi-map-storage-concurrenthashmap-dir =  ${kc.home.dir:default}${file.separator}data${file.separator}chm (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-map-storage-concurrenthashmap-key-type-authz-resource-servers =  string (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-map-storage-concurrenthashmap-key-type-realms =  string (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-map-storage-concurrenthashmap-key-type-single-use-objects =  string (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-theme-cache-templates =  false (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-theme-cache-themes =  false (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.spi-theme-static-max-age =  -1 (PropertiesConfigSource[source=jar:file:///opt/keycloak/lib/lib/main/org.keycloak.keycloak-quarkus-server-22.0.1.jar!/META-INF/keycloak.conf])
    kc.version =  22.0.1 (SysPropConfigSource)
bash-5.1$
```


## Clientの変更

Keycloakのpathが変わるので、OIDC Clientは変更を変えないといけません。

簡単にいうと、 `/auth/realms` の部分から `/auth` を消して `/realms` にします。

as-is

```rb
oidc_url = "https://your-keycloak.example.com/auth/realms/master/protocol/openid-connect/auth"
```

to-be

```rb
oidc_url = "https://your-keycloak.example.com/realms/master/protocol/openid-connect/auth"
```

ただし以前の挙動を残すオプションが用意されてます

```
bin/kc.[sh|bat] start-dev --http-relative-path /auth
```

環境変数だとこうです。

```
KC_HTTP_RELATIVE_PATH=/auth
```

defaultは `KC_HTTP_RELATIVE_PATH=/` です


ずっと古いpathつけるの嫌なので、今回は利用者にupdateしてもらいました。


# Reference
special thanks!

https://qiita.com/kanedaq/items/10398784038dfd11b972

# 終わり


とっても苦労したので、ハート押してね！



Duo を使った記事はこちらです。

https://qiita.com/uturned0/items/8f45ce492ec59680f9f2