google-site-verification: google61fe8ba583a51912.html
Operator Pattern in Java: Building Kubernetes Native Applications

The Operator Pattern extends Kubernetes to manage complex, stateful applications using custom controllers and custom resources. This guide covers implementing Kubernetes Operators in Java using the Fabric8 Kubernetes Client and Java Operator SDK.


Core Concepts

What is a Kubernetes Operator?

  • Custom controller that extends Kubernetes API
  • Manages complex, stateful applications
  • Encodes operational knowledge into software
  • Uses Custom Resource Definitions (CRDs)

Key Components:

  • Custom Resource Definition (CRD): Extends Kubernetes API
  • Custom Resource (CR): Instance of the CRD
  • Controller/Operator: Reconciliation loop
  • Reconciliation: Desired state vs. current state

Dependencies and Setup

1. Maven Dependencies
<properties> <java-operator-sdk.version>4.4.0</java-operator-sdk.version> <fabric8.version>6.7.2</fabric8.version> <quarkus.version>3.2.3.Final</quarkus.version> </properties> <dependencies> <!-- Java Operator SDK --> <dependency> <groupId>io.javaoperatorsdk</groupId> <artifactId>operator-framework</artifactId> <version>${java-operator-sdk.version}</version> </dependency> <!-- Kubernetes Client --> <dependency> <groupId>io.fabric8</groupId> <artifactId>kubernetes-client</artifactId> <version>${fabric8.version}</version> </dependency> <!-- Quarkus (optional, for runtime) --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> <version>${quarkus.version}</version> </dependency> <!-- Testing --> <dependency> <groupId>io.javaoperatorsdk</groupId> <artifactId>operator-framework-junit</artifactId> <version>${java-operator-sdk.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies>
2. Project Structure
java-operator/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── operator/ │ │ │ ├── crd/ │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ └── OperatorApplication.java │ │ └── resources/ │ │ └── crds/ │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── operator/ └── pom.xml

Core Implementation

1. Custom Resource Definition (CRD)
// src/main/java/com/example/operator/crd/JavaApp.java package com.example.operator.crd; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; import io.fabric8.kubernetes.model.annotation.Kind; @Group("operator.java.example.com") @Version("v1alpha1") @Kind("JavaApp") public class JavaApp extends CustomResource<JavaAppSpec, JavaAppStatus> { @Override protected JavaAppStatus initStatus() { return new JavaAppStatus(); } } // JavaAppSpec - Desired state package com.example.operator.crd; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.fabric8.kubernetes.model.annotation.PrinterColumn; @JsonDeserialize public class JavaAppSpec { @JsonProperty @PrinterColumn(name = "APPLICATION", description = "The application name", priority = 1) private String applicationName; @JsonProperty @PrinterColumn(name = "VERSION", description = "The application version", priority = 2) private String version; @JsonProperty private int replicas = 1; @JsonProperty private ResourceRequirements resources; @JsonProperty private DatabaseConfig database; @JsonProperty private CacheConfig cache; @JsonProperty private ServiceConfig service; // Getters and setters public String getApplicationName() { return applicationName; } public void setApplicationName(String applicationName) { this.applicationName = applicationName; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public int getReplicas() { return replicas; } public void setReplicas(int replicas) { this.replicas = replicas; } public ResourceRequirements getResources() { return resources; } public void setResources(ResourceRequirements resources) { this.resources = resources; } public DatabaseConfig getDatabase() { return database; } public void setDatabase(DatabaseConfig database) { this.database = database; } public CacheConfig getCache() { return cache; } public void setCache(CacheConfig cache) { this.cache = cache; } public ServiceConfig getService() { return service; } public void setService(ServiceConfig service) { this.service = service; } } // Supporting classes package com.example.operator.crd; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize public class ResourceRequirements { @JsonProperty private String memory = "512Mi"; @JsonProperty private String cpu = "250m"; @JsonProperty private String storage = "10Gi"; // Getters and setters public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public String getStorage() { return storage; } public void setStorage(String storage) { this.storage = storage; } } @JsonDeserialize public class DatabaseConfig { @JsonProperty private boolean enabled = false; @JsonProperty private String type = "postgresql"; @JsonProperty private String version = "13"; // Getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } } @JsonDeserialize public class CacheConfig { @JsonProperty private boolean enabled = false; @JsonProperty private String type = "redis"; @JsonProperty private String version = "7.0"; // Getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } } @JsonDeserialize public class ServiceConfig { @JsonProperty private String type = "ClusterIP"; @JsonProperty private int port = 8080; @JsonProperty private boolean metricsEnabled = true; @JsonProperty private boolean healthChecksEnabled = true; // Getters and setters public String getType() { return type; } public void setType(String type) { this.type = type; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public boolean isMetricsEnabled() { return metricsEnabled; } public void setMetricsEnabled(boolean metricsEnabled) { this.metricsEnabled = metricsEnabled; } public boolean isHealthChecksEnabled() { return healthChecksEnabled; } public void setHealthChecksEnabled(boolean healthChecksEnabled) { this.healthChecksEnabled = healthChecksEnabled; } } // JavaAppStatus - Actual state package com.example.operator.crd; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.fabric8.kubernetes.model.annotation.PrinterColumn; import java.util.List; @JsonDeserialize public class JavaAppStatus { @JsonProperty @PrinterColumn(name = "PHASE", description = "The current phase", priority = 0) private String phase = "Pending"; @JsonProperty private String message; @JsonProperty private int availableReplicas; @JsonProperty private List<Condition> conditions; @JsonProperty private String lastUpdateTime; // Getters and setters public String getPhase() { return phase; } public void setPhase(String phase) { this.phase = phase; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public int getAvailableReplicas() { return availableReplicas; } public void setAvailableReplicas(int availableReplicas) { this.availableReplicas = availableReplicas; } public List<Condition> getConditions() { return conditions; } public void setConditions(List<Condition> conditions) { this.conditions = conditions; } public String getLastUpdateTime() { return lastUpdateTime; } public void setLastUpdateTime(String lastUpdateTime) { this.lastUpdateTime = lastUpdateTime; } } @JsonDeserialize public class Condition { @JsonProperty private String type; @JsonProperty private String status; @JsonProperty private String reason; @JsonProperty private String message; @JsonProperty private String lastTransitionTime; // Getters and setters public String getType() { return type; } public void setType(String type) { this.type = type; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getLastTransitionTime() { return lastTransitionTime; } public void setLastTransitionTime(String lastTransitionTime) { this.lastTransitionTime = lastTransitionTime; } }
2. CRD YAML Definition
# src/main/resources/crds/javaapp-crd.yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: javaapps.operator.java.example.com labels: app.kubernetes.io/name: javaapp app.kubernetes.io/version: "1.0.0" spec: group: operator.java.example.com versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: applicationName: type: string version: type: string replicas: type: integer minimum: 1 maximum: 10 resources: type: object properties: memory: type: string cpu: type: string storage: type: string database: type: object properties: enabled: type: boolean type: type: string version: type: string cache: type: object properties: enabled: type: boolean type: type: string version: type: string service: type: object properties: type: type: string port: type: integer metricsEnabled: type: boolean healthChecksEnabled: type: boolean status: type: object properties: phase: type: string message: type: string availableReplicas: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string reason: type: string message: type: string lastTransitionTime: type: string subresources: status: {} scope: Namespaced names: plural: javaapps singular: javaapp kind: JavaApp shortNames: - japp categories: - all
3. Operator Controller
// src/main/java/com/example/operator/controller/JavaAppController.java package com.example.operator.controller; import com.example.operator.crd.JavaApp; import com.example.operator.crd.JavaAppStatus; import com.example.operator.service.KubernetesService; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; import java.util.concurrent.TimeUnit; @ControllerConfiguration( namespaces = Constants.WATCH_CURRENT_NAMESPACE, name = "javaapp", retry = @Retry(maxAttempts = 5, backoff = @Backoff(delay = 2, delayUnit = TimeUnit.SECONDS)) ) public class JavaAppController implements Reconciler<JavaApp> { private static final Logger log = LoggerFactory.getLogger(JavaAppController.class); private final KubernetesService kubernetesService; public JavaAppController(KubernetesService kubernetesService) { this.kubernetesService = kubernetesService; } @Override public UpdateControl<JavaApp> reconcile(JavaApp javaApp, Context<JavaApp> context) { log.info("Reconciling JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName()); try { // Update status to Processing updateStatus(javaApp, "Processing", "Reconciliation in progress"); // 1. Create or update deployment kubernetesService.createOrUpdateDeployment(javaApp); // 2. Create or update service kubernetesService.createOrUpdateService(javaApp); // 3. Create or update config maps kubernetesService.createOrUpdateConfigMaps(javaApp); // 4. Handle database if enabled if (javaApp.getSpec().getDatabase().isEnabled()) { kubernetesService.createOrUpdateDatabase(javaApp); } // 5. Handle cache if enabled if (javaApp.getSpec().getCache().isEnabled()) { kubernetesService.createOrUpdateCache(javaApp); } // 6. Update status with success updateStatus(javaApp, "Ready", "Application deployed successfully"); log.info("Successfully reconciled JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName()); return UpdateControl.updateStatus(javaApp); } catch (Exception e) { log.error("Error reconciling JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName(), e); updateStatus(javaApp, "Error", "Reconciliation failed: " + e.getMessage()); return UpdateControl.updateStatus(javaApp); } } @Override public DeleteControl cleanup(JavaApp javaApp, Context<JavaApp> context) { log.info("Cleaning up JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName()); try { kubernetesService.cleanupResources(javaApp); log.info("Successfully cleaned up JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName()); } catch (Exception e) { log.error("Error cleaning up JavaApp: {}/{}", javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName(), e); } return DeleteControl.defaultDelete(); } private void updateStatus(JavaApp javaApp, String phase, String message) { JavaAppStatus status = javaApp.getStatus(); if (status == null) { status = new JavaAppStatus(); javaApp.setStatus(status); } status.setPhase(phase); status.setMessage(message); status.setLastUpdateTime(Instant.now().toString()); // Update available replicas from deployment status int availableReplicas = kubernetesService.getAvailableReplicas( javaApp.getMetadata().getNamespace(), javaApp.getMetadata().getName() ); status.setAvailableReplicas(availableReplicas); } }
4. Kubernetes Service
// src/main/java/com/example/operator/service/KubernetesService.java package com.example.operator.service; import com.example.operator.crd.JavaApp; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import java.util.HashMap; import java.util.Map; @ApplicationScoped public class KubernetesService { private static final Logger log = LoggerFactory.getLogger(KubernetesService.class); @Inject KubernetesClient kubernetesClient; private static final String APP_LABEL = "app.kubernetes.io/name"; private static final String INSTANCE_LABEL = "app.kubernetes.io/instance"; private static final String MANAGED_BY_LABEL = "app.kubernetes.io/managed-by"; private static final String MANAGED_BY_VALUE = "java-operator"; public void createOrUpdateDeployment(JavaApp javaApp) { String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); Deployment deployment = new DeploymentBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .withLabels(createLabels(javaApp)) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withNewSpec() .withReplicas(javaApp.getSpec().getReplicas()) .withNewSelector() .withMatchLabels(createMatchLabels(javaApp)) .endSelector() .withNewTemplate() .withNewMetadata() .withLabels(createLabels(javaApp)) .endMetadata() .withNewSpec() .addNewContainer() .withName("java-app") .withImage("eclipse-temurin:17-jre") .withImagePullPolicy("IfNotPresent") .withPorts(createContainerPorts(javaApp)) .withEnv(createEnvironmentVariables(javaApp)) .withResources(createResourceRequirements(javaApp)) .withLivenessProbe(createLivenessProbe(javaApp)) .withReadinessProbe(createReadinessProbe(javaApp)) .withVolumeMounts(createVolumeMounts(javaApp)) .endContainer() .withVolumes(createVolumes(javaApp)) .withRestartPolicy("Always") .endSpec() .endTemplate() .endSpec() .build(); kubernetesClient.apps().deployments() .inNamespace(namespace) .resource(deployment) .createOrReplace(); log.info("Created/Updated deployment for JavaApp: {}/{}", namespace, name); } public void createOrUpdateService(JavaApp javaApp) { String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); Service service = new ServiceBuilder() .withNewMetadata() .withName(name + "-service") .withNamespace(namespace) .withLabels(createLabels(javaApp)) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withNewSpec() .withType(javaApp.getSpec().getService().getType()) .withSelector(createMatchLabels(javaApp)) .addNewPort() .withPort(javaApp.getSpec().getService().getPort()) .withTargetPort(new IntOrString(javaApp.getSpec().getService().getPort())) .withProtocol("TCP") .endPort() .endSpec() .build(); kubernetesClient.services() .inNamespace(namespace) .resource(service) .createOrReplace(); log.info("Created/Updated service for JavaApp: {}/{}", namespace, name); } public void createOrUpdateConfigMaps(JavaApp javaApp) { String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); // Application configuration ConfigMap appConfig = new ConfigMapBuilder() .withNewMetadata() .withName(name + "-config") .withNamespace(namespace) .withLabels(createLabels(javaApp)) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withData(createConfigMapData(javaApp)) .build(); kubernetesClient.configMaps() .inNamespace(namespace) .resource(appConfig) .createOrReplace(); log.info("Created/Updated config maps for JavaApp: {}/{}", namespace, name); } public void createOrUpdateDatabase(JavaApp javaApp) { if (!javaApp.getSpec().getDatabase().isEnabled()) { return; } String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); // Create database deployment Deployment dbDeployment = new DeploymentBuilder() .withNewMetadata() .withName(name + "-database") .withNamespace(namespace) .withLabels(createLabels(javaApp)) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withNewSpec() .withReplicas(1) .withNewSelector() .withMatchLabels(createDatabaseLabels(javaApp)) .endSelector() .withNewTemplate() .withNewMetadata() .withLabels(createDatabaseLabels(javaApp)) .endMetadata() .withNewSpec() .addNewContainer() .withName("database") .withImage(javaApp.getSpec().getDatabase().getType() + ":" + javaApp.getSpec().getDatabase().getVersion()) .withEnv(createDatabaseEnvVars(javaApp)) .withPorts(new ContainerPortBuilder() .withContainerPort(5432) .build()) .withResources(createDatabaseResourceRequirements(javaApp)) .withVolumeMounts(new VolumeMountBuilder() .withName("db-data") .withMountPath("/var/lib/postgresql/data") .build()) .endContainer() .withVolumes(new VolumeBuilder() .withName("db-data") .withNewPersistentVolumeClaim() .withClaimName(name + "-db-pvc") .endPersistentVolumeClaim() .build()) .endSpec() .endTemplate() .endSpec() .build(); kubernetesClient.apps().deployments() .inNamespace(namespace) .resource(dbDeployment) .createOrReplace(); // Create database service Service dbService = new ServiceBuilder() .withNewMetadata() .withName(name + "-database-service") .withNamespace(namespace) .withLabels(createDatabaseLabels(javaApp)) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withNewSpec() .withSelector(createDatabaseLabels(javaApp)) .addNewPort() .withPort(5432) .withTargetPort(new IntOrString(5432)) .endPort() .endSpec() .build(); kubernetesClient.services() .inNamespace(namespace) .resource(dbService) .createOrReplace(); // Create PVC PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() .withNewMetadata() .withName(name + "-db-pvc") .withNamespace(namespace) .addToOwnerReferences(createOwnerReference(javaApp)) .endMetadata() .withNewSpec() .withAccessModes("ReadWriteOnce") .withNewResources() .withRequests(Map.of("storage", new Quantity(javaApp.getSpec().getResources().getStorage()))) .endResources() .endSpec() .build(); kubernetesClient.persistentVolumeClaims() .inNamespace(namespace) .resource(pvc) .createOrReplace(); log.info("Created/Updated database for JavaApp: {}/{}", namespace, name); } public void createOrUpdateCache(JavaApp javaApp) { if (!javaApp.getSpec().getCache().isEnabled()) { return; } String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); // Similar implementation for cache (Redis, etc.) log.info("Cache deployment for JavaApp: {}/{}", namespace, name); } public void cleanupResources(JavaApp javaApp) { String namespace = javaApp.getMetadata().getNamespace(); String name = javaApp.getMetadata().getName(); // Delete deployment kubernetesClient.apps().deployments() .inNamespace(namespace) .withName(name) .delete(); // Delete service kubernetesClient.services() .inNamespace(namespace) .withName(name + "-service") .delete(); // Delete config maps kubernetesClient.configMaps() .inNamespace(namespace) .withName(name + "-config") .delete(); // Delete database resources if they exist if (javaApp.getSpec().getDatabase().isEnabled()) { kubernetesClient.apps().deployments() .inNamespace(namespace) .withName(name + "-database") .delete(); kubernetesClient.services() .inNamespace(namespace) .withName(name + "-database-service") .delete(); kubernetesClient.persistentVolumeClaims() .inNamespace(namespace) .withName(name + "-db-pvc") .delete(); } log.info("Cleaned up resources for JavaApp: {}/{}", namespace, name); } public int getAvailableReplicas(String namespace, String name) { try { Deployment deployment = kubernetesClient.apps().deployments() .inNamespace(namespace) .withName(name) .get(); return deployment != null && deployment.getStatus() != null ? deployment.getStatus().getAvailableReplicas() : 0; } catch (Exception e) { log.warn("Could not get available replicas for {}/{}", namespace, name, e); return 0; } } // Helper methods for creating Kubernetes objects private Map<String, String> createLabels(JavaApp javaApp) { Map<String, String> labels = new HashMap<>(); labels.put(APP_LABEL, "java-app"); labels.put(INSTANCE_LABEL, javaApp.getMetadata().getName()); labels.put(MANAGED_BY_LABEL, MANAGED_BY_VALUE); labels.put("app", javaApp.getMetadata().getName()); return labels; } private Map<String, String> createMatchLabels(JavaApp javaApp) { Map<String, String> labels = new HashMap<>(); labels.put("app", javaApp.getMetadata().getName()); return labels; } private Map<String, String> createDatabaseLabels(JavaApp javaApp) { Map<String, String> labels = new HashMap<>(); labels.put(APP_LABEL, "database"); labels.put(INSTANCE_LABEL, javaApp.getMetadata().getName() + "-database"); labels.put(MANAGED_BY_LABEL, MANAGED_BY_VALUE); return labels; } private OwnerReference createOwnerReference(JavaApp javaApp) { return new OwnerReferenceBuilder() .withApiVersion(javaApp.getApiVersion()) .withKind(javaApp.getKind()) .withName(javaApp.getMetadata().getName()) .withUid(javaApp.getMetadata().getUid()) .withBlockOwnerDeletion(true) .withController(true) .build(); } private List<ContainerPort> createContainerPorts(JavaApp javaApp) { ContainerPort port = new ContainerPortBuilder() .withContainerPort(javaApp.getSpec().getService().getPort()) .withProtocol("TCP") .build(); return List.of(port); } private List<EnvVar> createEnvironmentVariables(JavaApp javaApp) { List<EnvVar> envVars = new ArrayList<>(); envVars.add(new EnvVarBuilder() .withName("JAVA_OPTS") .withValue("-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0") .build()); envVars.add(new EnvVarBuilder() .withName("SPRING_PROFILES_ACTIVE") .withValue("kubernetes") .build()); if (javaApp.getSpec().getDatabase().isEnabled()) { envVars.add(new EnvVarBuilder() .withName("SPRING_DATASOURCE_URL") .withValue("jdbc:postgresql://" + javaApp.getMetadata().getName() + "-database-service:5432/" + javaApp.getMetadata().getName()) .build()); } return envVars; } private List<EnvVar> createDatabaseEnvVars(JavaApp javaApp) { List<EnvVar> envVars = new ArrayList<>(); envVars.add(new EnvVarBuilder() .withName("POSTGRES_DB") .withValue(javaApp.getMetadata().getName()) .build()); envVars.add(new EnvVarBuilder() .withName("POSTGRES_USER") .withValue("appuser") .build()); envVars.add(new EnvVarBuilder() .withName("POSTGRES_PASSWORD") .withValue("password") .build()); return envVars; } private ResourceRequirements createResourceRequirements(JavaApp javaApp) { return new ResourceRequirementsBuilder() .withRequests(Map.of( "memory", new Quantity(javaApp.getSpec().getResources().getMemory()), "cpu", new Quantity(javaApp.getSpec().getResources().getCpu()) )) .withLimits(Map.of( "memory", new Quantity(javaApp.getSpec().getResources().getMemory()), "cpu", new Quantity(javaApp.getSpec().getResources().getCpu()) )) .build(); } private ResourceRequirements createDatabaseResourceRequirements(JavaApp javaApp) { return new ResourceRequirementsBuilder() .withRequests(Map.of( "memory", new Quantity("256Mi"), "cpu", new Quantity("100m") )) .withLimits(Map.of( "memory", new Quantity("512Mi"), "cpu", new Quantity("200m") )) .build(); } private Probe createLivenessProbe(JavaApp javaApp) { if (!javaApp.getSpec().getService().isHealthChecksEnabled()) { return null; } return new ProbeBuilder() .withNewHttpGet() .withPath("/actuator/health/liveness") .withPort(new IntOrString(javaApp.getSpec().getService().getPort())) .endHttpGet() .withInitialDelaySeconds(30) .withPeriodSeconds(10) .withTimeoutSeconds(5) .withFailureThreshold(3) .build(); } private Probe createReadinessProbe(JavaApp javaApp) { if (!javaApp.getSpec().getService().isHealthChecksEnabled()) { return null; } return new ProbeBuilder() .withNewHttpGet() .withPath("/actuator/health/readiness") .withPort(new IntOrString(javaApp.getSpec().getService().getPort())) .endHttpGet() .withInitialDelaySeconds(5) .withPeriodSeconds(5) .withTimeoutSeconds(3) .withFailureThreshold(3) .build(); } private List<VolumeMount> createVolumeMounts(JavaApp javaApp) { VolumeMount configMount = new VolumeMountBuilder() .withName("config-volume") .withMountPath("/app/config") .withReadOnly(true) .build(); return List.of(configMount); } private List<Volume> createVolumes(JavaApp javaApp) { Volume configVolume = new VolumeBuilder() .withName("config-volume") .withNewConfigMap() .withName(javaApp.getMetadata().getName() + "-config") .endConfigMap() .build(); return List.of(configVolume); } private Map<String, String> createConfigMapData(JavaApp javaApp) { Map<String, String> data = new HashMap<>(); String applicationYml = """ server: port: %d spring: application: name: %s management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always probes: enabled: true """.formatted(javaApp.getSpec().getService().getPort(), javaApp.getSpec().getApplicationName()); data.put("application.yml", applicationYml); return data; } }
5. Operator Application
// src/main/java/com/example/operator/OperatorApplication.java package com.example.operator; import com.example.operator.controller.JavaAppController; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.javaoperatorsdk.operator.Operator; import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.QuarkusApplication; import io.quarkus.runtime.annotations.QuarkusMain; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @QuarkusMain public class OperatorApplication { public static void main(String[] args) { Quarkus.run(OperatorRunner.class, args); } public static class OperatorRunner implements QuarkusApplication { private static final Logger log = LoggerFactory.getLogger(OperatorRunner.class); @Inject Operator operator; @Override public int run(String... args) throws Exception { log.info("Starting Java Operator..."); operator.start(); log.info("Java Operator started successfully"); Quarkus.waitForExit(); return 0; } } } // Operator Configuration package com.example.operator; import com.example.operator.controller.JavaAppController; import io.javaoperatorsdk.operator.Operator; import io.quarkus.runtime.StartupEvent; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.enterprise.inject.Produces; import javax.inject.Inject; @ApplicationScoped public class OperatorConfiguration { @Inject JavaAppController javaAppController; void onStartup(@Observes StartupEvent ev) { // Operator startup logic handled by Quarkus extension } @Produces @ApplicationScoped Operator operator(JavaAppController javaAppController) { Operator operator = new Operator(); operator.register(javaAppController); return operator; } }

Deployment Configuration

1. Operator Deployment
# src/main/resources/kubernetes/operator-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: java-operator namespace: java-operator-system labels: app.kubernetes.io/name: java-operator app.kubernetes.io/version: "1.0.0" spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: java-operator template: metadata: labels: app.kubernetes.io/name: java-operator spec: serviceAccountName: java-operator containers: - name: operator image: ghcr.io/myorg/java-operator:latest ports: - containerPort: 8080 protocol: TCP env: - name: WATCH_NAMESPACE value: "" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "200m" livenessProbe: httpGet: path: /q/health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 5 readinessProbe: httpGet: path: /q/health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: java-operator namespace: java-operator-system labels: app.kubernetes.io/name: java-operator spec: ports: - port: 8080 targetPort: 8080 protocol: TCP name: http selector: app.kubernetes.io/name: java-operator --- apiVersion: v1 kind: ServiceAccount metadata: name: java-operator namespace: java-operator-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: java-operator rules: - apiGroups: [""] resources: ["pods", "services", "configmaps", "persistentvolumeclaims"] verbs: ["*"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["*"] - apiGroups: ["operator.java.example.com"] resources: ["javaapps"] verbs: ["*"] - apiGroups: ["operator.java.example.com"] resources: ["javaapps/status"] verbs: ["get", "patch", "update"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: java-operator subjects: - kind: ServiceAccount name: java-operator namespace: java-operator-system roleRef: kind: ClusterRole name: java-operator apiGroup: rbac.authorization.k8s.io
2. Custom Resource Example
# examples/my-java-app.yaml apiVersion: operator.java.example.com/v1alpha1 kind: JavaApp metadata: name: my-java-application namespace: default spec: applicationName: "my-java-app" version: "1.0.0" replicas: 3 resources: memory: "1Gi" cpu: "500m" storage: "20Gi" database: enabled: true type: "postgresql" version: "13" cache: enabled: true type: "redis" version: "7.0" service: type: "ClusterIP" port: 8080 metricsEnabled: true healthChecksEnabled: true

Testing

1. Unit Tests
// src/test/java/com/example/operator/JavaAppControllerTest.java package com.example.operator; import com.example.operator.controller.JavaAppController; import com.example.operator.crd.JavaApp; import com.example.operator.crd.JavaAppSpec; import com.example.operator.service.KubernetesService; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class JavaAppControllerTest { @Mock private KubernetesService kubernetesService; private JavaAppController controller; @BeforeEach void setUp() { controller = new JavaAppController(kubernetesService); } @Test void shouldReconcileJavaAppSuccessfully() { // Given JavaApp javaApp = createTestJavaApp(); // When UpdateControl<JavaApp> result = controller.reconcile(javaApp, null); // Then assertNotNull(result); assertTrue(result.isUpdateStatusSubResource()); verify(kubernetesService).createOrUpdateDeployment(javaApp); verify(kubernetesService).createOrUpdateService(javaApp); verify(kubernetesService).createOrUpdateConfigMaps(javaApp); } @Test void shouldHandleDatabaseWhenEnabled() { // Given JavaApp javaApp = createTestJavaApp(); javaApp.getSpec().getDatabase().setEnabled(true); // When controller.reconcile(javaApp, null); // Then verify(kubernetesService).createOrUpdateDatabase(javaApp); } @Test void shouldCleanupResourcesOnDelete() { // Given JavaApp javaApp = createTestJavaApp(); // When controller.cleanup(javaApp, null); // Then verify(kubernetesService).cleanupResources(javaApp); } private JavaApp createTestJavaApp() { JavaApp javaApp = new JavaApp(); ObjectMeta metadata = new ObjectMeta(); metadata.setName("test-app"); metadata.setNamespace("default"); javaApp.setMetadata(metadata); JavaAppSpec spec = new JavaAppSpec(); spec.setApplicationName("Test Application"); spec.setVersion("1.0.0"); spec.setReplicas(2); javaApp.setSpec(spec); return javaApp; } }
2. Integration Tests
// src/test/java/com/example/operator/JavaAppControllerIT.java package com.example.operator; import com.example.operator.crd.JavaApp; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import static org.awaitility.Awaitility.await; import static java.util.concurrent.TimeUnit.SECONDS; class JavaAppControllerIT { @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder() .withReconciler(new JavaAppController(operator.getKubernetesClient())) .build(); @Test void shouldCreateJavaAppResource() { // Given JavaApp javaApp = createTestJavaApp(); // When operator.getKubernetesClient().resources(JavaApp.class) .inNamespace("default") .resource(javaApp) .create(); // Then await().atMost(30, SECONDS).until(() -> { JavaApp updated = operator.getKubernetesClient().resources(JavaApp.class) .inNamespace("default") .withName("test-app") .get(); return updated != null && "Ready".equals(updated.getStatus().getPhase()); }); } private JavaApp createTestJavaApp() { JavaApp javaApp = new JavaApp(); ObjectMeta metadata = new ObjectMeta(); metadata.setName("test-app"); metadata.setNamespace("default"); javaApp.setMetadata(metadata); // Set spec as needed return javaApp; } }

Advanced Features

1. Event Handling
// Event handling in controller package com.example.operator.controller; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import java.util.Map; import java.util.concurrent.TimeUnit; public class JavaAppController implements Reconciler<JavaApp>, EventSourceInitializer<JavaApp> { private TimerEventSource timerEventSource; @Override public Map<String, EventSource> prepareEventSources(EventSourceContext<JavaApp> context) { timerEventSource = new TimerEventSource(); return Map.of("timer", timerEventSource); } public void scheduleReconciliation(JavaApp javaApp, long delay, TimeUnit unit) { timerEventSource.schedule(javaApp, delay, unit); } }
2. Metrics Integration
// Metrics service package com.example.operator.service; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @ApplicationScoped public class MetricsService { private static final Logger log = LoggerFactory.getLogger(MetricsService.class); @Inject MeterRegistry meterRegistry; private final Counter reconciliationCounter; private final Counter errorCounter; public MetricsService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.reconciliationCounter = Counter.builder("java_operator_reconciliations_total") .description("Total number of reconciliations") .register(meterRegistry); this.errorCounter = Counter.builder("java_operator_errors_total") .description("Total number of reconciliation errors") .register(meterRegistry); } public void recordReconciliation(String appName, String namespace) { reconciliationCounter.increment(); log.info("Recorded reconciliation for {}/{}", namespace, appName); } public void recordError(String appName, String namespace) { errorCounter.increment(); log.error("Recorded error for {}/{}", namespace, appName); } }

Best Practices

  1. Idempotent Operations: Ensure reconciliation can run multiple times safely
  2. Owner References: Set owner references for garbage collection
  3. Status Updates: Keep status updated with meaningful information
  4. Error Handling: Implement robust error handling and retry logic
  5. Resource Management: Clean up resources properly
  6. Testing: Comprehensive unit and integration tests
  7. Monitoring: Implement metrics and logging
  8. Security: Follow Kubernetes security best practices

Conclusion

The Operator Pattern in Java provides:

  • Custom Automation: Encode operational knowledge into software
  • Kubernetes Native: First-class Kubernetes citizens
  • Declarative API: Users declare desired state
  • Self-Healing: Automatic reconciliation
  • Complex Application Management: Handle stateful applications

By implementing operators in Java, you leverage the rich Java ecosystem while building powerful Kubernetes-native applications that can manage complex operational workflows automatically.

Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/

OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/

OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/

Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.

https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics

Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2

Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide

Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2

Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide

Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.

https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server

Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper