EKSを使ってクラスタ構成のKeycloakを構築してみた

前回[こちらの記事](https://qiita.com/s-takino/items/8d3c0c84253533292c91)でECSを使ってKeycloakのクラスタ構成を検証してみたが、今回は同じAWSのコンテナ関連サービスであるEKSをつかってKeycloakのクラスタ構成を構築し、動作確認してみる。

# 本記事でやること
1. EKS環境設定
2. マニュフェスト作成
3. デプロイ
4. 動作検証

# 前提条件
- Keycloak バージョン：21.1.1
- コンテナイメージは[公式](https://quay.io/repository/keycloak/keycloak)のものをそのまま利用
- JGroups によるノード間通信は「DNS_PING」を利用
- Kubernetesのマニュフェストファイルは[公式で案内](https://www.keycloak.org/getting-started/getting-started-kube)されているものをベースにカスタマイズ

# EKS環境設定
## Cloud Shellの設定
今回はkubenetes操作用の端末として`Cloud Shell`を利用する。
[こちらの記事](https://qiita.com/hayao_k/items/62232e2a75ca298b2bc4)を参考にさせていただき、以下のコマンドを実行してkubectlをインストールする。

```shell:CloudShell
# ディレクトリ作成
mkdir -p $HOME/.local/bin
cd $HOME/.local/bin

# kubectl のインストール
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.13/bin/linux/amd64/kubectl
chmod +x kubectl

# ホームディレクトリに戻る
cd $HOME
```
これでKubernetesの操作に必要なツールをCloudShell上に準備することができた。

## クラスターの作成
AWSマネージメントコンソール上からクラスタを作成していく。
EKSの画面で`クラスター`を選択するとクラスター一覧が表示され、その画面上にある`クラスターを作成`ボタンを押下して作成を開始する。
各設定にて変更した部分を抜粋して以下に記載する。

### クラスターを設定
- 名前：任意のクラスタ名
- kubernetesバージョン：1.27（デフォルトを選択）
- クラスターサービスロール：
  `AmazonEKSClusterPolicy`、`AmazonEKSServicePolicy`をアタッチしたロールを作成して選択

### ネットワーキングを指定
- VPC：事前に作成したVPCを指定
- サブネット：事前に作成したサブネットを2つ指定
- クラスターエンドポイントアクセス：パブリック

### ログ記録の設定
　今回はすべてOFFとした。

### アドオンを選択
　デフォルトでインストールとなっている以下のみ選択
- kube-puroxy
- CoreDNS
- Amazon VPC CNI

### 選択したアドオン設定を構成する
　特に変更せずデフォルトのバージョンを選択

各設定を入力・選択後に確認画面で`作成`ボタンを押下するとクラスターの作成が開始される。
クラスター作成には数分かかるため、しばらく完了するまで待つ。
正常にクラスターが作成されると以下のようにステータスがアクティブでクラスター一覧に表示される。
![1_クラスタ作成.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/f5c1d885-8b5a-9994-9e08-827a9ca5eb21.png)

:::note info
eksctlを利用してコマンドベースでクラスター作成することも可能。
eksctlを利用する場合はツールを操作環境へインストールする必要がある。
:::

## ノードグループの作成
ノードグループはノードのプロビジョニングとライフサイクル管理を自動化してくれるもので、EKSでノードを使うためにこちらを作成する。
先ほど作成したクラスターを選択して、クラスターの詳細画面で`コンピューティング`タブを選択し、`ノードグループを追加`ボタンを押下しノードグループの設定画面を開く。
![2_ノードグループ作成.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/db04d7d5-3aab-67fe-29bb-1cbed0ccfa6e.png)

ノードグループの各設定にて変更した部分を抜粋して以下に記載する。

### ノードグループを設定
- 名前：任意の名前を設定
- ノードIAMロール：
　`mazonEKSWorkerNodePolicy`、`AmazonEC2ContainerRegistryReadOnly`、`AWSAppSyncPushToCloudWatchLogs`、`AmazonEKS_CNI_Policy`をアタッチしたロールを作成して選択

### ノードグループのコンピューティング設定
- AMIタイプ：Amazon Linux (AL2_x86_64)
- キャパシティタイプ：On-Demand
- インスタンスタイプ：t3.medium
- ディスクサイズ：20 GiB
- ノードグループのスケーリング設定
    - 希望のサイズ：1 ノード
    - 最小サイズ：1 ノード
    - 最大サイズ：1 ノード
- ノードグループの更新設定：数値 1 ノード
- ノードグループのネットワーク設定：クラスター設定指定したものと同じサブネットを指定

各設定を入力・選択後に確認画面で`作成`ボタンを押下するとノードグループおよび指定したサイズのノードの作成が開始される。
こちらに関しても作成には数分かかるため、しばらく完了するまで待つ。
![2-1_ノードグループ作成.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/24540bbc-0df3-f68a-cd50-65b351f800bf.png)

正常に作成されると上記のように1つのノードと1つのノードグループが表示される。

## クラスターへ接続
Cloud Shellから作成したクラスターの操作ができるように設定する。[こちら](https://repost.aws/ja/knowledge-center/eks-cluster-connection)を参考にして、Cloud Shell上で以下のコマンドを実行する。

```shell:CloudShell
aws eks --region example_region update-kubeconfig --name cluster_name
```
正常にコマンドが実行できると`Added new context ~`のようなメッセージが表示され、クラスターの kubeconfig ファイルが作成または更新される。
また、以下のコマンドを実行してノードの情報がみれるか確認する。
```shell:CloudShell
kubectl get node
```
ここまでの設定がすべて正しくできていると以下のようにノードの情報を確認できる。
```shell:CloudShell
NAME                                              STATUS   ROLES    AGE   VERSION
ip-192-168-4-94.ap-northeast-1.compute.internal   Ready    <none>   57m   v1.27.1-eks-2f008fe
```

# マニュフェスト作成
最初にマニュフェストファイルの全容は以下の通りで、こちらを`kubectl apply`すればALBからDBまですべて起動するようになっている。

```yaml:keycloak.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-server
spec:
  type: ClusterIP
  ports:
    - name: mysql
      port: 3306
      targetPort: 3306
      protocol: TCP
  selector:
    app: mysql-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-server
spec:
  selector:
    matchLabels:
      app: mysql-server
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql-server
    spec:
      containers:
        - image: mysql:5.7
          name: mysql
          resources:
          env:
            - name: MYSQL_USER
              value: keycloak_user
            - name: MYSQL_PASSWORD
              value: keycloak_password
            - name: MYSQL_ROOT_PASSWORD
              value: password
            - name: LANG
              value: C.UTF-8
          ports:
            - containerPort: 3306
              name: mysql
          volumeMounts:
            - name: mysql-server-initdb
              mountPath: /docker-entrypoint-initdb.d
            - name: mysql-server-conf
              mountPath: /etc/mysql/conf.d
      volumes:
        - name: mysql-server-initdb
          configMap:
            name: mysql-server-initdb-config
        - name: mysql-server-conf
          configMap:
            name: mysql-server-conf-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-server-initdb-config
data:
  createdb.sql: |
    CREATE DATABASE keycloak_db;
    GRANT ALL ON keycloak_db.* TO keycloak_user;
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-server-conf-config
data:
  custom.cnf: |
    [mysqld]
    character-set-server=utf8
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak
  type: NodePort
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak-discovery
spec:
  clusterIP: None
  ports:
    - name: "http"
      port: 8080
      protocol: "TCP"
      targetPort: 8080
  selector:
    app: keycloak
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  replicas: 2
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      containers:
        - name: keycloak
          image: quay.io/keycloak/keycloak:21.1.1
          args: ["start"]
          env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
            - name: KC_PROXY
              value: "edge"
            - name: KC_HTTP_ENABLED
              value: "true"
            - name: KC_HOSTNAME_STRICT
              value: "false"
            - name: KC_DB
              value: "mysql"
            - name: KC_DB_URL
              value: "jdbc:mysql://mysql-server:3306/keycloak_db"
            - name: KC_DB_USERNAME
              value: "keycloak_user"
            - name: KC_DB_PASSWORD
              value: "keycloak_password"
            - name: KC_CACHE
              value: "ispn"
            - name: KC_CACHE_STACK
              value: "kubernetes"
            - name: KC_HEALTH_ENABLED
              value: "true"
            - name: JAVA_OPTS_APPEND
              value: "-Djgroups.dns.query=keycloak-discovery.default.svc.cluster.local"
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /realms/master
              port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: keycloak-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/subnets: subnet-xxx, subnet-xxx #your subnets
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS" :443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:xxxxxx:certificate/xxxx #your cert arn
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: keycloak
                port:
                  number: 8080

```
非常に長いので個別に分けて説明していく。
### MySQL
MySQLのコンテナとサービスを作成に関する記載は以下の通り。
MySQLはクラスター内でのみサービスを提供できれば良いため、サービスのタイプを`ClusterIP`としている。
```yaml
kind: Service
metadata:
  name: mysql-server
spec:
  type: ClusterIP
  ports:
    - name: mysql
      port: 3306
      targetPort: 3306
      protocol: TCP
  selector:
    app: mysql-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-server
spec:
  selector:
    matchLabels:
      app: mysql-server
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql-server
    spec:
      containers:
        - image: mysql:5.7
          name: mysql
          resources:
          env:
            - name: MYSQL_USER
              value: keycloak_user
            - name: MYSQL_PASSWORD
              value: keycloak_password
            - name: MYSQL_ROOT_PASSWORD
              value: password
            - name: LANG
              value: C.UTF-8
          ports:
            - containerPort: 3306
              name: mysql
          volumeMounts:
            - name: mysql-server-initdb
              mountPath: /docker-entrypoint-initdb.d
            - name: mysql-server-conf
              mountPath: /etc/mysql/conf.d
      volumes:
        - name: mysql-server-initdb
          configMap:
            name: mysql-server-initdb-config
        - name: mysql-server-conf
          configMap:
            name: mysql-server-conf-config
```
また、以下のように`ConfigMap`上にDBの初期設定とクエリを登録して、MySQL起動時に初期DBと初期ユーザーを作成するように設定。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-server-initdb-config
data:
  createdb.sql: |
    CREATE DATABASE keycloak_db;
    GRANT ALL ON keycloak_db.* TO keycloak_user;
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-server-conf-config
data:
  custom.cnf: |
    [mysqld]
    character-set-server=utf8
```

### Keycloak
Keycloakのコンテナとサービスを作成に関する記載は以下の通り。
クラスター構成を確認するため、`replicas`の設定を`2`にしてKeycloakの2台起動するように設定。
また、Keycloakノード間通信は「DNS_PING」を利用するため、`KC_CACHE_STACK`を`kubernetes`と`JAVA_OPTS_APPEND`で`jgroups.dns.query`を設定。こちらに設定する内容は次に説明するHeadless servicesを利用する。
```yaml
apiVersion: v1
kind: Service
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak
  type: NodePort
---yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  replicas: 2
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      containers:
        - name: keycloak
          image: quay.io/keycloak/keycloak:21.1.1
          args: ["start"]
          env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
            - name: KC_PROXY
              value: "edge"
            - name: KC_HTTP_ENABLED
              value: "true"
            - name: KC_HOSTNAME_STRICT
              value: "false"
            - name: KC_DB
              value: "mysql"
            - name: KC_DB_URL
              value: "jdbc:mysql://mysql-server:3306/keycloak_db"
            - name: KC_DB_USERNAME
              value: "keycloak_user"
            - name: KC_DB_PASSWORD
              value: "keycloak_password"
            - name: KC_CACHE
              value: "ispn"
            - name: KC_CACHE_STACK
              value: "kubernetes"
            - name: KC_HEALTH_ENABLED
              value: "true"
            - name: JAVA_OPTS_APPEND
              value: "-Djgroups.dns.query=keycloak-discovery.default.svc.cluster.local"
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /realms/master
              port: 8080
```

### Headless services
Headless servicesでは、各Pod の IP アドレスを DNS ラウンドロビンで返却できるため、こちらをKeycloakノード間通信は「DNS_PING」に利用する。
Headless servicesは`clusterIP: None`を設定することで利用可能となる。
```yaml
apiVersion: v1
kind: Service
metadata:
  name: keycloak-discovery
spec:
  clusterIP: None
  ports:
    - name: "http"
      port: 8080
      protocol: "TCP"
      targetPort: 8080
  selector:
    app: keycloak
```

### Ingress
IngressはAWSのALBを利用するため、`ingressClassName: alb`を設定。
また、ALBをHTTPSで利用可能とするため、事前に発行した証明書をACMへインポートしておきマニュフェストで指定している。
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: keycloak-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/subnets: subnet-xxx, subnet-xxx #your subnets
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS" :443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:xxxxxx:certificate/xxxx #your cert arn
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: keycloak
                port:
                  number: 8080
```

# デプロイ
## AWS Load Balancer Controller アドオンのインストール
ALBを使ってKubernetesのIngressを作成する場合にこちらのアドオンをインストールしておく必要があるため、[AWS公式の手順](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/aws-load-balancer-controller.html)に沿ってAWS Load Balancer Controller アドオンをインストールする。

## デプロイ
作成したマニュフェストファイルをCloud Shell上にアップロード(もしくはShell上で作成)してデプロイする。
以下のコマンドを実行することでマニュフェストに記載した内容を順次デプロイされる。
```shell:CloudShell
kubectl apply -f keycloak.yaml
```
マニュフェストの内容が正しい形式であれば、以下のようにそれぞれが作成される旨の出力が返ってくる。
```shell:CloudShell
service/mysql-server created
deployment.apps/mysql-server created
configmap/mysql-server-initdb-config created
configmap/mysql-server-conf-config created
service/keycloak created
service/keycloak-discovery created
deployment.apps/keycloak created
ingress.networking.k8s.io/keycloak-ingress created
```

正常にデプロイされているか以下のコマンドを実行して確認する。
```shell:CloudShell
kubectl get all
```
以下のようにすべてのPODのSTATUSがRunningになっていればIngress(ALB)以外のデプロイはOK。
```shell:CloudShell
NAME                                READY   STATUS    RESTARTS   AGE
pod/keycloak-7d8f8949df-67894       1/1     Running   0          2m6s
pod/keycloak-7d8f8949df-zcx2g       1/1     Running   0          2m6s
pod/mysql-server-6c8d866586-s42rb   1/1     Running   0          2m6s

NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/keycloak             NodePort    10.100.3.101    <none>        8080:32549/TCP   2m6s
service/keycloak-discovery   ClusterIP   None            <none>        8080/TCP         2m6s
service/kubernetes           ClusterIP   10.100.0.1      <none>        443/TCP          6h28m
service/mysql-server         ClusterIP   10.100.106.59   <none>        3306/TCP         2m6s

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/keycloak       2/2     2            2           2m6s
deployment.apps/mysql-server   1/1     1            1           2m6s

NAME                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/keycloak-7d8f8949df       2         2         2       2m6s
replicaset.apps/mysql-server-6c8d866586   1         1         1       2m6s
```
次にIngressの状況を以下のコマンドで確認する

```shell:CloudShell
kubectl get ingress
```
以下のように表示されればIngressの作成も成功。ただし、ALBのサービスがActiveになるまでには少し時間がかかるため、適宜コンソール上でALBがActiveになっているかは確認が必要。
```shell:CloudShell
NAME               CLASS   HOSTS   ADDRESS                                                                       PORTS   AGE
keycloak-ingress   alb     *       k8s-default-keycloak-xxxxxxxxxx-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com   80      5m29s
```

# 動作検証
[前回のECSの時](https://qiita.com/s-takino/items/8d3c0c84253533292c91)と同様に確認していく、
まず、コンテナ起動時に2つのコンテナが正しくクラスタを実現できているか確認。
以下のコマンドを実行してコンテナの起動時のログを確認していく。
```shell:CloudShell
kubectl log コンテナ名
```

以下が出力されたコンテナログの抜粋であり、メンバーとして2つのコンテナ名が出力されている。
```shell:CloudShell
2023-06-02 08:14:38,055 INFO  [org.infinispan.LIFECYCLE] (jgroups-16,keycloak-7d8f8949df-67894-17810) [Context=offlineSessions] ISPN100002: Starting rebalance with members [keycloak-7d8f8949df-67894-17810, keycloak-7d8f8949df-zcx2g-58694], phase READ_OLD_WRITE_ALL, topology id 4
2023-06-02 08:14:38,057 INFO  [org.infinispan.LIFECYCLE] (jgroups-16,keycloak-7d8f8949df-67894-17810) [Context=offlineSessions] ISPN100010: Finished rebalance with members [keycloak-7d8f8949df-67894-17810, keycloak-7d8f8949df-zcx2g-58694], topology id 4
```

次に、Keycloakのアプリの動作確認を行う。
こちらもECSの時と同様にアプリの動作確認には、Keycloakに付属するログインユーザの個人情報画面を利用する。

　1. ALBのDNS名+/realms/master/account/#/personal-infoにアクセスし、ログイン画面を表示
　2. admin でログイン(パスワードも同じ)
　3. 個人情報画面が表示される
　4. マニュフェストファイルのKeyclaokのreplicasの値を1に変更して`kubectl apply`を実行する。
　5. 再ログインが発生しないで個人情報画面内を遷移できることを確認
　6. マニュフェストファイルのKeyclaokのreplicasの値を2に戻して`kubectl apply`を実行する。
　7. 再ログインが発生しないで個人情報画面内を遷移できることを確認
　8. マニュフェストファイルのKeyclaokのreplicasの値を1に変更して`kubectl apply`を実行する。
　9. 再ログイン発生しないでプロフィール画面内を遷移できることを確認

上記のような操作をし、Podのreplicasを変更しても再ログイン操作が発生せずに操作可能であり、クラスタ構成が正常に動作していることが確認できた

# 参考資料
https://www.keycloak.org/getting-started/getting-started-kube
https://repost.aws/ja/knowledge-center/eks-cluster-connection
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/alb-ingress.html