How to build and deploy a Jakarta EE application on Kubernetes

In this series of tutorials, we will show how to create and deploy a Jakarta EE service in a Cloud environment. Within this first article, we will learn how to deploy a WildFly application on Kubernetes using Minikube and JKube Maven plugin. In the next article, we will target OpenShift as Cloud environment.

Let’s get started. The first step is obviously installing Kubernetes so that we can deploy applications on top of it.

Install a Kubernetes Cluster with Minikube

Kubernetes is an open source system for managing containerized applications across multiple hosts. It provides basic mechanisms for deployment, maintenance, and scaling of applications. The simplest way to get started with Kubernetes is to install Minikube.

Minikube is a lightweight Kubernetes implementation that creates a VM on your local machine and deploys a simple cluster containing a single node and ships with a CLI that provides basic bootstrapping operations for working with your cluster, including start, stop, status, and delete

The procedure for installing Minikube is detailed at: https://minikube.sigs.k8s.io/docs/start/

To install the binary distribution you can just download it and run the “install” command on it:

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ sudo install minikube-linux-amd64 /usr/local/bin/minikube

When done, launch the “minikube start”command which will select the Driver for your environment and download the Virtual Machine required to run Kubernetes components:

$ minikube start

🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

When done, verify that the default and kube-system services are available:

$ minikube service list
|-------------|------------|--------------|-----|
|  NAMESPACE  |    NAME    | TARGET PORT  | URL |
|-------------|------------|--------------|-----|
| default     | kubernetes | No node port |
| kube-system | kube-dns   | No node port |
|-------------|------------|--------------|-----|

Great, you Kubernetes cluster is now up and running.

Then, in order to build the Docker image using Minikube’s Docker instance, execute:

$ eval $(minikube docker-env)

The command “minikube docker-env” returns a set of Bash environment variable exports to configure your local environment to re-use the Docker daemon inside the Minikube instance.

If you fail to configure your Minikube environment as above, you will see an error when deploying your resource: “Connection reset by peer

Configuring the Jakarta EE Service

We will now set up a Jakarta EE REST service which depends on a Database service running as well on Kubernetes.

The REST service follows the standard pattern, that is a Model class named Customer:

@Entity
@Table(name = "customer")
@NamedQuery(name = "findAllCustomer", query = "SELECT c FROM Customer c")
public class Customer implements Serializable {

    @Id
    @SequenceGenerator(
            name = "customerSequence",
            sequenceName = "customerId_seq",
            allocationSize = 1,
            initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customerSequence")
    private Long id;

    @Column
    private String name;

    @Column
    private String surname;

    // Getter/Setters omitted for brevity
}

Then, we include a REST endpoint with a method for adding a new Customer (via HTTP POST) and one for returning the list of Customers (via HTTP GET)

@Path("customers")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerEndpoint {

@Inject CustomerManager manager;

    @POST
    public void createCustomer(Customer customer) {
        manager.createCustomer(customer);
    }
    @GET
    public List<Customer> getAllCustomers() {
        return manager.getAllCustomers();
    }
}

The Manager class is the layer responsible for updating the Database:

@ApplicationScoped
public class CustomerManager {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void createCustomer(Customer customer) {
        em.persist(customer);
        System.out.println("Created Customer "+customer);
    }
    public List<Customer> getAllCustomers() {
        List<Customer> tasks = new ArrayList<>();
        try {
            tasks = em.createNamedQuery("findAllCustomer").getResultList();
        } catch (Exception e){
            e.printStackTrace();
        }
        return tasks;
    }
}

As we will be using PostgreSQL, our persistence.xml file will contain the following persistence unit definition:

<persistence version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/persistence
        http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="primary">
        <jta-data-source>java:jboss/datasources/PostgreSQLDS</jta-data-source>
        <properties>
            <!-- Properties for Hibernate -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

You should have noticed that a Datasource is defined as JTA Datasource. In order to provide a Datasource to our Jakarta EE service, we will provision a WildFly Bootable Jar, which includes the following layers:

<build>
        <finalName>wildfly-jar-sample</finalName>
        <plugins>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-jar-maven-plugin</artifactId>
                <version>${version.wildfly.jar}</version>
                <configuration>
                    <feature-packs>
                        <feature-pack>
                            <location>wildfly@maven(org.jboss.universe:community-universe)#${version.wildfly}</location>
                        </feature-pack>
                        <feature-pack>
                            <groupId>org.wildfly</groupId>
                            <artifactId>wildfly-datasources-galleon-pack</artifactId>
                            <version>1.1.0.Final</version>
                        </feature-pack>
                    </feature-packs>
                    <layers>
                        <layer>cloud-profile</layer>
                        <layer>postgresql-datasource</layer>
                    </layers>
                    <excluded-layers>
                        <layer>deployment-scanner</layer>
                    </excluded-layers>
                    <cloud/>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

As you can see from the above Maven plugin configuration, our WildFly bootable Jar will be provisioned with a:

  • cloud-profile: This is an aggregation of some basic layers (bean-validation, cdi, ee-security, jaxrs, jms-activemq, jpa, observability, resource-adapters, web-server) to address cloud use cases
  • postgresql-datasource: This layer installs postgresql driver as a module inside a WildFly server. The driver is named postgresql. (For more info, this layer is available at: https://github.com/wildfly-extras/wildfly-datasources-galleon-pack

Configuring PostgreSQL Service on Kubernetes

We will use PostgreSQL as database backend. There are several strategies for deploying PostgreSQL on Kubernetes. We will show here how to use the JKube Maven plugin to address the deployment of all resources required to deploy PostgreSQL. For this purpose, we will add the following files under the src/main/jkube/raw folder of your Maven project:

│   └── raw
│       ├── postgres-configmap.yml
│       ├── postgres-deployment.yml
│       └── postgres-service.yml

The first file, postgres-configmap.yml, contains the Kubernetes ConfigMap with the credentials to access PostgreSQL:

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRESQL_USER: user
  POSTGRESQL_PASSWORD: password
  POSTGRESQL_DATABASE: wildflydb

The second one, postgres-deployment.yml, contains information about the Deployment unit, including the base image used for the PostgreSQL service:

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: centos/postgresql-96-centos7:latest
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 5432
          envFrom:
            - configMapRef:
                name: postgres-config

Finally, the third one (postgres-service.yml), defines the PostgreSQL service:

apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  type: NodePort
  ports:
   - port: 5432
  selector:
   app: postgres

That’s all.

Connecting the Jakarta EE service with PostgreSQL service:

Now we have configured both services. The last thing we need is some glue between the two services. As a matter of fact, the WildFly image when it’s built will search for some environment variables to find out the settings to connect to the PostgreSQL database. So we have to provide this information via Environment variables. That’s a simple task. We will add another YAML file (deployment.yaml) that contains this information and, also, the JVM Settings we need for our Jakarta EE service:

spec:
  template:
    spec:
      containers:
      - env:
        - name: POSTGRESQL_USER
          value: user
        - name: POSTGRESQL_PASSWORD
          value: password
        - name: POSTGRESQL_DATABASE
          value: wildflydb
        - name: POSTGRESQL_SERVICE_HOST
          value: postgres
        - name: POSTGRESQL_SERVICE_PORT
          value: 5432
        - name: JAVA_OPTIONS
          value: '-Xms128m -Xmx1024m'
        - name: GC_MAX_METASPACE_SIZE
          value: 256
        - name: GC_METASPACE_SIZE
          value: 96

Here is the full project tree:

src
└── main
    ├── java
    │   └── com
    │       └── mastertheboss
    │           ├── model
    │           │   └── Customer.java
    │           └── rest
    │               ├── CustomerEndpoint.java
    │               ├── CustomerManager.java
    │               └── JaxrsConfiguration.java
    ├── jkube
    │   ├── deployment.yaml
    │   └── raw
    │       ├── postgres-configmap.yml
    │       ├── postgres-deployment.yml
    │       └── postgres-service.yml
    ├── resources
    │   ├── import.sql
    │   └── META-INF
    │       └── persistence.xml
    └── webapp
        ├── index.jsp
        └── WEB-INF
            └── beans.xml

The project includes as well a SQL script to load some Entity objects at start up.

Deploying our services on on Kubernetes

Now that both plugins are in place, we will deploy the application on Kubernetes. To do that, we will include, as Maven profile, the kubernates-maven-plugin which is in charge to create the Kubernetes descriptors, build and deploy the service on Kubernetes:

<profile>
    <id>kubernetes</id>
    <build>
        <plugins>
            <plugin>
                <groupId>org.eclipse.jkube</groupId>
                <artifactId>kubernetes-maven-plugin</artifactId>
                <version>${jkube.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>resource</goal>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <enricher>
                        <config>
                            <jkube-service>
                                <type>NodePort</type>
                            </jkube-service>
                        </config>
                    </enricher>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

From the shell, execute the following steps:

1) Create your Kubernetes resource descriptors.

mvn clean k8s:resource -Pkubernetes

2) Then start docker build by hitting the build goal.

mvn package k8s:build -Pkubernetes

3) Finally, deploy your application on the Kubernetes cluster:

mvn k8s:apply -Pkubernetes

Once deployed, you can see the pods running inside your Kubernetes cluster:

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
jakartaee-demo-7d98897d57-jhpx5   1/1     Running   0          2m
postgresql-bfbb7ff8b-nb9bx        1/1     Running   0          2m

Let’s check the updated Service List:

minikube service list
|----------------------|---------------------------|--------------|----------------------------|
|      NAMESPACE       |           NAME            | TARGET PORT  |            URL             |
|----------------------|---------------------------|--------------|----------------------------|
| default              | jakartaee-demo            | http/8080    | http://192.168.39.35:31353 |
| default              | kubernetes                | No node port |
| default              | postgres                  |         5432 | http://192.168.39.35:30662 |
| kube-system          | kube-dns                  | No node port |
| kubernetes-dashboard | dashboard-metrics-scraper | No node port |
| kubernetes-dashboard | kubernetes-dashboard      | No node port |
|----------------------|---------------------------|--------------|----------------------------|

Now we can test the service:

jakarta ee tutorial
You can now manage your application from Kubernetes using either the ‘kubectl’ command line, or from the Kubernetes Dashboard:

minikube dashboard

jakarta ee tutorial

Troubleshooting notes

Please notice that there’s a known issue which affects some JDKs: https://bugs.openjdk.java.net/browse/JDK-8236039

When this occurs the client throws an exception:

“javax.net.ssl.SSLHandshakeException: extension (5) should not be presented in certificate_request”

This happens because JDK 11 onwards has support for TLS 1.3 which can cause the above error.

You can work around this issue by setting the property -Djdk.tls.client.protocols=TLSv1.2 to the JVM args to make it use 1.2 instead. As an alternative, update to the latest version od JDK (it’s fully solved in JDK 15).

Enjoy Jakarta EE on Kubernetes!

You can find the source code for this Maven project at: https://github.com/fmarchioni/mastertheboss/tree/master/openshift/kubernetes-jakartaee