Sometimes it is desired to share one Camunda installation between multiple independent parties, also referred to as tenants. While sharing an installation means sharing computational resources, the tenants' data should be separated from each other. This tutorial shows how to work with the one process engine per tenant approach.
In detail it explains how to:
- configure one process engine per tenant on a WildFly Application Server such that data is isolated by database schemas
- develop a process application with tenant-specific deployments
- access the correct process engine from a REST resource based on a tenant identifier
See the user guide for a general introduction on multi-tenancy and the different options Camunda offers.
Before starting, make sure to install the Camunda Platform on Wildfly ≤26 server, follow the step in the manual installation guide. We will call the root folder $WILDFLY_HOME in the following explanations. Note that this example is only compatible with WildFly ≤26. The latest compatible pre-packaged Camunda WildFly distribution is 7.18.x.
Before configuring process engines, we have to set up a database schema for every tenant. In this section we will explain how to do so.
Start up WildFly. After the database has been created, start a DB client (e.g. DBeaver) and connect to the database. This is the default connection information:
- Driver Class: org.h2.Driver
- JDBC URL: jdbc:h2:./camunda-h2-dbs/process-engine
- User Name: sa
- Password: sa
Create two different schemas for the different process engines:
create schema TENANT1;
create schema TENANT2;
Next, inside each schema, create the database tables. To achieve this, get the SQL create scripts
from the WildFly distribution from the sql/create/
folder inside your distribution.
Execute the create scripts (h2_engine_7.22.0.sql
and h2_identity_7.22.0.sql
) after selecting the appropriate schema for the current connection:
set schema TENANT1;
<<paste sql/create/h2_engine_7.22.0.sql here>>
<<paste sql/create/h2_identity_7.22.0.sql here>>
set schema TENANT2;
<<paste sql/create/h2_engine_7.22.0.sql here>>
<<paste sql/create/h2_identity_7.22.0.sql here>>
Execute the SQL script.
After creating the tables in the two schemas, check that the tables have been created for both schemas.
In this step, we configure a process engine for each tenant. We ensure that these engines access the database schemas we have previously created. This way, process data of a tenant cannot interfere with that of another.
Open the file $WILDFLY_HOME/standalone/configuration/standalone.xml
. In that file, navigate to the configuration of the Camunda jboss subsystem, declared in an XML element <subsystem xmlns="urn:org.camunda.bpm.jboss:1.1">
. In this file, add two entries to the <process-engines>
section (do not remove default engine configuration):
The configuration of the process engine for tenant 1:
<process-engine name="tenant1">
<datasource>java:jboss/datasources/ProcessEngine</datasource>
<history-level>none</history-level>
<properties>
<property name="databaseTablePrefix">TENANT1.</property>
<property name="jobExecutorAcquisitionName">default</property>
<property name="isAutoSchemaUpdate">false</property>
<property name="authorizationEnabled">true</property>
<property name="jobExecutorDeploymentAware">true</property>
</properties>
<plugins>
<!-- plugin enabling Process Application event listener support -->
<plugin>
<class>org.camunda.bpm.application.impl.event.ProcessApplicationEventListenerPlugin</class>
</plugin>
</plugins>
</process-engine>
The configuration of the process engine for tenant 2:
<process-engine name="tenant2">
<datasource>java:jboss/datasources/ProcessEngine</datasource>
<history-level>none</history-level>
<properties>
<property name="databaseTablePrefix">TENANT2.</property>
<property name="jobExecutorAcquisitionName">default</property>
<property name="isAutoSchemaUpdate">false</property>
<property name="authorizationEnabled">true</property>
<property name="jobExecutorDeploymentAware">true</property>
</properties>
<plugins>
<!-- plugin enabling Process Application event listener support -->
<plugin>
<class>org.camunda.bpm.application.impl.event.ProcessApplicationEventListenerPlugin</class>
</plugin>
</plugins>
</process-engine>
(find the complete standalone.xml
here)
By having a look at the datasource
configuration, you will notice that the data source is shared between all engines. The property databaseTablePrefix
points the engines to different database schemas. This makes it possible to shares resources like a database connection pool between both engines. Also have a look at the entry jobExecutorAcquisitionName
. The job acquisition is part of the job executor, a component responsible for executing asynchronous tasks in the process engine (cf. the job-executor
element in the subsystem configuration). Again, the jobExecutorAcquisitionName
configuration enables reuse of one acquisition thread for all engines.
The approach of configuring multiple engines also allows you to differ engine configurations apart from the database-related parameters. For example, you can activate process engine plugins for some tenants while excluding them for others.
In this step, we describe a process application that deploys different processes for the two tenants. It also exposes a REST resource that returns a tenant's process definitions. To identify the tenant, we provide a user name in the REST request. In the implementation, we use CDI to transparently interact with the correct process engine based on the tenant identifier.
The following descriptions highlight the concepts related to implementing multi-tenancy in a step-by-step explanation to develop along.
In the project, we have set up a plain Camunda EJB process application.
In pom.xml, the camunda-engine-cdi
and camunda-ejb-client
dependencies are added:
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-engine-cdi</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.javaee</groupId>
<artifactId>camunda-ejb-client</artifactId>
</dependency>
These are required to inject process engines via CDI.
In the folder src/main/resources
, we have added a folder processes
and two subfolders tenant1
and tenant2
. These folders contain a process for tenant 1 and a process for tenant 2, respectively.
In order to deploy the two definitions to the two different engines, we have added a file src/main/resources/META-INF/processes.xml
with the following content:
<process-application
xmlns="http://www.camunda.org/schema/1.0/ProcessApplication"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process-archive name="tenant1-archive">
<process-engine>tenant1</process-engine>
<properties>
<property name="resourceRootPath">classpath:processes/tenant1/</property>
<property name="isDeleteUponUndeploy">false</property>
<property name="isScanForProcessDefinitions">true</property>
</properties>
</process-archive>
<process-archive name="tenant2-archive">
<process-engine>tenant2</process-engine>
<properties>
<property name="resourceRootPath">classpath:processes/tenant2/</property>
<property name="isDeleteUponUndeploy">false</property>
<property name="isScanForProcessDefinitions">true</property>
</properties>
</process-archive>
</process-application>
This file declares two process archives. By the process-engine
element, we can specify the engine to which an archive should be deployed. By the resourceRootPath
, we can assign different portions of the contained process definitions to different process archives.
To showcase the programming model for multi-tenancy with CDI, we have added a simple REST resource that returns all deployed process definitions for a process engine. The resource has the following source code:
@Path("/process-definition")
public class ProcessDefinitionResource {
@Inject
protected ProcessEngine processEngine;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ProcessDefinitionDto> getProcessDefinitions() {
List<ProcessDefinition> processDefinitions =
processEngine.getRepositoryService().createProcessDefinitionQuery().list();
return ProcessDefinitionDto.fromProcessDefinitions(processDefinitions);
}
}
Note that the distinction between tenants is not made in this resource.
We want the injected process engine to always be the one that matches the current tenant making a REST request. For this matter, we have added a request-scoped tenant bean:
@RequestScoped
public class Tenant {
protected String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
To populate this bean with the tenant ID for the current user, we add a RestEasy interceptor. This interceptor is called before a REST request is dispatched to the ProcessDefinitionResource
. It has the following content:
@Provider
public class TenantInterceptor implements ContainerRequestFilter {
protected static final Map<String, String> USER_TENANT_MAPPING = new HashMap<>();
static {
USER_TENANT_MAPPING.put("kermit", "tenant1");
USER_TENANT_MAPPING.put("gonzo", "tenant2");
}
@Inject
protected Tenant tenant;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
List<String> user = requestContext.getUriInfo().getQueryParameters().get("user");
if (user.size() != 1) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
String tenantForUser = USER_TENANT_MAPPING.get(user.get(0));
tenant.setId(tenantForUser);
}
}
Note that the tenant ID is determined based on a simple static map. Of course, in real-world applications one would implement a more sophisticated lookup procedure here.
To resolve the process engine based on the tenant, we have specialized the process engine producer bean as follows:
@Specializes
public class TenantAwareProcessEngineServicesProducer extends ProcessEngineServicesProducer {
@Inject
private Tenant tenant;
@Override
@Named
@Produces
@RequestScoped
public ProcessEngine processEngine() {
CommandContext commandContext = Context.getCommandContext();
if (commandContext == null) {
return getProcessEngineByTenantId(tenant.getId());
} else {
// used within the process engine (e.g. by the job executor)
return commandContext.getProcessEngineConfiguration().getProcessEngine();
}
}
protected ProcessEngine getProcessEngineByTenantId(String tenantId) {
if (tenantId != null) {
ProcessEngine processEngine = BpmPlatform.getProcessEngineService().getProcessEngine(tenantId);
if (processEngine != null) {
return processEngine;
} else {
throw new ProcessEngineException("No process engine found for tenant id '" + tenantId + "'.");
}
} else {
throw new ProcessEngineException("No tenant id specified. A process engine can only be retrieved based on a tenant.");
}
}
@Override
@Produces
@Named
@RequestScoped
public RuntimeService runtimeService() {
return processEngine().getRuntimeService();
}
...
}
The producer determines the engine based on the current tenant. It encapsulates the logic of resolving the process engine for the current tenant entirely. Every bean can simply declare @Inject ProcessEngine
without specifying which specific engine is addressed to work with the current tenant's engine.
Start up WildFly. Build the process application and deploy the resulting war file to WildFly.
Make a GET request (e.g., by entering the URL in your browser) against the following URL to get all process definitions deployed to tenant 1's engine: http://localhost:8080/multi-tenancy-tutorial/process-definition?user=kermit
Only the process for tenant 1 is returned.
Make a GET request against the following URL to get all process definitions deployed to tenant 2's engine: http://localhost:8080/multi-tenancy-tutorial/process-definition?user=gonzo
Only the process for tenant 2 is returned.
Go to Camunda Cockpit and switch the engine to tenant1
on the following URL (you will be asked to create an admin user first):
http://localhost:8080/camunda/app/cockpit/tenant1/
Only the process for tenant 1 shows up. You can check the same for tenant 2 by switching to engine tenant2
.
And you're done! :)
The test class ProcessIntegrationTest uses Arquillian to verify the behavior.
Follow the steps to run the test:
- Install the Camunda Platform on a Wildfly ≤26 server, follow the step in the manual installation guide (note that this example is not compatible with WildFly versions after WildFly 26).
- Replace the
camunda-bpm-wildfly-{version}/server/wildfly-{version}/standalone/configuration/standalone.xml
with- standalone.xml (two schemas - requires manual schema creation) or
- standalone_test.xml (two databases - auto schema creation)
- Start the server.
- Go to the project directory and run the test with the Maven command
mvn test