Simplify Kubernetes Operator Development using Java Operator SDK

(Posted by Rohan Kumar on 06/12/2024 tagged kubernetes java operators testing )

Introduction:

Kubernetes is designed for automation. We can develop Kubernetes Operators to encapsulate the operational knowledge for running specific applications and automate routine tasks. Operators monitor Kubernetes cluster state and make changes to maintain desired configuration, acting as a specialized controller. Operators are Kubernetes controllers that extend the cluster’s behavior by managing custom resources and performing domain-specific actions.

You could develop the Kubernetes Operators in any library using a Kubernetes API Client. For Java, it would be using Fabric8 Kubernetes Client. I have written some blogposts in past for doing this:

However, I recently tried out the Java Operator SDK for developing a Kubernetes Operator and realized how easy and smooth the development process was as compared to doing everything on your own using a Kubernetes Client.

The Java Operator SDK is a library that simplifies the process of building Kubernetes operators using Java. With the Java Operator SDK, developers can leverage the power of Kubernetes in a familiar Java ecosystem, enabling seamless integration and management of custom applications and infrastructure.

History of the Project

Java Operator SDK was initially developed by a European startup named Container Solutions. It was designed to be a high-level framework for implementing operators in Java, equivalent to controller-runtime GoLang library. It got recognized by Red Hat later and received contributions.

In 2023, this project became a part of CNCF as an incubating project as part of Operator Framework (see CNCF announcement).

Prerequisites:

You would need the following things to be able to follow this article:

Setting up Application:

In order to use Java Operator SDK, you need to include this dependency in your project:

<dependency>
    <groupId>io.javaoperatorsdk</groupId>
    <artifactId>operator-framework</artifactId>
</dependency>

If you’re using Quarkus, you can use this Quarkus Extension:

<dependency>
    <groupId>io.quarkiverse.operatorsdk</groupId>
    <artifactId>quarkus-operator-sdk</artifactId>
</dependency>

If you’re using Spring Boot, you can use this Spring Boot Extension:

<dependency>
    <groupId>io.javaoperatorsdk</groupId>
    <artifactId>operator-framework-spring-boot-starter</artifactId>
    <version>${java-operator-sdk.version}</version>
</dependency>

Using Java Operator SDK in your project

In order to use Java Operator SDK in your application, you need to define a class in your application that would be responsible for reacting to all the events for your custom resource.

Let’s try to understand how to use it with the help of an example.

We will try to port Kubernetes Sample Controller using Java Operator SDK.

It manages a simple Kubernetes Custom Resource named Foo that manages a Deployment resource. You define a Foo resource where you specify Deployment name and the number of replicas you want.

Here is a diagram to give you better idea of how it would work:

Sample Operator Kubernetes

Custom Resource Definition for Foo resource

In order to create a new Custom Resource, we need to create a Custom Resource Definition in Kubernetes.

You can see YAML for creating Foo CustomResourceDefinition in GitHub repository here.

Generating Java types for Foo Custom Resource

In order to use this Kubernetes Custom resource programmatically, you would need to generate java model types. I’ve used Java Generator Maven Plugin to do this:

Here is the plugin configuration:

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>java-generator-maven-plugin</artifactId>
    <version>${fabric8.version}</version>
    <configuration>
        <source>${project.basedir}/src/main/resources/crd/foo-crd.yaml</source>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

This configuration would read CustomResourceDefinition yaml file in src/main/resources directory and generate java classes in target/generated-sources.

Writing the main code for application

Usually in a Kubernetes Operator you need to define logic to handle all the events related to custom resource in order to maintain the desired state. This is done in a Reconciler.

We will implement this in a class using already provided io.javaoperatorsdk.operator.api.reconciler.Reconciler interface and @io.javaoperatorsdk.operator.api.reconciler.ControllerConfigurationControllerConfiguration annotation:

@ControllerConfiguration(
  dependents = {
    @Dependent(type = DeploymentDependentResource.class)
  })
public class FooReconciler implements Reconciler<Foo> {
  private static final Logger logger = LoggerFactory.getLogger(FooReconciler.class.getName());

  @Override
  public UpdateControl<Foo> reconcile(final Foo foo, Context<Foo> context) throws Exception {
    return context.getSecondaryResource(Deployment.class).map(deployment -> {
      Foo updatedFoo = updateAvailableReplicasInFooStatus(foo, deployment.getSpec().getReplicas());
      logger.info("Updating status of Foo {} in namespace {} to {} ready replicas",
        foo.getMetadata().getName(),
        foo.getMetadata().getNamespace(),
        foo.getSpec().getReplicas());
      return UpdateControl.patchStatus(updatedFoo);
    }).orElseGet(UpdateControl::noUpdate);
  }

  private Foo updateAvailableReplicasInFooStatus(Foo foo, long replicas) {
    FooStatus fooStatus = new FooStatus();
    fooStatus.setAvailableReplicas(replicas);
    // NEVER modify objects from the store. It's a read-only, local cache.
    // You can create a copy manually and modify it
    Foo fooClone = Serialization.clone(foo);
    fooClone.setStatus(fooStatus);
    return fooClone;
  }
}
  • @ControllerConfiguration annotation registers the class as controller for the operator. It also allows providing additional parameters like dependent resources, namespace, etc.
    • Since this Foo resource is going to manage a Deployment, we have used a dependents configuration option. In this option, we provide a class that would be handling dependents.
  • We have used the Reconciler interface from Java Operator SDK and overridden reconcile method. Here is where we define our logic for matching desired state of the resource by updating Foo status.

Here is code for handling dependent Deployment resource, it’s the class referenced in dependents of @ControllerConfiguration:

@KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=sample-operator")
public class DeploymentDependentResource extends CRUDKubernetesDependentResource<Deployment, Foo> {
  public DeploymentDependentResource() {
    super(Deployment.class);
  }

  @Override
  protected Deployment desired(Foo foo, Context<Foo> context) {
    final ObjectMeta fooMetadata = foo.getMetadata();
    final String fooName = fooMetadata.getName();
    return new DeploymentBuilder()
      .withNewMetadata()
      .withName(fooName)
      .withNamespace(fooMetadata.getNamespace())
      .addToLabels("app", fooName)
      .addToLabels("app.kubernetes.io/part-of", fooName)
      .addToLabels("app.kubernetes.io/managed-by", "tomcat-operator")
      .endMetadata()
      .withNewSpec()
      .withNewSelector().addToMatchLabels("app", fooName).endSelector()
      .withReplicas(foo.getSpec().getReplicas().intValue())
      .withNewTemplate()
      .withNewMetadata().addToLabels("app", fooName).endMetadata()
      .withNewSpec()
      .addNewContainer()
      .withName("nginx")
      .withImage("nginx:latest").endContainer()
      .endSpec()
      .endTemplate()
      .endSpec()
      .build();
  }
}

In above class, we define desired Deployment state as per the Foo custom resource. It creates a simple Deployment object with the provided number of replicas and name with opinionated nginx:latest image.

Deploying the Operator

Now that we’ve written code for handling events and dependent resources, let’s go ahead and deploy it.

We will be using Eclipse JKube Kubernetes Maven Plugin to deploy this operator to Kubernetes Cluster.

  1. Install Custom Resource Definition first:
kubectl create -f src/main/resources/crd/foo-crd.yaml
  1. Install ClusterRole, ClusterRoleBinding and ServiceAccount for Operator to work with
kubectl create -f src/main/resources/foo-serviceaccount-and-role-binding.yml
  1. Deploy Operator to Kubernetes cluster using Kubernetes Maven Plugin
# (Optional) To point your shell to minikube's docker-daemon, run:
eval $(minikube -p minikube docker-env)
mvn package k8s:build k8s:resource k8s:apply

Testing the Operator

In order to test whether our operator is working as expected, we need to create some sample Foo custom resource YAML files and see if corresponding Deployment is getting created for each Foo resource.

Create an instance of Foo resource:

kubectl create -f src/main/resources/example-foo.yaml
foo.samplecontroller.k8s.io/example-foo created

You’d notice that Operator detected this change and created the dependent resource Deployment for this example-foo resource.

Here is a short gif of demo using Podman Desktop:

Sample Operator Kubernetes Demo

Conclusion:

The Java Operator SDK streamlines the development of Kubernetes operators, enabling Java developers to harness the full potential of Kubernetes while leveraging their existing Java skills. Whether you’re managing complex applications or automating infrastructure tasks, the Java Operator SDK provides a powerful toolkit to extend Kubernetes with custom logic and automation.

You can find code used this blog post in this GitHub repository.

References