🔑

GKE に 冗長構成の Keycloak を構築して HTTPS で公開する

2025/02/03に公開

こんにちは!クラウドエースの kazz です。
この記事では 冗長構成の Keycloak を Google Kubernetes Engine(以下、GKE)上で構築し、HTTPS で公開する手順を解説します。

前提条件

  • gcloud コマンドを実行できる環境がある
  • DNS レコードの設定ができるドメインを取得している

システム構成

image01

冗長構成にすることで、いずれかのサーバーが停止しても、他のサーバーで処理を引き継いで継続できるため、高い可用性を維持できます。

準備

GKE に Keycloak を構築する前に必要なリソースの準備をします。

証明書

GKE の Gateway で利用する SSL 証明書を作成します。

gcloud compute ssl-certificates create keycloak-cert \
    --domains=YOUR_DOMAIN

SQL

Keycloak の情報を保持するための Database(以下、DB)を作成します。
今回は Cloud SQL の MySQL を利用します。

gcloud sql instances create keycloak-sql \
    --region=asia-northeast1 \
    --database-version=MYSQL_8_0 \
    --edition=ENTERPRISE \
    --tier=db-g1-small \
    --root-password=password

SQL インスタンスを作成できたら、データベースを作成します。

gcloud sql databases create keycloak-db --instance=keycloak-sql

Keycloak から接続する際に使用するユーザーを作成します。

gcloud sql users create keycloak \
--instance=keycloak-sql \
--password=password

Workload Identity の設定

GKE から Cloud SQL への接続は、Cloud SQL Auth Proxy を実行して接続します。
Workload Identity で、Pod が使用する Service Account(以下、SA)が SQL 管理者の権限を利用できるよう準備をしておきます。

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--role="roles/cloudsql.admin" \
--member="principal://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_PROJECT_ID.svc.id.goog/subject/ns/default/sa/keycloak-sa"

GKE クラスタ作成

Autopilot クラスタを作成します。
Standard クラスタを使用する場合は、Workload Identity がデフォルトで有効になっていないことに注意してください。

gcloud container clusters create-auto keycloak-cluster --region=asia-northeast1

作成できたら GKE クラスタに接続しておきましょう。

gcloud container clusters get-credentials keycloak-cluster --region asia-northeast1

マニフェスト作成

1 つのマニフェストファイルにしてしまうと非常に長くなるので、分割しながら解説していきます。

Gateway + HTTPRoute

Gateway と HTTPRoute を作成し、ロードバランサを通してインターネットから Keycloak へ HTTPS アクセスできるようにします。

HTTPRoute の hostnames は、準備で作成した証明書と同じドメインを設定してください。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: keycloak-gateway
spec:
  gatewayClassName: gke-l7-global-external-managed
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        options:
          networking.gke.io/pre-shared-certs: keycloak-cert
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: keycloak
  labels:
    gateway: keycloak-gateway
spec:
  parentRefs:
    - name: keycloak-gateway
  hostnames:
   - "YOUR_DOMAIN"
  rules:
    - backendRefs:
        - name: keycloak
          port: 8080

HealthCheckPolicy

Keycloak はヘルスチェックを有効化すると、デフォルトでポート 9000 に専用のエンドポイントを公開します。
https://www.keycloak.org/observability/health

apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
metadata:
  name: healthcheck
spec:
  default:
    checkIntervalSec: 15
    timeoutSec: 15
    healthyThreshold: 1
    unhealthyThreshold: 2
    config:
      type: HTTP
      httpHealthCheck:
        port: 9000
        requestPath: /health
  targetRef:
    group: ""
    kind: Service
    name: keycloak

Service

Keycloak のサービスに加えて、Headless Service を作成します。

Headless Service が必要な理由

Keycloak は Infinispan を使用することで、複数のインスタンス間でキャッシュを分散することが可能です。この Infinispan は JGroups を利用して複数の Keycloak インスタンス間で通信を行います。

Kubernetes における JGroups では、クラスタのメンバーを検出するために DNS_PING プロトコルを使用します。このプロトコルは、DNS Aレコードや SRV レコードを利用して、ポッドの状態やクラスタのメンバーを確認します。
http://www.jgroups.org/manual4/index.html#_dns_ping

Kubernetes には Headless Service という、ClusterIP を持たないサービスがあります。このサービスを使って名前解決を行うと、各ポッドのアドレスを得ることができます。
https://kubernetes.io/ja/docs/concepts/services-networking/service/#headless-service

つまり、Headless Service を作成することによって、JGroups が DNS を使用してクラスタメンバーを探せるようになり、Infinispan で分散キャッシュができるようになります。

apiVersion: v1
kind: Service
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak-headless-service
spec:
  clusterIP: None
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak

ServiceAccount

Workload Identity で使用する SA を作成します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: keycloak-sa

Deployment

最後に Deployment を作成します。
KC_HOSTNAME と cloud-sql-proxy コンテナの args にある YOUR_PROJECT_ID は各々の値に変更してください。

Kubernetes で冗長構成を実装する際のポイント

  • KC_CACHEispn にすることで、キャッシュとして Infinispan を使用し、分散キャッシュ機能を有効にします。
    デフォルトで ispn になっていますが、今回は明示的に設定しています。

  • KC_CACHE_STACKkubernetes にすることで、Infinispan は JGroups の DNS_PING プロトコルを使用して、クラスタ内の他の Keycloak インスタンスを 探します。

  • JAVA_OPTS_APPEND に Headless Service を設定することで、JGroups は指定した Headless Service を通じて、クラスタ内の Keycloak インスタンスを DNS で検出します。
    -Djgroups.dns.query={headless service name}.{namespace}.svc.cluster.local

サイドカー パターン の Cloud SQL Auth Proxy

Cloud SQL に接続するために、サイドカー で Cloud SQL Auth Proxy を実行します。
https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine?hl=ja#run_the_in_a_sidecar_pattern

apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  replicas: 3
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      serviceAccountName: keycloak-sa
      containers:
        - name: keycloak
          image: quay.io/keycloak/keycloak:26.0.0
          command: ["/bin/sh"]
          args:
            - "-c"
            - |
              exec /opt/keycloak/bin/kc.sh start
          env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
            - name: KC_PROXY
              value: "edge"
            - name: KC_HOSTNAME
              value: "https://YOUR_DOMAIN"
            - name: KC_HEALTH_ENABLED
              value: "true"
            - name: KC_HTTP_ENABLED
              value: "true"
            - name: KC_DB
              value: "mysql"
            - name: KC_DB_URL
              value: "jdbc:mysql://127.0.0.1:3306/keycloak-db"
            - name: KC_DB_USERNAME
              value: "keycloak"
            - name: KC_DB_PASSWORD
              value: "password"
            - name: KC_CACHE
              value: "ispn"
            - name: KC_CACHE_STACK
              value: "kubernetes"
            - name: JAVA_OPTS_APPEND
              value: "-Djgroups.dns.query=keycloak-headless-service.default.svc.cluster.local"
          ports:
            - name: http
              containerPort: 8080
          startupProbe:
            httpGet:
              path : /health/started
              port: 9000
            periodSeconds: 120
            failureThreshold: 30
          livenessProbe:
            httpGet:
              path: /health/live
              port: 9000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 9000
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1
          args:
            - "--port=3306"
            - "YOUR_PROJECT_ID:asia-northeast1:keycloak-sql"
            - "--structured-logs"
          securityContext:
            runAsNonRoot: true

デプロイ

それでは作成したマニフェストを apply します。
上記で分割したマニフェストを keycloak.yaml にまとめてから実行します。ファイルを分けたい方はそれぞれ apply してください。

kubectl apply -f keycloak.yaml

2、3 分待った後、正常にデプロイできたか確認します。

$ kubectl get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/keycloak-567854899c-6n6vq   2/2     Running   0          2m26s
pod/keycloak-567854899c-sxfbd   2/2     Running   0          2m26s
pod/keycloak-567854899c-zgdsd   2/2     Running   0          2m27s

NAME                                TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/keycloak                    ClusterIP   34.0.0.0     <none>        8080/TCP   2m27s
service/keycloak-headless-service   ClusterIP   None         <none>        8080/TCP   2m27s
service/kubernetes                  ClusterIP   34.0.0.0     <none>        443/TCP    34m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/keycloak   3/3     3            3           2m27s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/keycloak-567854899c   3         3         3       2m27s

Keycloak が正常にデプロイできていることを確認したら、Gateway に付与された IP アドレスの値を調べます。

$ kubectl get gateway
NAME               CLASS                            ADDRESS    PROGRAMMED   AGE
keycloak-gateway   gke-l7-global-external-managed   34.0.0.0   True         5m19s

この IP アドレスをドメインの A レコードとして DNS に設定します。

レコードを設定してしばらく待つと、証明書の状態が ACTIVE になります。
ACTIVE にならないときは、一旦証明書をコンソールなどから削除して再作成してみてください。

$ gcloud compute ssl-certificates describe keycloak-certs --format="get(managed.status)"
ACTIVE

動作確認

証明書に設定したドメインにアクセスします。
正常にデプロイできていたら、以下のような画面が表示されます。
KEYCLOAK_ADMINKEYCLOAK_ADMIN_PASSWORD に設定した値を入力すると管理コンソールへ入れます。
image02

冗長構成の確認

最後に、冗長構成がうまくできているかを確認します。

$ kubectl get pod
NAME                        READY   STATUS    RESTARTS   AGE
keycloak-567854899c-6n6vq   2/2     Running   0          14m
keycloak-567854899c-sxfbd   2/2     Running   0          14m
keycloak-567854899c-zgdsd   2/2     Running   0          14m

どれかの Pod のログを確認します。

kubectl logs keycloak-567854899c-6n6vq -c keycloak

大量のログが出ていますが、以下のようなログが出ていたらクラスタリングはうまくいっています。

2025-01-30 03:08:54,865 INFO  [org.infinispan.CLUSTER] (jgroups-10,keycloak-567854899c-sxfbd-11327) ISPN000093: Received new, MERGED cluster view for channel ISPN: MergeView::[keycloak-567854899c-sxfbd-11327|1] (3) [keycloak-567854899c-sxfbd-11327, keycloak-567854899c-zgdsd-44294, keycloak-567854899c-6n6vq-8174], 3 subgroups: [keycloak-567854899c-zgdsd-44294|0] (1) [keycloak-567854899c-zgdsd-44294], [keycloak-567854899c-6n6vq-8174|0] (1) [keycloak-567854899c-6n6vq-8174], [keycloak-567854899c-sxfbd-11327|0] (1) [keycloak-567854899c-sxfbd-11327]

このログには、Infinispan で以下のクラスタがマージされたことが示されています。

  • keycloak-567854899c-sxfbd-11327
  • keycloak-567854899c-zgdsd-44294
  • keycloak-567854899c-6n6vq-8174

これにより、 Keycloak インスタンスが 1 つのクラスタとして動作を開始したことが確認できます。

おわりに

本記事では GKE に 冗長構成の Keycloak を構築する手順を紹介しました。
Headless Service 周りが少し複雑ですが、冗長構成にすることで高い可用性を確保できます。
この手順が、Keycloak の構築に役立てば幸いです。

Discussion