From Zero to Hero: How I Built My First Kubernetes Operator in Java and Why You Should Too
using Java instead of Go
Have you ever felt you are in charge of your Kubernetes cluster? Constantly checking if pods are healthy, manually scaling services, or waking up at 2 AM because something needs a restart? I’ve been there. That’s when I discovered Kubernetes Operators — and honestly, it changed everything. But here’s the twist: while most developers think operators must be written in Go, I built mine in Java using the Java Operator SDK (JOSDK). Let me show you how the code can turn your operational nightmares into automated dreams.
What Exactly Is a Kubernetes Operator?
Think of a Kubernetes Operator as your most experienced colleague who never sleeps, never takes vacation, and never forgets to follow the runbook. It’s a pattern that captures human operational knowledge and automates it using code.
Operators use Custom Resource Definitions (CRDs) to extend Kubernetes with your own resource types, then continuously watch and reconcile the state of your applications. No more “Did someone remember to scale the database?” moments
Why Java for Operators?
While Go dominates the operator landscape, Java brings some compelling advantages:
- Familiar territory: Most enterprise teams already know Java inside and out
- Rich ecosystem: Leverage existing Java libraries and frameworks
- Quarkus power: Fast startup times and low memory footprint — perfect for cloud-native workloads
- Fabric8 client: Mature Kubernetes client library with excellent API coverage
Let’s Build Something Real: A Simple Application Operator in Java
I’m going to walk you through creating an operator that automates application deployments using the Java Operator SDK. Instead of writing multiple YAML files for Deployments and Services, we’ll create one simple custom resource that does it all.
Step 1: Setting Up Your Project
First, let’s create our Maven project with the Java Operator SDK and Quarkus:
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
<version>4.9.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-operator-sdk</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>6.9.2</version>
</dependency>
Step 2: Define Your Custom Resource
Let’s create our custom resource for applications. First, the spec class:
package com.example.operator;
public class ApplicationSpec {
private String name;
private String image;
private Integer replicas = 1;
private Integer port = 8080;
private Map<String, String> env = new HashMap<>();
// Constructors, getters, and setters
public ApplicationSpec() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getImage() { return image; }
public void setImage(String image) { this.image = image; }
public Integer getReplicas() { return replicas; }
public void setReplicas(Integer replicas) { this.replicas = replicas; }
public Integer getPort() { return port; }
public void setPort(Integer port) { this.port = port; }
public Map<String, String> getEnv() { return env; }
public void setEnv(Map<String, String> env) { this.env = env; }
}
Now the status class:
package com.example.operator;
public class ApplicationStatus {
private Integer readyReplicas = 0;
private String deploymentStatus = "Pending";
private String serviceStatus = "Pending";
// Constructors, getters, and setters
public ApplicationStatus() {}
public Integer getReadyReplicas() { return readyReplicas; }
public void setReadyReplicas(Integer readyReplicas) {
this.readyReplicas = readyReplicas;
}
public String getDeploymentStatus() { return deploymentStatus; }
public void setDeploymentStatus(String deploymentStatus) {
this.deploymentStatus = deploymentStatus;
}
public String getServiceStatus() { return serviceStatus; }
public void setServiceStatus(String serviceStatus) {
this.serviceStatus = serviceStatus;
}
}
And finally, the main custom resource:
package com.example.operator;
import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;
@Group("apps.example.com")
@Version("v1")
public class Application extends CustomResource<ApplicationSpec, ApplicationStatus>
implements Namespaced {
@Override
protected ApplicationStatus initStatus() {
return new ApplicationStatus();
}
}
Step 3: The Heart of the Operator — The Reconciler
Now comes the exciting part. Let’s implement the reconciliation logic:
package com.example.operator;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import javax.inject.Inject;
import java.util.Map;
import java.util.Optional;
@ControllerConfiguration
public class ApplicationReconciler implements Reconciler<Application> {
@Inject
KubernetesClient kubernetesClient;
@Override
public UpdateControl<Application> reconcile(Application application, Context<Application> context) {
String name = application.getSpec().getName();
String namespace = application.getMetadata().getNamespace();
try {
// Create or update Deployment
Deployment deployment = createOrUpdateDeployment(application);
// Create or update Service
Service service = createOrUpdateService(application);
// Update status
ApplicationStatus status = application.getStatus();
if (status == null) {
status = new ApplicationStatus();
}
// Get actual deployment status
Deployment actualDeployment = kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name + "-deployment")
.get();
if (actualDeployment != null) {
status.setReadyReplicas(
actualDeployment.getStatus().getReadyReplicas() != null ?
actualDeployment.getStatus().getReadyReplicas() : 0
);
status.setDeploymentStatus("Ready");
}
status.setServiceStatus("Ready");
application.setStatus(status);
return UpdateControl.updateStatus(application);
} catch (Exception e) {
// Log error and requeue
System.err.println("Error reconciling application: " + e.getMessage());
return UpdateControl.requeue();
}
}
private Deployment createOrUpdateDeployment(Application application) {
String name = application.getSpec().getName();
String namespace = application.getMetadata().getNamespace();
Deployment deployment = new DeploymentBuilder()
.withNewMetadata()
.withName(name + "-deployment")
.withNamespace(namespace)
.addNewOwnerReference()
.withApiVersion(application.getApiVersion())
.withKind(application.getKind())
.withName(application.getMetadata().getName())
.withUid(application.getMetadata().getUid())
.withController(true)
.endOwnerReference()
.endMetadata()
.withNewSpec()
.withReplicas(application.getSpec().getReplicas())
.withNewSelector()
.addToMatchLabels("app", name)
.endSelector()
.withNewTemplate()
.withNewMetadata()
.addToLabels("app", name)
.endMetadata()
.withNewSpec()
.addNewContainer()
.withName(name)
.withImage(application.getSpec().getImage())
.addNewPort()
.withContainerPort(application.getSpec().getPort())
.endPort()
.withEnv(application.getSpec().getEnv().entrySet().stream()
.map(entry -> new io.fabric8.kubernetes.api.model.EnvVarBuilder()
.withName(entry.getKey())
.withValue(entry.getValue())
.build())
.collect(java.util.stream.Collectors.toList()))
.endContainer()
.endSpec()
.endTemplate()
.endSpec()
.build();
return kubernetesClient.apps().deployments()
.inNamespace(namespace)
.createOrReplace(deployment);
}
private Service createOrUpdateService(Application application) {
String name = application.getSpec().getName();
String namespace = application.getMetadata().getNamespace();
Service service = new ServiceBuilder()
.withNewMetadata()
.withName(name + "-service")
.withNamespace(namespace)
.addNewOwnerReference()
.withApiVersion(application.getApiVersion())
.withKind(application.getKind())
.withName(application.getMetadata().getName())
.withUid(application.getMetadata().getUid())
.withController(true)
.endOwnerReference()
.endMetadata()
.withNewSpec()
.addToSelector("app", name)
.addNewPort()
.withPort(application.getSpec().getPort())
.withNewTargetPort(application.getSpec().getPort())
.endPort()
.withType("ClusterIP")
.endSpec()
.build();
return kubernetesClient.services()
.inNamespace(namespace)
.createOrReplace(service);
}
}
Step 4: Main Application Class
Create the main Quarkus application:
package com.example.operator;
import io.javaoperatorsdk.operator.Operator;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
import javax.inject.Inject;
@QuarkusMain
public class ApplicationOperatorMain implements QuarkusApplication {
@Inject
Operator operator;
public static void main(String... args) {
Quarkus.run(ApplicationOperatorMain.class, args);
}
@Override
public int run(String... args) throws Exception {
operator.start();
Quarkus.waitForExit();
return 0;
}
}
Deploy and Test Your Operator
Build your operator as a native image with Quarkus:
./mvnw clean package -Pnative -Dquarkus.native.container-build=true
docker build -f src/main/docker/Dockerfile.native -t your-registry/app-operator:v1 .
docker push your-registry/app-operator:v1
Create the deployment YAML for your operator:
apiVersion: apps/v1
kind: Deployment
metadata:
name: application-operator
spec:
replicas: 1
selector:
matchLabels:
app: application-operator
template:
metadata:
labels:
app: application-operator
spec:
containers:
- name: operator
image: your-registry/app-operator:v1
env:
- name: QUARKUS_OPERATOR_SDK_CONTROLLERS_APPLICATION_NAMESPACES
value: "default"
Now create an application using your custom resource:
apiVersion: apps.example.com/v1
kind: Application
metadata:
name: my-awesome-app
spec:
name: awesome-app
image: nginx:latest
replicas: 3
port: 80
env:
ENV: "production"
VERSION: "1.0.0"
Apply it and watch the magic happen:
kubectl apply -f my-app.yaml
kubectl get applications
kubectl get deployments
kubectl get services
kubectl describe application my-awesome-app
Your Java-based operator will automatically create the deployment and service. Change the replica count in your Application resource, and watch it scale automatically.
Why This Is a Game-Changer for Java Teams
Building this operator in Java taught me that automation isn’t just about saving time — it’s about leveraging your team’s existing skills and knowledge. Your Java developers can now contribute to infrastructure automation without learning a completely new language.
Plus, once you have operators managing your infrastructure, scaling becomes trivial. Need 50 identical applications? Just create 50 Application resources. The operator handles the rest, all written in the language your team already knows and loves.
The Java Operator SDK provides all the heavy lifting — event handling, retries, caching, and optimal Kubernetes API interaction — so you can focus on your business logic.
Ready to automate your way to freedom using Java? Start with something simple like this Application operator, then gradually add more complex logic as you get comfortable. Trust me, your future self (and your on-call rotation) will thank you for speaking the same language as the rest of your stack.
Finally, if the article was helpful, please clap 👏and follow, Thank you!
From Zero to Hero: How I Built My First Kubernetes Operator in Java and Why You Should Too was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More