Keycloak on Kubernetes
はじめに
こんにちは。
フォルシア株式会社エンジニアの籏野です。
この度新規アプリを構築するにあたって、認証を通してからアプリにアクセスできるようにする必要が出てきました。
認証アプリにはKeycloakを利用し、Kubernetes(EKS)上にアプリをデプロイしています。
Kubernetes上にKeycloakアプリをデプロイするにあたり対応した内容を紹介したいと思います。
前準備
データベースの用意
Keycloakを利用するには、ユーザー情報等を保持しておくためのDBを用意する必要があります。
今回はAWSのマネージドサービスを利用できるため、CloudFormationを使ってAmazonRDSのインスタンスをサクッと用意しました。
※本番アプリで利用する際には暗号化設定等適宜行ってください。
関連する部分を抜粋すると以下のようになります。
# RDSインスタンス作成のための設定ファイル
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
BaseName:
Type: String
Default: keycloak
DBMasterUserName:
Type: String
Default: postgres
NoEcho: true
DBPassword:
Default: "dbpassword"
NoEcho: true
Type: String
MinLength: 8
MaxLength: 41
AllowedPattern: "[a-zA-Z0-9]*"
ConstraintDescription: "must contain only alphanumeric characters."
Resources:
DBInstance:
Type: "AWS::RDS::DBInstance"
DeletionPolicy: "Delete"
Properties:
DBInstanceIdentifier: !Sub ${BaseName}-DB
Engine: postgres
EngineVersion: 13.4
DBInstanceClass: db.t3.micro
AllocatedStorage: 20
StorageType: gp2
MasterUsername: !Ref DBMasterUserName
MasterUserPassword: !Ref DBPassword
DBSubnetGroupName: !Ref DBSubnetGroup
PubliclyAccessible: false
MultiAZ: true
AutoMinorVersionUpgrade: false
DBParameterGroupName: !Ref DBParameterGroup
VPCSecurityGroups:
- !Ref DBSecurityGroup
BackupRetentionPeriod: 7
Tags:
- Key: "Name"
Value: !Sub ${BaseName}-DB
DBSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupName: !Sub "${BaseName}-DB-SUBNET"
DBSubnetGroupDescription: "-"
# EKS利用のために作成したサブネットIDを指定
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
- !Ref PrivateSubnet3
DBParameterGroup:
Type: "AWS::RDS::DBParameterGroup"
Properties:
Family: !Sub "Postgres13"
Description: !Sub "${BaseName}-DB-PARAM"
DBSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: !Sub ${BaseName}-DB-SG
# EKSが利用するVPCのIDを指定
VpcId: !Ref EksWorkVPC
Tags:
- Key: 'Name'
Value: !Sub ${BaseName}-DB-SG
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: "5432"
ToPort: "5432"
# EKSから接続できるようにIp範囲を指定
CidrIp: 172.25.0.0/16
管理者情報の生成
Keycloakをコンテナで立ち上げる際には、環境変数で管理者ID/パスワードを設定できます。
環境変数はKubernetesのマニフェストファイルに書くことになりますが、マニフェストファイルに直接ログイン情報を記載してリポジトリに登録してしまうのはあまりよくありません。
今回はKubernetesのsecretを用いてKubernetes上にログイン情報を保持しておく形にしました。
※Secretに登録している情報はbase64でエンコードされているだけの値になります。
今回作成するアプリではKubernetesリソースにアクセスできるメンバーを制限しているので十分と判断しましたが、構築するアプリの状況に応じて適切に管理しましょう。
以下のようなコマンドを叩けばsecretが作成できます。
# KEYCLOAK_USERNAME/KEYCLOAK_PASSWORDは任意の値
kubectl create secret generic keycloak-secret \
--from-literal=username=${KEYCLOAK_USERNAME} \
--from-literal=password=${KEYCLOAK_PASSWORD}
Keycloakの設定
Keycloakを本番運用する際には、システム全体の可用性を確保するため冗長構成を取ることになります。
この時ユーザーのログイン状態を保持するセッション情報は、KeycloakのAPサーバーであるWildFlyに搭載されたJGroupsを用いて各ホスト間で共有されるようです。
JGroupsを用いてセッション情報を共有するには、Keycloakが動作する各サーバーのIPを読み込ませクラスタを構成する必要があります。
EC2などでオンデマンド環境に構築する場合には立ち上げたEC2のIPを調べればよいので特に問題はありません。
ただ、Kubernetes上にKeycloakを立ち上げる場合には新しいポッドが次々に立ち上がっていくため、IPが可変となってしまいます。
色々調べてみると、KubernetesのHeadless Serviceと呼ばれるものを利用する例が紹介されていました。
Headless Searviceは立ち上がったポッドの個々のIPを返してくれるServiceのようです。
これを利用することにより、Keycloakが動作する各ポッドのIPが取得できるのですね。
というわけでマニフェストファイルは以下のようになりました。
# Keycloakアプリのマニフェストファイル
---
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: keycloak-headless-svc
spec:
type: ClusterIP
selector:
app: keycloak
ports:
- port: 8080
targetPort: 8080
clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
selector:
matchLabels:
app: keycloak
replicas: 2
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: jboss/keycloak:16.1.1
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
- name: jgroups
containerPort: 7600
readinessProbe:
httpGet:
path: /auth/
scheme: HTTP
port: 8080
env:
- name: TZ
value: Asia/Tokyo
- name: DB_VENDOR
value: postgres
- name: DB_DATABASE
value: keycloak
- name: DB_USER
value: keycloak
- name: DB_PASSWORD
value: keycloak
- name: DB_ADDR
# 前準備で作成したRDSのホスト名を指定
value: keycloak.ap-northeast-1.rds.amazonaws.com
- name: DB_PORT
value: "5432"
- name: KEYCLOAK_USER
valueFrom:
secretKeyRef:
name: keycloak-secret
key: username
- name: KEYCLOAK_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secret
key: password
- name: PROXY_ADDRESS_FORWARDING
value: "true"
- name: JGROUPS_DISCOVERY_PROTOCOL
value: dns.DNS_PING
- name: JGROUPS_DISCOVERY_PROPERTIES
value: "keycloak-headless-svc"
動作チェック
上記マニフェストファイルを読み込ませて起動ログを見てみます。
先に立ち上がったポッドのログには以下のように表示されていました。
...
13:36:14,373 INFO [org.infinispan.CLUSTER] (ServerService Thread Pool -- 56) ISPN000094: Received new cluster view for channel ejb: [keycloak-5c4598646b-zm84w|0] (1) [keycloak-5c4598646b-zm84w]
...
この時点では1つしかポッドが立ち上がっていないのでクラスタに参加しているポッドは1つのようです。
では、2つ目のpodも見てみましょう。
...
13:39:02,840 INFO [org.infinispan.CLUSTER] (ServerService Thread Pool -- 60) ISPN000094: Received new cluster view for channel ejb: [keycloak-5c4598646b-zm84w|1] (2) [keycloak-5c4598646b-zm84w, keycloak-5c4598646b-kzjf9]
...
無事複数のポッドがクラスタに参加していそうです!
1つ目のポッドの方にも新たなポッドがクラスタに参加したログが出ていました。
...
13:39:01,569 INFO [org.infinispan.CLUSTER] (thread-7,ejb,keycloak-5c4598646b-zm84w) ISPN100000: Node keycloak-5c4598646b-kzjf9 joined the cluster
...
最後に
今回はKubernetes(EKS)上にKeycloakを構築する際の一例を紹介しました。
実際のアプリではさらにApacheのmod_auth_openidcを用いてKeycloakと連携したのですが、その時にも今回のような冗長構成の場合の注意点がありました。
この時の経験談もまた改めて記事にしたいと思います。
Discussion