Portability of applications across Kubernetes distributions, part 2

John Alcorn
AI+ Enterprise Engineering
8 min readFeb 14, 2022

Java frameworks that make portability a reality

My team, the Cloud Journey Optimization Team, is primarily focused on helping customers migrate to any of the major cloud vendors, such as AWS, Azure, GCP, or IBM Cloud. As discussed in previous articles, we wrote and maintain our cloud-native application, the IBM Stock Trader, to show how to accomplish this via a set of mostly Java-based microservices. In this article, we’ll focus on how using industry-standard frameworks, like JDBC, JMS, and JCache help ensure you’ll be able to wire up your application to use the managed services offered by each of the major hyperscalers.

To prove our point that such cloud-native applications should run without significant changes, we picked Amazon Web Services (AWS) as the first new landing zone (keeping in mind we’ve successfully run on IBM Cloud for years). We have efforts underway to also get things working in Azure and GCP, but we’ll focus on the work done on AWS in this article, as it’s the furthest along, and it will be easiest to understand if we aren’t switching context between different clouds. I’d especially like to thank my colleague, Ryan Claussen, for doing most of the heavy lifting of getting the IBM Stock Trader application working in AWS. The net is, we now have it to where all of the main functionality of the IBM Stock Trader application runs within the AWS network, leveraging a significant number of AWS managed services:

Stock Trader using AWS Services

Let’s start with looking at the first green box, in the top left of the diagram above — the Relational Database Service (RDS). As you may recall, back when we ran exclusively in IBM Cloud, we used IBM DB2 as our relational database. Sometimes we ran with a “local” DB2 installed into our Kubernetes cluster (via the IBM Cloud Pak for Data), and sometimes we used the hosted/managed DB2 on Cloud offering. Now, in AWS, we are using an Aurora database in RDS, which has PostgreSQL compatibility.

The Aurora database in AWS RDS that we used

It is our Portfolio microservice that needs such a relational database, and since that microservice is coded to the JPA and JDBC standards, it can work with any relational database without code changes, as long as it is configured appropriately. The work we needed to do to get things working with a PostgreSQL database was all about implementing such configuration changes.

First, we had to make the microservice support a range of JDBC providers. Previously, it only included the JDBC jar file for IBM DB2, but now, we have modified the build process to download the JDBC jar file for each major relational database (DB2, Oracle, Microsoft SQL Server, PostgreSQL, MySQL and Derby). Here’s a snippet of the Maven pom.xml that does this for PostgreSQL:

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.25</version>
<scope>provided</scope>
</dependency>

We now have similar stanzas for each of the other supported relational database types. We also use the standard Maven approach to copy the jar files for each JDBC provider into a place that we can access when running our Docker build:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>target/prereqs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>

This basically says that the jar file for each Maven dependency should get copied to the target/prereqs directory during the build. Then we need to copy those JDBC jar files into the Docker container we build for the Portfolio microservice. Rather than make a separate layer in the container for each JDBC jar file, we just copy the whole directory into the container via a single line in our Dockerfile:

COPY target/prereqs /config/prereqs

Then our Open Liberty server.xml needs to refer to the appropriate JDBC jar file. We had to get a little fancy here. As discussed in the previous article, we have a fairly sophisticated Kubernetes operator for this application, and it has a field for you to specify which database type you want; it wires that up as an environment variable named JDBC_KIND to the Portfolio microservice. So we use the ability that Open Liberty has to include additional configuration files into the server.xml, and use the value of that env var in the name of the file to be included:

<variable name=”JDBC_KIND” defaultValue=”db2"/>
<include location=”${server.config.dir}/includes/${JDBC_KIND}.xml”/>

Note that, for backward compatibility reasons, it defaults to db2 as the kind, since we didn’t want to break anyone that didn’t specify the database.kind field in the Stock Trader CR yaml. In the past, db2 was the only option, so that’s what we use if not specified. But if it is specified, like if postgres was chosen from the enumeration of legal values, then this will cause a postgres.xml to get included. Here are the contents of that included file:

<server>
<variable name=”JDBC_SSL” defaultValue=”false”/>
<dataSource id=”PortfolioDB” jndiName=”jdbc/Portfolio/PortfolioDB” connectionManagerRef=”dbConnections” isolationLevel=”TRANSACTION_REPEATABLE_READ”>
<jdbcDriver>
<library name=”Postgres” description=”PostgreSQL JDBC driver”>
<file id=”postgres” name=”/config/prereqs/postgresql-42.2.25.jar”/>
</library>
</jdbcDriver>
<properties.postgresql serverName=”${env.JDBC_HOST}” portNumber=”${env.JDBC_PORT}” databaseName=”${env.JDBC_DB}”
user=”${env.JDBC_ID}” password=”${env.JDBC_PASSWORD}”/>
sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory”
ssl=”${JDBC_SSL}” sslMode=”verify-ca”/>
</dataSource>
</server>

As you can see, it points to the PostgreSQL JDBC jar file that our Dockerfile copied into the /config/prereqs directory of the Docker container. It also configures the properties for the database, like the server name, port number, credentials, etc. The other include files, like db2.xml andoracle.xml, do similar things, though the field names for the dataSource configuration properties vary between providers. Note also this is where we specify the isolation level — we were pretty strict, choosing repeatable read here, to ensure consistency of the data.

In the interest of full disclosure, we did encounter an issue in the script we use to create the tables in the database. We had accidentally used a DB2-proprietary data type, called DOUBLE, rather than the JDBC standard type of DOUBLE PRECISION. Once we fixed this, our DDL script was portable, and worked just fine in a PostgreSQL-compatible database, such as Aurora from the AWS RDS (and still worked fine in DB2 as well):

CREATE TABLE Portfolio(owner VARCHAR(32) NOT NULL, total DOUBLE PRECISION, accountID VARCHAR(64), PRIMARY KEY(owner));CREATE TABLE Stock(owner VARCHAR(32) NOT NULL, symbol VARCHAR(8) NOT NULL, shares INTEGER, price DOUBLE PRECISION, total DOUBLE PRECISION, dateQuoted VARCHAR(10), commission DOUBLE PRECISION, FOREIGN KEY (owner) REFERENCES Portfolio(owner) ON DELETE CASCADE, PRIMARY KEY(owner, symbol));

Note that for anyone using a managed relational database service that is PostgreSQL compatible, we found the pgAdmin tool to be very helpful, like to run the script above:

Using pgAdmin to configure the AWS Relational Database Service

And with that, our cloud-native application was happily using AWS RDS for all of its relational database needs! We can even see some monitoring statistics on it in AWS CloudWatch:

CloudWatch monitoring for our AWS RDS database

In addition to this work to switch JDBC providers to AWS RDS/PostgreSQL, we also switched JMS providers, to use Apache Active MQ, which the managed Amazon MQ service is based upon.

Note that the Amazon MQ service is specifically based upon the “classic” flavor Active MQ, which among other things only supports JMS 1.1 (fortunately, Stock Trader doesn’t use any of the “new” features introduced in JMS 2.0); Apache is also incubating an “Artemis” flavor of Active MQ that supports modern frameworks like JMS 2.0 and even Jakarta Messaging 3.0, which perhaps Amazon will adopt in its managed MQ service in the future. Anyway, the current managed Amazon MQ service was plenty sufficient, for our relatively simplistic point-to-point messaging needs.

The managed Amazon MQ service, for point-to-point messaging

The good news is that the same pattern we used with JDBC worked in switching JMS providers over to Amazon MQ; we added the dependency stanza in the Maven pom.xml for the Account microservice for Apache Active MQ, updated the Dockerfile accordingly, and made the server.xml do an include of an XML file based on the MQ_KIND env var that the operator passes to Account. All is working fine with the managed MQ service in AWS:

Using the managed MQ service in AWS

That pretty much wraps up how we moved JDBC and JMS providers in our cloud native Stock Trader application to use managed services in AWS. Note that it was all configuration stuff, not changes to the actual Java source code, and that these changes were all additive; we still support IBM DB2 and IBM MQ for people that prefer those options in the IBM Cloud — you get to make that choice in the CR yaml you pass to my operator.

One last thought in this space: the stock quote caching that we do in Stock Trader is coded directly to the Redis Java APIs today. For AWS, that turned out to be fine, as they have the managed ElastiCache service that is Redis API compliant, and we are successfully using that now in AWS, with zero changes to the StockQuote microservice.

Using the managed ElastiCache service in AWS for Redis-based caching

But one improvement I’d like to make in the future is to move away from the proprietary Redis APIs and move to JCache (JSR 107 — the javax.cache APIs) instead, so that we could plug in any JCache provider, such as Hazelcast or Infinispan (productized as the Red Hat Data Grid), as well as using it to continue talking to Redis if desired.

The summary, for those of you that are starting new cloud native projects and want to be proactive in the area of portability, is as follow:

  1. Industry standard APIs and frameworks related to the services that your microservices will use should be leveraged and can provide you very good source code portability across cloud providers.
  2. Additional thought and work will be necessary in setup and configuration when you want to leverage different managed service providers of the various capabilities.

So far, however, our work has shown that portability across cloud vendors is pretty attainable with some planning and discipline.

Stay tuned for further articles in this series that will explore other work we did to use additional managed services in hyperscalers such as AWS. Thanks for your time, and as always, feedback is welcome!

Thanks to Eric Herness for his review of this article.

--

--

John Alcorn
AI+ Enterprise Engineering

Member of the Cloud Journey Optimization Team at Kyndryl. Usually busy writing/testing code, or teaching others what I’ve learned.