Injecting Kubernetes Services in CDI managed beans using Fabric8
Prologue
The thing I love the most in Kubernetes is the way services are discovered. Why?
Mostly because the user code doesn’t have to deal with registering, looking up services and also because there are no networking surprises (if you’ve ever tried a registry based approach, you’ll know what I am talking about).
This post is going to cover how you can use Fabric8 in order to inject Kubernetes services in Java using CDI.
Kubernetes Services
Covering in-depth Kubernetes Services is beyond the scope of this post, but I’ll try to give a very brief overview of them.
In Kubernetes, applications are packaged as Docker containers. Usually, it’s a nice idea to split the application into individual pieces, so you will have multiple Docker containers that most probably need communicate with each other. Some containers may be collocated together by placing them in the same Pod, while others may be remote and need a way to talk to each other. This is where Services get in the picture.
A container may bind to one or more ports providing one or more “services” to other containers. For example:
- A database server.
- A message broker.
- A rest service.
The question is “How other containers know how to access those services?“
So, Kubernetes allows you to “label” each Pod and use those labels to “select” Pods that provide a logical service. Those labels are simple key, value pairs.
Here’s an example of how we can “label” a pod by specifying a label with key name and value mysql.
{ "apiVersion" : "v1beta3", "kind" : "ReplicationController", "metadata" : { "labels" : { "name" : "mysql" }, "name" : "mysql" }, "spec" : { "replicas" : 1, "selector" : { "name" : "mysql" }, "template" : { "metadata" : { "labels" : { "name" : "mysql" } }, "spec" : { "containers" : [ { "image" : "mysql", "imagePullPolicy" : "IfNotPresent", "name" : "mysql", "ports" : [ { "containerPort" : 3306, "name" : "mysql" } ] }] } } } }
And here’s an example of how we can define a Service that exposes the mysql port. The service selector is using the key/value pair we specified above in order to define which are the pod(s) that provide the service.
{ "kind": "Service", "apiVersion": "v1beta3", "metadata": { "name": "mysql" }, "spec": { "ports": [ { "name": "mysql", "protocol": "TCP", "port": 3306, "targetPort": 3306 } ], "selector": { "name": "mysql" } } }
The Service information passed to each container as environment variables by Kubernetes. For each container that gets created Kubernetes will make sure that the appropriate environment variables will be passed for ALL services visible to the container.
For the mysql service of the example above, the environment variables will be:
- MYSQL_SERVICE_HOST
- MYSQL_SERVICE_PORT
Fabric8 provides a CDI extension which can be used in order to simplify development of Kubernetes apps, by providing injection of Kubernetes resources.
Getting started with the Fabric8 CDI extension
To use the cdi extension the first step is to add the dependency to the project.
<dependency> <groupId>io.fabric8</groupId> <artifactId>fabric8-cdi</artifactId> <version>2.1.11</version> </dependency>
Next step is to decide which service you want to inject to what field and then add a @ServiceName annotation to it.
import javax.inject.Inject; import io.fabric8.annotations.ServiceName; public class MysqlExample { private static final DB = "mydb"; private static final TCP_PROTO = "tcp"; private static final JDBC_PROTO = "jdbc:mysql"; private final Connection connection; public MysqlExample(@Inject @ServiceName("mysql") String serivceUrl) { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection(toJdbcUrl(serivceUrl)); } private static String toJdbcUrl(String url) { return url.replaceFirst(TCP_PROTO, JDBC_PROTO) + "/" + DB; } //More stuff }
In the example above we have a class that needs a JDBC connection to a mysql database that is available via Kubernetes Services.
The injected serivceUrl will have the form: [tcp|udp]://[host]:[port]. Which is a perfectly fine url, but its not a proper jdbc url. So we need a utility to convert that. This is the purpose of the toJdbcUrl.
Even though its possible to specify the protocol when defining the service, one is only able to specify core transportation protocols such as TCP or UDP and not something like http, jdbc etc.
The @Protocol annotation
Having to find and replace the “tcp” or “udp” values with the application protocol, is smelly and it gets old really fast. To remove that boilerplate Fabric8 provides the @Protocol annotation. This annotation allows you to select that application protocol that you want in your injected service url. In the previous example that is “jdbc:mysql”. So the code could look like:
import javax.inject.Inject; import io.fabric8.annotations.Protocol; import io.fabric8.annotations.ServiceName; public class MysqlExampleWithProtocol { private static final DB = "mydb"; private final Connection connection; public MysqlExampleWithProtocol(@Inject @Protocol("jdbc:mysql") @ServiceName("mysql") String serivceUrl) { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection(serivceUrl + "/" + DB); } //More stuff }
Undoubtably, this is much cleaner. Still it doesn’t include information about the actual database or any parameters that are usually passed as part of the JDBC Url, so there is room for improvement here.
One would expect that in the same spirit a @Path or a @Parameter annotations would be available, but both of these are things that belong to configuration data and are not a good fit for hardcoding into code. Moreover, the CDI extension of Fabric8 doesn’t aspire to become a URL transformation framework. So, instead it takes things up a notch by allowing you to directly instantiate the client for accessing any given service and inject it into the source.
Creating clients for Services using the @Factory annotation
In the previous example we saw how we could obtain the url for a service and create a JDBC connection with it. Any project that wants a JDBC connection can copy that snippet and it will work great, as long as the user remembers that he needs to set the actual database name.
Wouldn’t it be great, if instead of copying and pasting that snippet one could component-ise it and reuse it? Here’s where the factory annotation kicks in. You can annotate with @Factory any method that accept as an argument a service url and returns an object created using the URL (e.g. a client to a service). So for the previous example we could have a MysqlConnectionFactory:
import java.sql.Connection; import io.fabric8.annotations.Factory; import io.fabric8.annotations.ServiceName; public class MysqlConnectionFactory { @Factory @ServiceName public Connection createConnection(@ServiceName @Protocol("jdbc:mysql") String url) { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection(serivceUrl + "/" + DB); } }
Then instead of injecting the URL one could directly inject the connection, as shown below.
import java.sql.Connection; import javax.inject.Inject; import io.fabric8.annotations.ServiceName; public class MysqlExampleWithFactory { private Connection connection; public MysqlExampleWithProtocol(@Inject @ServiceName("mysql") Connection connection) { this.connection = connection; } //More stuff }
What happens here?
When the CDI application starts, the Fabric8 extension will receive events about all annotated methods. It will track all available factories, so for for any non-String injection point annotated with @ServiceName, it will create a Producer that under the hood uses the matching @Factory.
In the example above first the MysqlConnectionFactory will get registered, and when Connection instance with the @ServiceName qualifier gets detected a Producer that delegates to the MysqlConnectionFactory will be created (all qualifiers will be respected).
This is awesome, but it is also simplistic too. Why?
Because rarely a such a factory only requires a url to the service. In most cases other configuration parameters are required, like:
- Authentication information
- Connection timeouts
- more ….
Using @Factory with @Configuration
In the next section we are going to see factories that use configuration data. I am going to use the mysql jdbc example and add support for specifying configurable credentials. But before that I am going to ask a rhetorical question?
“How, can you configure a containerised application?”
The shortest possible answer is “Using Environment Variables”.
So in this example I’ll assume that the credentials are passed to the container that needs to access mysql using the following environment variables:
- MYSQL_USERNAME
- MYSQL_PASSWORD
Now we need to see how our @Factory can use those.
I’ve you wanted to use environment variables inside CDI in the past, chances are that you’ve used Apache DeltaSpike. This project among other provides the @ConfigProperty annotation, which allows you to inject an environment variable into a CDI bean (it does more than that actually).
import org.apache.deltaspike.core.api.config.ConfigProperty; import javax.inject.Inject; public class MysqlConfiguration { @Inject @ConfigProperty(name = "USERNAME", defaultValue = "admin") private String username; @Inject @ConfigProperty(name = "PASSWORD", defaultValue = "admin") private String password; @Inject @ConfigProperty(name = "DATABASE_NAME", defaultValue = "mydb") private String databaseName; public String getUsername() { return username; } public String getPassword() { return password; } public String getDatabaseName() { return databaseName; } }
This bean could be combined with the @Factory method, so that we can pass configuration to the factory itself.
But what if we had multiple database servers, configured with a different set of credentials, or multiple databases? In this case we could use the service name as a prefix, and let Fabric8 figure out which environment variables it should look up for each @Configuration instance.
import javax.inject.Inject; import io.fabric8.annotations.ServiceName; import io.fabric8.annotations.Factory; import io.fabric8.annotations.Protocol; import io.fabric8.annotations.Configuration; public class MysqlExampleWithFactoryAndConfiguration { @Factory @ServiceName public Connection createConnection(@ServiceName @Protocol("jdbc:mysql") String url, @Configuration MysqlConfiguration conf) { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection(serivceUrl + "/" + conf.getDatabaseName(), conf.getUsername(), conf.getPassword()); } }
Now, we have a reusable component that can be used with any mysql database running inside kubernetes and is fully configurable.
There are additional features in the Fabric8 CDI extension, but since this post is already too long, they will be covered in future posts.
Stay tuned.
Reference: | Injecting Kubernetes Services in CDI managed beans using Fabric8 from our JCG partner Ioannis Canellos at the Ioannis Canellos Blog blog. |