これは、なにをしたくて書いたもの?
KeycloakでRealmやユーザーを定義するのにWeb UIを使えますが、何度も同じことを操作するのはやや面倒ですし、やり方を忘れます。
管理CLIでも操作できますが、できればリソース定義のような形でやれたらと思うものです。
Keycloak 19.0の管理CLIを使ってみる - CLOVER🍀
Terraformで調べてみると、Keycloak Providerがあるようなので試してみることにしました。
Terraform Keycloak Provider
TerraformのKeycloak Providerのドキュメントはこちら。
GitHub - mrparkers/terraform-provider-keycloak: Terraform provider for Keycloak
ただ、メンテナンスの状態はちょっと微妙なところがあったりします…。
基本的な機能を使う分には問題なさそうなので、そのまま進めることにします。
Keycloak ProviderからKeycloakを操作する方法としては、Client CredentialsとPassword Grantの2種類があるようです。
推奨はClient Credentialsのようなのですが、今回はまずは簡単に使えそうなPassword Grantの方で扱ってみたいと思います。
Keycloak Provider / Keycloak Setup
リソース定義するお題は、こちらを焼き直したいと思います。
Keycloak+WildFlyのElytron OpenID Connect ClientサブシステムでOpenID Connect - CLOVER🍀
Reamをひとつ作成し、その中にユーザーを2人作成して、それぞれ専用のロールを割り当てます。
環境
今回の環境はこちら。
Terraform。
$ terraform version Terraform v1.9.5 on linux_amd64
Keycloak。
$ bin/kc.sh --version Keycloak 25.0.4 JVM: 21.0.4 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.4+7-LTS) OS: Linux 5.15.0-119-generic amd64
Keycloakは172.17.0.2で動作しているものとし、以下のコマンドで起動させておきます。
$ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password bin/kc.sh start-dev
管理ユーザーはadmin / passwordですね。
動作確認に使うJavaアプリケーションを作成、実行する環境。
$ java --version openjdk 21.0.4 2024-07-16 OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04) OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-119-generic", arch: "amd64", family: "unix"
TerraformでKeycloakのリソースを作成する
まずはKeycloak Providerを使って、TerraformでKeycloakのリソースを作成します。
TerraformとKeycloak Providerのバージョンを固定。
versions.tf
terraform { required_version = "1.9.5" required_providers { keycloak = { source = "mrparkers/keycloak" version = "4.4.0" } } }
リソース定義。
main.tf
provider "keycloak" { client_id = "admin-cli" username = "admin" password = "password" url = "http://172.17.0.2:8080" } ## Realm resource "keycloak_realm" "sample_realm" { realm = "sample-realm" display_name = "Sample Realm" enabled = true } ## Client resource "keycloak_openid_client" "sample_client" { realm_id = keycloak_realm.sample_realm.id client_id = "sample-client" name = "Sample Client" enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = true # テスト用 root_url = "http://localhost:8080" valid_redirect_uris = ["http://localhost:8080/*"] } ## User resource "keycloak_user" "my_user" { realm_id = keycloak_realm.sample_realm.id username = "my-user" enabled = true initial_password { value = "password" temporary = false } } resource "keycloak_user" "test_user" { realm_id = keycloak_realm.sample_realm.id username = "test-user" enabled = true initial_password { value = "password" temporary = false } } # Role resource "keycloak_role" "my_role" { realm_id = keycloak_realm.sample_realm.id name = "my-role" } resource "keycloak_role" "test_role" { realm_id = keycloak_realm.sample_realm.id name = "test-role" } # Assign Role resource "keycloak_user_roles" "my_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.my_user.id role_ids = [ keycloak_role.my_role.id ] } resource "keycloak_user_roles" "test_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.test_user.id role_ids = [ keycloak_role.test_role.id ] }
少し説明していきましょう。
Providerを使うための設定は、Password Grantを使っています。KeycloakのURLおよび、管理ユーザーのアカウントの情報を設定します。
provider "keycloak" { client_id = "admin-cli" username = "admin" password = "password" url = "http://172.17.0.2:8080" }
Keycloak Provider / Keycloak Setup
client_idはadmin-cliを使用します。こちらのことですね。

Realmの定義。
## Realm resource "keycloak_realm" "sample_realm" { realm = "sample-realm" display_name = "Sample Realm" enabled = true }
今回は変わったものはありませんが、他のリソースを作成する時にはこのRealmのidが必要になります。
クライアントの定義。access_typeやどのフローを有効にするかは明示的に指定する必要があります。
## Client resource "keycloak_openid_client" "sample_client" { realm_id = keycloak_realm.sample_realm.id client_id = "sample-client" name = "Sample Client" enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = true # テスト用 root_url = "http://localhost:8080" valid_redirect_uris = ["http://localhost:8080/*"] }
keycloak_openid_client Resource
direct_access_grants_enabledは今回は動作確認用にtrueにしました。
ユーザー定義。my-userとtest-userの2人を作成しています。
## User resource "keycloak_user" "my_user" { realm_id = keycloak_realm.sample_realm.id username = "my-user" enabled = true initial_password { value = "password" temporary = false } } resource "keycloak_user" "test_user" { realm_id = keycloak_realm.sample_realm.id username = "test-user" enabled = true initial_password { value = "password" temporary = false } }
ロール定義。my-roleとtest-roleの2つを作成しています。
# Role resource "keycloak_role" "my_role" { realm_id = keycloak_realm.sample_realm.id name = "my-role" } resource "keycloak_role" "test_role" { realm_id = keycloak_realm.sample_realm.id name = "test-role" }
ユーザーとロールの紐付け。my-roleをmy-userに、test-roleをtest-userにそれぞれ割り当てています。
# Assign Role resource "keycloak_user_roles" "my_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.my_user.id role_ids = [ keycloak_role.my_role.id ] } resource "keycloak_user_roles" "test_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.test_user.id role_ids = [ keycloak_role.test_role.id ] }
これでリソースを作成します。
$ terraform init $ terraform apply
確認すると、Realmおよび関連するリソースが作成されています。

OKですね。
動作確認用のアプリケーションを作成する
作成したリソースを使った動作確認を行います。確認用のアプリケーションを作成しましょう。
アプリケーションも、こちらの焼き直しです。
Keycloak+WildFlyのElytron OpenID Connect ClientサブシステムでOpenID Connect - CLOVER🍀
作った時はJakarta EE 8だったので、Jakarta EE 10向けに少し書き直します。アプリケーションはWildFlyを使いますが、WildFly Maven Pluginで
Bootable JARを作成することにします。
pom.xml。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>wildfly33-elytron-oidc-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>5.0.0.Final</version> <executions> <execution> <goals> <goal>package</goal> </goals> </execution> </executions> <configuration> <bootable-jar>true</bootable-jar> <bootable-jar-name>${project.artifactId}-${project.version}-server-bootable.jar</bootable-jar-name> <overwrite-provisioned-server>true</overwrite-provisioned-server> <discover-provisioning-info> <version>33.0.1.Final</version> </discover-provisioning-info> </configuration> </plugin> </plugins> </build> </project>
Jakarta RESTful Web Services(JAX-RS)の有効化。
src/main/java/org/littlewings/keycloak/wildfly/JaxrsActivator.java
package org.littlewings.keycloak.wildfly; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
リソースクラスの定義。
src/main/java/org/littlewings/keycloak/wildfly/SampleResource.java
package org.littlewings.keycloak.wildfly; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import java.security.Principal; @Path("/") @ApplicationScoped public class SampleResource { @Inject private SecurityContext securityContext; @GET @Path("hello") @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello Keycloak and WildFly!!"; } @GET @Path("my-role/principal-name") @Produces(MediaType.TEXT_PLAIN) public String principal() { Principal principal = securityContext.getUserPrincipal(); return principal.getName(); } @GET @Path("test-role/message") @Produces(MediaType.TEXT_PLAIN) public String message() { return "Authenticated!!"; } }
上から順に、以下の用途で使います。
- 認証なしでアクセスできるエンドポイント
- ユーザーがロールmy-roleを保持している場合にアクセスできるエンドポイント
- ユーザーがロールtest-roleを保持している場合にアクセスできるエンドポイント
その認可設定となるように、web.xmlを設定します。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <security-constraint> <web-resource-collection> <web-resource-name>my-role resource collection</web-resource-name> <url-pattern>/my-role/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>my-role</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>test-role resource collection</web-resource-name> <url-pattern>/test-role/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>test-role</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>OIDC</auth-method> </login-config> <security-role> <role-name>my-role</role-name> </security-role> <security-role> <role-name>test-role</role-name> </security-role> </web-app>
auth-methodがOIDCになっているところがポイントですね。
OpenID Connectを使うにあたって今回使用するのは、WildFly Elytron OpenID Connect Clientサブシステムです。
WEB-INF配下にoidc.jsonというファイルを作成する必要があります。
src/main/webapp/WEB-INF/oidc.json
{ "client-id" : "sample-client", "credentials" : { "secret" : "VX5D3RmlA9ziwA5tbKqQi1w1S7K6g24m" }, "provider-url" : "http://172.17.0.2:8080/realms/sample-realm", "principal-attribute" : "preferred_username", "ssl-required" : "external" }
client-idはTerraformで作成したクライアントのid、provider-urlは同じくTerraformで作成したKeycloakのRealmへのURLを指定します。
secret(Client Secret)はKeycloakの画面で確認して設定しましょう。

もしくは管理CLIで確認してもよいと思います。
$ bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password password $ bin/kcadm.sh get clients -r sample-realm -q 'clientId=sample-client' -F secret
Terraformのoutputに指定してもいいのですが、sensitive扱いになっているので値の確認には使わない方が良いでしょう。
パッケージング。
$ mvn package
WildFly Glowが選択したレイヤー。
[INFO] --- wildfly:5.0.0.Final:package (default) @ wildfly33-elytron-oidc-example --- [INFO] Glow is scanning... [INFO] Glow scanning DONE. [INFO] context: bare-metal [INFO] enabled profile: none [INFO] galleon discovery [INFO] - feature-packs org.wildfly:wildfly-galleon-pack:33.0.1.Final - layers ee-core-profile-server jaxrs elytron-oidc-client [INFO] Some suggestions have been found. You could enable suggestions with the <suggest>true</suggest> option.
ビルドできたので、起動します。
$ java -jar target/wildfly33-elytron-oidc-example-0.0.1-SNAPSHOT-server-bootable.jar
まずは認証不要なエンドポイントにアクセス。
$ curl localhost:8080/hello Hello Keycloak and WildFly!!
次に、ブラウザでhttp://localhost:8080/my-role/principal-nameにアクセスします。
すると、Keycloakのログイン画面にリダイレクトされるので、ユーザー名とパスワードを入力します。

すると、メールアドレスと名前を入力するように言われるので入力します…。

すると、my-roleを保持しているユーザー向けのエンドポイントにアクセスできました。

test-roleを必要とするhttp://localhost:8080/test-role/messageにはアクセスできません。
ところで、最初にログインしようとした時にメールアドレスなどを求められたことについてですが、これはUser Profileのデフォルトの設定が
理由です。
ここでemail、firstName、lastNameの3つが必須のフィールドとして定義されているからです。


以前はなかったようですがKeycloak 23でプレビューに、Keycloak 24で機能として正式に含まれるようになったみたいです。
Keycloak 23.0.0 released - Keycloak
Keycloak 24.0.0 released - Keycloak
ドキュメントはこちら。
Server Administration Guide / Managing users / Managing user attributes
Keycloak Providerで必須ではないようにしたかったのですが、今は難しそうです(後述します)。
初回ログイン時に値を設定する、Terraformでリソースを定義する時に値を一緒に入れてしまう、Web UIで各フィールドを必須ではないようにする
といった対応がありますが、今回は各フィールドの必須定義を外すことにしました。
話を戻して。次は現在ログインしているmy-userをKeycloakからログアウトさせてブラウザ上のCookieを削除したうえで、アクセスするのに
test-roleが必要なhttp://localhost:8080/test-role/messageにアクセスしてみます。
ログアウトする機能は作っていないので、Keycloakからログアウトさせるにはこちらから。

test-userでログイン。

対象のエンドポイントにアクセスできました。

アクセスするのにmy-roleが必要なhttp://localhost:8080/my-role/principal-nameにはアクセスできません。

OKですね。
アクセストークンをResource Owner Password Credentialsで取得する
Keycloak ProviderでDirect Access Grantsを有効にしているので、こちらの動作確認もしておきましょう。
Resource Owner Password Credentialsはそもそも推奨されていないので、動作確認の用途などで使うようにしましょう。
アクセス方法はこちらに書かれています。
こんな感じでしょうか。
$ CLIENT_SECRET=.....
$ curl 172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/token \
-d client_id=sample-client \
-d client_secret=${CLIENT_SECRET} \
-d username=my-user \
-d password=password \
-d scope=openid \
-d grant_type=password | jq
結果の例。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItaTZYVnhra1dKUE1FNjB6MUx2WVpWYk5vdm1FN0g3U0FsSlZoSnNyM1RZIn0.eyJleHAiOjE3MjcwOTE4NzIsImlhdCI6MTcyNzA5MTU3MiwianRpIjoiNjBjODk4NjQtODhhMy00N2I3LTliZjktNDgxMWUzOTI3OGIyIiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiOGNmM2QxYWEtNDgwMS00MWM3LWJkZTUtNjViMjY1YTZkNDgxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2FtcGxlLWNsaWVudCIsInNpZCI6IjVjZmQ4MTNkLWQwNWItNDljNi1iYTNmLTM5MTEwYjI4ODhhNyIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsibXktcm9sZSJdfSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6Im15IHVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJteS11c2VyIiwiZ2l2ZW5fbmFtZSI6Im15IiwiZmFtaWx5X25hbWUiOiJ1c2VyIiwiZW1haWwiOiJteS11c2VyQGV4YW1wbGUuY29tIn0.yf1bdrwrPdsSU28sw4mW9JnJsFkRnl9GevJbwKp5bZkUYYUhJQLQrpmYACWjtvMFgaql_mLls3qPsq7A-KnsLA8nulLdoaG7hFiCpB3Ju6ZyoOlktcWhD-pnk69POXnAXgUfQ0noq4cXWoKNv021cl3o-2hITlOszG7nZ_xp92wVLpuZG0CiuXeHQlKKwpO8T76gL_Kd-I_uHwdLAm8JSuzAZbYT_ZkR6nYAOtjI0eneBr1brn3lvjTLXaeZOLnoNPjidHiy37k5vTmoeQJudDJNHWRM20iJtwfCyOOrAeueTguXgqZnm5VL1jx_YirZU5swE5ZNETnrIiEm00Z46g", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwNjBlYWVhYy0zZTBkLTQ2YTMtOTA3ZS1iNzJkOWZiYjNjOGIifQ.eyJleHAiOjE3MjcwOTMzNzIsImlhdCI6MTcyNzA5MTU3MiwianRpIjoiOWU3MTExMjQtNGZkZi00Yzg2LWFiYjgtMDgzMmQzYmVmNzkzIiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiOGNmM2QxYWEtNDgwMS00MWM3LWJkZTUtNjViMjY1YTZkNDgxIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6InNhbXBsZS1jbGllbnQiLCJzaWQiOiI1Y2ZkODEzZC1kMDViLTQ5YzYtYmEzZi0zOTExMGIyODg4YTciLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHdlYi1vcmlnaW5zIGJhc2ljIGFjciByb2xlcyBlbWFpbCJ9.SHY961o8fGZs53XchKFn653PHzlLUehE2DFc1l9_1IMWzUwzYlKrMXCK6P7U9uQx6OF-8_T3Q6ra2gW3_54TYg", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItaTZYVnhra1dKUE1FNjB6MUx2WVpWYk5vdm1FN0g3U0FsSlZoSnNyM1RZIn0.eyJleHAiOjE3MjcwOTE4NzIsImlhdCI6MTcyNzA5MTU3MiwianRpIjoiNmJhM2JjYTEtZDQyYS00MDU5LTllNDItOTE5ZGJiZTE2OWM0IiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoic2FtcGxlLWNsaWVudCIsInN1YiI6IjhjZjNkMWFhLTQ4MDEtNDFjNy1iZGU1LTY1YjI2NWE2ZDQ4MSIsInR5cCI6IklEIiwiYXpwIjoic2FtcGxlLWNsaWVudCIsInNpZCI6IjVjZmQ4MTNkLWQwNWItNDljNi1iYTNmLTM5MTEwYjI4ODhhNyIsImF0X2hhc2giOiI5NVRJOE45cWF2U1dMVlpGNzlwaG9BIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6Im15IHVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJteS11c2VyIiwiZ2l2ZW5fbmFtZSI6Im15IiwiZmFtaWx5X25hbWUiOiJ1c2VyIiwiZW1haWwiOiJteS11c2VyQGV4YW1wbGUuY29tIn0.MS_HW0BRnoRd1MdnJpcwnKnSMd9NxXglHeGiVPETqfBBSPqsqwfTi-A3ldLbHXZ_P2S-7OvpJyXJvPX43R2o5twb08Ucw3e-2-XjGmXlgD8SLqynpRYpmrtKa4uxwV_zSWC65D8m_rdHn6D_GULXIxD2Y-J0IL_Q0F3n41yfC0mhkJuBw6-r8Q8rYjVVxoL4DjsR-Tvrv1yqvpv4JOT2_IlmJu8RjCERQgXEi9l8hrIj5iGAea0hpcQuxESJRuIiD9DUUZXOirm7XK3IQhGuuSUztPNaCmKgtN71cnoy0EAdG9TTiUKYLz2B-LtWyhWe-XQt3srY4p8ZKBTY3U6ehA", "not-before-policy": 0, "session_state": "5cfd813d-d05b-49c6-ba3f-39110b2888a7", "scope": "openid profile email" }
OKですね。
Keycloak ProviderがUser Profileをうまく扱えないという話
Keycloak Providerは、Keycloak 23から追加されたUser Profileをうまく扱えません。
Keycloak version >= 24 support? · Issue #944 · mrparkers/terraform-provider-keycloak · GitHub
User Profileに対応するkeycloak_realm_user_profileというリソースはあるのですが、実際にこれを使おうとするとusernameまたはemailは
削除できないと言われます。
こんな感じですね。
│ Error: error sending PUT request to /admin/realms/sample-realm/users/profile: 400 Bad Request. Response body: {"errorMessage":"[The attribute 'username' can not be removed, The attribute 'email' can not be removed]"}
厄介なことに1度これを起こしてしまうと抜けられなくなります…。
新規リソースの追加ではなく既存のUser Profileを更新できないといけないようなのですが、それが現状ではできません。
おわりに
TerraformでKeycloakのリソース定義を行える、Keycloak Providerを試してみました。
管理CLIでリソース作成をしていってもいいのですが、コードで定義できるとやっぱり便利です。
開発が停滞しているところは気になりますが、基本的なリソース作成はできるのでふだんはこちらを使おうかなと思います。