What is Keycloak?

Keycloak is an open source Identity and Access Management solution aimed at modern applications and services. It allows developers to integrate single-sign-on (SSO) capabilities within their applications with little or no effort.


In this article we are going to guide you in deploying Keycloak on AWS EC2 instances in clustered mode and allowing the application to leverage the AWS IAM credentials to carry out user authentication and authorization.


Step 1: Initial Setup

Login to your EC2 Instance using ssh.

Choose a location where you would like to install Keycloak and navigate to the specific location. In alignment with linux conventions, we have chosen /opt to be the parent folder for the keycloak installation directory.

cd /opt

Download the latest release of Keycloak. As of this writing the latest version available for download is 12.0.4.

sudo wget https://github.com/keycloak/keycloak/releases/download/12.0.4/keycloak-12.0.4.tar.gz -P .

Extract the downloaded tar files using the command

sudo tar xzf keycloak-12.0.4.tar.gz

Step 2: Install and verify OpenJDK

Step 3: Enable MySQL Driver Support using JBoss Command Line 

By default, keycloak uses the built-in H2 database for data storage, which is not very suitable for production purposes. We are therefore going to configure keycloak to use the mysql driver in this section.

Download the MySQL JDBC driver

sudo wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-8.0.22.tar.gz -P .

Extract the contents of the tar file to the current directory.

sudo tar xzf mysql-connector-java-8.0.22.tar.gz 

Launch the keycloak server from the command line in order to be able to connect to it using the jboss command line tool:

bin/standalone.sh

From another command line terminal, run jboss-cli.sh from the keycloak installation directory to connect to the server.

bin/jboss-cli.sh

Execute the connect command

You are disconnected at the moment. Type 'connect' to connect to the server or 'help' for the list of supported commands.
[disconnected /] connect

Run the following command to create a module for the mysql connector using the JBoss command line tool

module add --name=com.mysql --resources=<path-to-mysql-connector-java-8.0.22.jar> --dependencies=javax.api,javax.transaction.api

This will create a new module configuration in the folder <keycloak-installation-directory>/modules/com/mysql/main and will also copy the mysql-connector-java-8.0.22.jar file from the path defined in the above step. In addition you should find a module.xml file with the following contents:

<?xml version='1.0' encoding='UTF-8'?> 
<module xmlns="urn:jboss:module:1.1" name="com.mysql">
    <resources>
        <resource-root path="mysql-connector-java-8.0.22.jar"/>
    </resources>
    <dependencies>
        <module name="javax.api"/>
        <module name="javax.transaction.api"/>
    </dependencies>
</module>


NOTE: There currently seems to be an issue with the mysql-connector-8.0.23.jar driver as described here, which allows keycloak to launch perfectly the first time, however fails with the following error once the server is shutdown and restarted. If you encounter the same problem, just downgrade to version 8.0.22 of the mysql-connector which has been used in this article.

FATAL [org.keycloak.services] (ServerService Thread Pool -- 67) Error during startup: java.lang.RuntimeException: Exception invoking method [listUnrunChangeSets] on object [[email protected]], using arguments [null,(),false]
	at [email protected]//org.keycloak.common.util.reflections.Reflections.invokeMethod(Reflections.java:385)
	at [email protected]//org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider.getLiquibaseUnrunChangeSets(LiquibaseJpaUpdaterProvider.java:285)
	at [email protected]//org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider.validateChangeSet(LiquibaseJpaUpdaterProvider.java:253)
	at [email protected]//org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider.validate(LiquibaseJpaUpdaterProvider.java:226)
	at [email protected]//org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory.migration(DefaultJpaConnectionProviderFactory.java:301)
	at [email protected]//org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory.lambda$lazyInit$0(DefaultJpaConnectionProviderFactory.java:182)
	at [email protected]//org.keycloak.models.utils.KeycloakModelUtils.suspendJtaTransaction(KeycloakModelUtils.java:654)
	at [email protected]//org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory.lazyInit(DefaultJpaConnectionProviderFactory.java:133)
	at [email protected]//org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory.create(DefaultJpaConnectionProviderFactory.java:81)
	at [email protected]//org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory.create(DefaultJpaConnectionProviderFactory.java:59)
	at [email protected]//org.keycloak.services.DefaultKeycloakSession.getProvider(DefaultKeycloakSession.java:274)
	at org.keycloak.keycloak-model-j[email protected]//org.keycloak.models.jpa.JpaRealmProviderFactory.create(JpaRealmProviderFactory.java:51)
	at [email protected]//org.keycloak.models.jpa.JpaRealmProviderFactory.create(JpaRealmProviderFactory.java:33)
	at [email protected]//org.keycloak.services.DefaultKeycloakSession.getProvider(DefaultKeycloakSession.java:274)
	at [email protected]//org.keycloak.services.DefaultKeycloakSession.realmLocalStorage(DefaultKeycloakSession.java:199)
	at [email protected]//org.keycloak.models.cache.infinispan.RealmCacheSession.getRealmDelegate(RealmCacheSession.java:152)
	at [email protected]//org.keycloak.models.cache.infinispan.RealmCacheSession.getMigrationModel(RealmCacheSession.java:145)
	at [email protected]//org.keycloak.migration.MigrationModelManager.migrate(MigrationModelManager.java:99)
	at [email protected]//org.keycloak.services.resources.KeycloakApplication.migrateModel(KeycloakApplication.java:234)
	at [email protected]//org.keycloak.services.resources.KeycloakApplication.migrateAndBootstrap(KeycloakApplication.java:175)
	at [email protected]//org.keycloak.services.resources.KeycloakApplication$1.run(KeycloakApplication.java:138)
	at [email protected]//org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction(KeycloakModelUtils.java:228)
	at [email protected]//org.keycloak.services.resources.KeycloakApplication.startup(KeycloakApplication.java:129)
	at [email protected]//org.keycloak.provider.wildfly.WildflyPlatform.onStartup(WildflyPlatform.java:29)
	at [email protected]//org.keycloak.services.resources.KeycloakApplication.<init>(KeycloakApplication.java:115)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
	at [email protected]//org.jboss.resteasy.core.ConstructorInjectorImpl.construct(ConstructorInjectorImpl.java:152)
	at [email protected]//org.jboss.resteasy.spi.ResteasyProviderFactory.createProviderInstance(ResteasyProviderFactory.java:2815)
	at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.createApplication(ResteasyDeployment.java:371)
	at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.startInternal(ResteasyDeployment.java:283)
	at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.start(ResteasyDeployment.java:93)
	at [email protected]//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.init(ServletContainerDispatcher.java:140)
	at [email protected]//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.init(HttpServletDispatcher.java:42)
	at [email protected]//io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:117)
	at [email protected]//org.wildfly.extension.undertow.security.RunAsLifecycleInterceptor.init(RunAsLifecycleInterceptor.java:78)
	at [email protected]//io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:103)
	at [email protected]//io.undertow.servlet.core.ManagedServlet$DefaultInstanceStrategy.start(ManagedServlet.java:305)
	at [email protected]//io.undertow.servlet.core.ManagedServlet.createServlet(ManagedServlet.java:145)
	at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:588)
	at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:559)
	at [email protected]//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:42)
	at [email protected]//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
	at [email protected]//org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
	at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl.start(DeploymentManagerImpl.java:601)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentService.startContext(UndertowDeploymentService.java:97)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentService$1.run(UndertowDeploymentService.java:78)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at [email protected]//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
	at java.base/java.lang.Thread.run(Thread.java:834)
	at [email protected]//org.jboss.threads.JBossThread.run(JBossThread.java:513)
Caused by: java.lang.ClassCastException: class java.time.LocalDateTime cannot be cast to class java.lang.String (java.time.LocalDateTime and java.lang.String are in module java.base of loader 'bootstrap')
	at org.liquibase//liquibase.changelog.StandardChangeLogHistoryService.getRanChangeSets(StandardChangeLogHistoryService.java:287)
	at org.liquibase//liquibase.database.AbstractJdbcDatabase.getRanChangeSetList(AbstractJdbcDatabase.java:1124)
	at org.liquibase//liquibase.changelog.DatabaseChangeLog.validate(DatabaseChangeLog.java:257)
	at org.liquibase//liquibase.Liquibase.listUnrunChangeSets(Liquibase.java:1189)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at [email protected]//org.keycloak.common.util.reflections.Reflections.invokeMethod(Reflections.java:379)
	... 60 more 

Step 4: Create a MySQL Database Instance from your RDS Dashboard


Create Database from within RDS


Select MySQL as the Engine Type and make sure you have the right version of mysql selected from the dropdown box. Fothe purpose of this article we used version 8.0.23 of the MySQL database available within Amazon RDS.

Create Standard MySQL Database Engine within AWS RDS

Tip: Make sure you scroll down to the "Additional configuration" section and create an initial database with a name of your choice right away from within RDS database creation panel, in order to avoid confusion when connecting to the database while launching keycloak.

Set Initial Database Name while creating Database Instance

Step 5: Configure Keycloak to use the mysql based datasource configuration


 <datasources>
                <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
                    <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
                    <driver>h2</driver>
                    <security>
                        <user-name>sa</user-name>
                        <password>sa</password>
                    </security>
                </datasource>
                <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
                    <connection-url>jdbc:mysql://RDSDatabaseInstanceEndpoint:3306/dbname?useSSL=false&amp;characterEncoding=UTF-8&amp;allowPublicKeyRetrieval=true</connection-url>
                    <driver>mysql</driver>
                    <security>
                        <user-name>rds-username</user-name>
                        <password>rds-password</password>
                    </security>
                </datasource>
                <drivers>
                    <driver name="h2" module="com.h2database.h2">
                        <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
                    </driver>
                    <driver name="mysql" module="com.mysql">
                       <driver-class>com.mysql.cj.jdbc.Driver</driver-class>
                       <xa-datasource-class>com.mysql.cj.jdbc.MysqlXADataSource</xa-datasource-class>
                    </driver>

                </drivers>
            </datasources>

Step 6: Launch Keycloak

Since JBoss EAP / Wildfly uses localhost as the default bind address, we have to set the command line parameter using the -b flag and instruct the server to listen on all available interfaces. .

Keycloak and the underlying JBoss / Wildfly server are configured to bind to localhost / 127.0.0.1 by default. In order to make the service available publicly we he to configure the server to bind to an external interface.

Odoo text and image block

Using the AWS Instance metadata Service available within your EC2 instance, you can run the following command to retrieve the IPv4 address currently assigned to your instance. 

curl http://169.254.169.254/latest/meta-data/public-ipv4

In order to run the server you can combine the above command with the command used to launch the keycloak server instance from within the Keycloak installation directory.

bin/standalone.sh -c standalone-ha.xml -b `curl http://169.254.169.254/latest/meta-data/public-ipv4`

However, running the above command on keycloak (version 12.0.4) throws the following error:

ERROR [org.jboss.msc.service.fail] (MSC service thread 1-1) MSC000001: Failed to start service org.wildfly.network.interface.public: org.jboss.msc.service.StartException in service org.wildfly.network.interface.public: WFLYSRV0082: failed to resolve interface public
	at [email protected]//org.jboss.as.server.services.net.NetworkInterfaceService.start(NetworkInterfaceService.java:98)
	at [email protected]//org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1739)
	at [email protected]//org.jboss.msc.service.ServiceControllerImpl$StartTask.execute(ServiceControllerImpl.java:1701)
	at [email protected]//org.jboss.msc.service.ServiceControllerImpl$ControllerTask.run(ServiceControllerImpl.java:1559)
	at [email protected]//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1363)
	at java.base/java.lang.Thread.run(Thread.java:834)

The reason for this error as described here seems to be that jboss tries to create server sockets on the bind interface that has been provided as a a parameter, and since there is no physical interface assigned to the public IP address, the server fails to launch successfully.

In order to circumvent this problem, we can launch the keycloak server with the private IP address and let the VPC assigned to your EC2 instance handle the routing to incoming requests.

bin/standalone.sh -c standalone-ha.xml -b `curl http://169.254.169.254/latest/meta-data/local-ipv4`


Step 7: Configuring the Inbound Rules 

Although the server should be up and running using the above steps, you will need to configure the security group associated with your EC2 instance to accepting incoming requests on port 8080.



Once you have completed the above step, you should be able to access your running instance by going to the following URL:

http://<ec2EndpointURL>:8080