<p>I'm trying to deploy a HA Keycloak cluster (2 nodes) on Kubernetes (GKE). So far the cluster nodes (pods) are failing to discover each other in all the cases as of what I deduced from the logs. Where the pods initiate and the service is up but they fail to see other nodes.</p>
<p>Components</p>
<ul>
<li>PostgreSQL DB deployment with a clusterIP service on the default port.</li>
<li>Keycloak Deployment of 2 nodes with the needed ports container ports 8080, 8443, a relevant clusterIP, and a service of type LoadBalancer to expose the service to the internet</li>
</ul>
<p>Logs Snippet:</p>
<pre><code>INFO  [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (MSC service thread 1-4) ISPN000078: Starting JGroups channel ejb
INFO  [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (MSC service thread 1-4) ISPN000094: Received new cluster view for channel ejb: [keycloak-567575d6f8-c5s42|0] (1) [keycloak-567575d6f8-c5s42]
INFO  [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (MSC service thread 1-1) ISPN000094: Received new cluster view for channel ejb: [keycloak-567575d6f8-c5s42|0] (1) [keycloak-567575d6f8-c5s42]
INFO  [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (MSC service thread 1-3) ISPN000094: Received new cluster view for channel ejb: [keycloak-567575d6f8-c5s42|0] (1) [keycloak-567575d6f8-c5s42]
INFO  [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (MSC service thread 1-4) ISPN000079: Channel ejb local address is keycloak-567575d6f8-c5s42, physical addresses are [127.0.0.1:55200]
.
.
.
INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 15.0.2 (WildFly Core 15.0.1.Final) started in 67547ms - Started 692 of 978 services (686 services are lazy, passive or on-demand)
INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
</code></pre>
<p><strong>And as we can see in the above logs the node sees itself as the only container/pod ID</strong></p>
<h2>Trying KUBE_PING protocol</h2>
<p>I tried using the <strong>kubernetes.KUBE_PING</strong> protocol for discovery but it didn't work and the call to the kubernetes downward API. With a <strong>403 Authorization error</strong> in the logs (BELOW IS PART OF IT):</p>
<pre><code>Server returned HTTP response code: 403 for URL: https://[SERVER_IP]:443/api/v1/namespaces/default/pods
</code></pre>
<p>At this point, I was able to log in to the portal and do the changes but it was not yet an HA cluster since changes were not replicated and the session was not preserved, in other words, if I delete the pod that I was using I was redirected to the other with a new session (as if it was a separate node)</p>
<h2>Trying DNS_PING protocol</h2>
<p>When I tried DNS_PING things were different I had no Kubernetes downward API issues but I was not able to log in.</p>
<p>In detail, I was able to reach the login page normally, but when I enter my credentials and try logging in the page tries loading but gets me back to the login page with no logs in the pods in this regard.</p>
<p>Below are some of the references I resorted to over the past couple of days:</p>
<ul>
<li><a href="https://github.com/keycloak/keycloak-containers/blob/main/server/README.md#openshift-example-with-dnsdns_ping" rel="nofollow noreferrer">https://github.com/keycloak/keycloak-containers/blob/main/server/README.md#openshift-example-with-dnsdns_ping</a></li>
<li><a href="https://github.com/keycloak/keycloak-containers/blob/main/server/README.md#clustering" rel="nofollow noreferrer">https://github.com/keycloak/keycloak-containers/blob/main/server/README.md#clustering</a></li>
<li><a href="https://www.youtube.com/watch?v=g8LVIr8KKSA" rel="nofollow noreferrer">https://www.youtube.com/watch?v=g8LVIr8KKSA</a></li>
<li><a href="https://www.keycloak.org/2019/05/keycloak-cluster-setup.html" rel="nofollow noreferrer">https://www.keycloak.org/2019/05/keycloak-cluster-setup.html</a></li>
<li><a href="https://www.keycloak.org/docs/latest/server_installation/#creating-a-keycloak-custom-resource-on-kubernetes" rel="nofollow noreferrer">https://www.keycloak.org/docs/latest/server_installation/#creating-a-keycloak-custom-resource-on-kubernetes</a></li>
</ul>
<h2>My Yaml Manifest files</h2>
<p><strong>Postgresql Deployment</strong></p>
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:13
          imagePullPolicy: IfNotPresent
          ports:
          - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              value: &quot;postgres&quot;
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
</code></pre>
<p><strong>Keycloak HA cluster Deployment</strong></p>
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  replicas: 2 
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      containers:
      - name: keycloak
        image: jboss/keycloak
        env:
            - name: KEYCLOAK_USER 
              value: admin
            - name: KEYCLOAK_PASSWORD 
              value: admin123
            - name: DB_VENDOR
              value: POSTGRES
            - name: DB_ADDR
              value: &quot;postgres&quot; 
            - name: DB_PORT
              value: &quot;5432&quot;
            - name: DB_USER
              value: &quot;postgres&quot;
            - name: DB_PASSWORD
              value: &quot;postgres&quot;
            - name: DB_SCHEMA
              value: &quot;public&quot;
            - name: DB_DATABASE
              value: &quot;keycloak&quot;
#            - name: JGROUPS_DISCOVERY_PROTOCOL
#              value: kubernetes.KUBE_PING
#            - name: JGROUPS_DISCOVERY_PROPERTIES
#              value: dump_requests=true,port_range=0,namespace=default
#              value: port_range=0,dump_requests=true
            - name: JGROUPS_DISCOVERY_PROTOCOL
              value: dns.DNS_PING
            - name: JGROUPS_DISCOVERY_PROPERTIES
              value: &quot;dns_query=keycloak&quot;
            - name: CACHE_OWNERS_COUNT
              value: '2'
            - name: CACHE_OWNERS_AUTH_SESSIONS_COUNT
              value: '2'
            - name: PROXY_ADDRESS_FORWARDING
              value: &quot;true&quot;
        ports:
            - name: http
              containerPort: 8080
            - name: https
              containerPort: 8443

---
apiVersion: v1
kind: Service
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: 8443
  selector:
    app: keycloak
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak-np
  labels:
    app: keycloak
spec:
  type: LoadBalancer 
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: 8443
  selector:
    app: keycloak
</code></pre>
<h2>IMPORTANT NOTE</h2>
<ul>
<li>I tried both protocols with and without the database setup.</li>
<li>The above yaml has all the discovery protocol combinations I tried each at a time (the ones commented)</li>
</ul>

<h3>For Keycloak 17 and newer</h3>
<p>By default, those versions use DNS_PING as the discovery mechanism for JGroups (the underlying cluster mechanism) but you still need to activate it.</p>
<p>You'll need:</p>
<ul>
<li>a <a href="https://kubernetes.io/docs/concepts/services-networking/service/#headless-services" rel="nofollow noreferrer">headless service</a> pointing to your keycloak pods (a headless service is just a normal service but with <code>ClusterIP: none</code>)</li>
<li>env <code>KC_CACHE_STACK=kubernetes</code> (to activate the kubernetes jgroup configs) and <code>JAVA_OPTS_APPEND=-Djgroups.dns.query=&lt;name-of-headless-service&gt;</code> (to tell it how to find the other keycloak pods).</li>
</ul>
<p>That way, when starting up, jgroups will issue a dns query for  (example: keycloak-headless.my_namespace.svc.cluster.local) and the response will be the IP of all pods associated to the headless service.</p>
<p>JGroups will then contact every IP in communication port and stablish the cluster.</p>
<hr />
<p><strong>UPDATE</strong> 2022-08-01: This configuration below is for the legacy version of keycloak (or versions up to 16). From 17 on Keycloak migrated to the Quarkus distribution and the configuration is different, as above.</p>
<h3>For Keycloak up to 16</h3>
<p>The way KUBE_PING works is similar to running <code>kubectl get pods</code> inside one Keycloak pod to find the other Keycloak pods' IPs and then trying to connect to them one by one. However, Keycloak does this by querying the Kubernetes API directly instead of using <code>kubectl</code>.</p>
<p>To access the Kubernetes API, Keycloak needs credentials in the form of an access token. You can pass your token directly, but this is not very secure or convenient.</p>
<p>Kubernetes has a built-in mechanism for injecting a token into a pod (or the software running inside that pod) to allow it to query the API. This is done by creating a service account, giving it the necessary permissions through a RoleBinding, and setting that account in the pod configuration.</p>
<p>The token is then mounted as a file at a known location, which is hardcoded and expected by all Kubernetes clients. When the client wants to call the API, it looks for the token at that location.</p>
<p><a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/" rel="nofollow noreferrer">You can get a deeper look at the Service Account mechanism in the documentation</a>.</p>
<p>In some situations, you may not have the necessary permissions to create RoleBindings. In this case, you can ask an administrator to create the service account and RoleBinding for you or pass your own user's token (if you have the necessary permissions) through the SA_TOKEN_FILE environment variable.</p>
<p>You can create the file using a secret or configmap, mount it to the pod, and set SA_TOKEN_FILE to the file location. Note that this method is specific to JGroups library (used by Keycloak) and <a href="https://github.com/jgroups-extras/jgroups-kubernetes/blob/master/src/main/java/org/jgroups/protocols/kubernetes/KUBE_PING.java" rel="nofollow noreferrer">the documentation is here</a>.</p>
<hr />
<p>If you do have permissions to create service accounts and RoleBindings in the cluster:</p>
<p>An example (not tested):</p>
<pre class="lang-bash prettyprint-override"><code>export TARGET_NAMESPACE=default

# convenient method to create a service account 
kubectl create serviceaccount keycloak-kubeping-service-account -n $TARGET_NAMESPACE

# No convenient method to create Role and RoleBindings
# Needed to explicitly define them.
cat &lt;&lt;EOF | kubectl apply -f -
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: keycloak-kubeping-pod-reader
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;]

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: keycloak-kubeping-api-access
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: keycloak-kubeping-pod-reader
subjects:
- kind: ServiceAccount
  name: keycloak-kubeping-service-account
  namespace: $TARGET_NAMESPACE

EOF
</code></pre>
<p>On the deployment, you set the serviceAccount:</p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
spec:
  template:
    spec:
      serviceAccount: keycloak-kubeping-service-account
      serviceAccountName: keycloak-kubeping-service-account
      containers:
      - name: keycloak
        image: jboss/keycloak
        env:
#          ...
            - name: JGROUPS_DISCOVERY_PROTOCOL
              value: kubernetes.KUBE_PING
            - name: JGROUPS_DISCOVERY_PROPERTIES
              value: dump_requests=true
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
#          ...
</code></pre>
<p><code>dump_requests=true</code> will help you debug Kubernetes requests. Better to have it <code>false</code> in production. You can use <code>namespace=&lt;yournamespace</code> instead of <code>KUBERNETES_NAMESPACE</code>, but that's a handy way the pod has to autodetect the namespace it's running at.</p>
<p>Please note that KUBE_PING will find all pods in the namespace, not only keycloak pods, and will try to connect to all of them. Of course, if your other pods don't care about that, it's OK.</p>
