The Quarkus Kubernetes extension provides seamless integration with Kubernetes, enabling easy deployment, configuration, and management of Quarkus applications in Kubernetes environments.
Setup and Dependencies
1. Maven Configuration
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.company</groupId> <artifactId>quarkus-k8s-app</artifactId> <version>1.0.0-SNAPSHOT</version> <properties> <quarkus.version>3.2.0.Final</quarkus.version> <compiler-plugin.version>3.11.0</compiler-plugin.version> <surefire-plugin.version>3.0.0</surefire-plugin.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-bom</artifactId> <version>${quarkus.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Kubernetes Extension --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-kubernetes</artifactId> </dependency> <!-- Kubernetes Client --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-kubernetes-client</artifactId> </dependency> <!-- Configurations for specific Kubernetes distributions --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-openshift</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-minikube</artifactId> </dependency> <!-- Health checks --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-health</artifactId> </dependency> <!-- Metrics --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-micrometer-registry-prometheus</artifactId> </dependency> <!-- RESTEasy Reactive --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive-jackson</artifactId> </dependency> <!-- Configuration --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-config-yaml</artifactId> </dependency> <!-- Testing --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.quarkus</groupId> <artifactId>quarkus-maven-plugin</artifactId> <version>${quarkus.version}</version> <extensions>true</extensions> <executions> <execution> <goals> <goal>build</goal> <goal>generate-code</goal> <goal>generate-code-tests</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler-plugin.version}</version> <configuration> <parameters>true</parameters> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${surefire-plugin.version}</version> <configuration> <systemPropertyVariables> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> </systemPropertyVariables> </configuration> </plugin> </plugins> </build> </project> 2. Application Configuration
# src/main/resources/application.yml quarkus: application: name: user-service version: 1.0.0 # Kubernetes Configuration kubernetes: # Generate Kubernetes resources generate: true export: true # Deployment configuration deployment: kind: Deployment replicas: 3 labels: app.kubernetes.io/part-of: user-management app.kubernetes.io/version: ${quarkus.application.version} annotations: description: "User Management Microservice" # Container configuration container: image: quay.io/company/user-service:${quarkus.application.version} image-pull-policy: IfNotPresent ports: - name: http container-port: 8080 env: - name: JAVA_OPTS value: "-Xmx256m -Xms128m" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" # Service configuration service: type: ClusterIP ports: - name: http port: 8080 target-port: 8080 # Ingress configuration ingress: expose: true annotations: nginx.ingress.kubernetes.io/rewrite-target: / rules: - host: users.company.com path: / # Liveness and Readiness probes liveness-probe: http-action-path: /q/health/live initial-delay: 30 period: 10 readiness-probe: http-action-path: /q/health/ready initial-delay: 5 period: 10 # Service Account service-account: user-service-account # Security Context security-context: run-as-user: 1001 run-as-non-root: true # Node Selector and Affinity node-selector: disktype: ssd affinity: node-affinity: required-during-scheduling-ignored-during-execution: node-selector-terms: - match-expressions: - key: kubernetes.io/arch operator: In values: - amd64 # Health Configuration smallrye-health: root-path: /q/health # Metrics Configuration micrometer: export: prometheus: path: /q/metrics binders: process: enabled: true enabled: true # Logging log: level: INFO category: "com.company": DEBUG # HTTP Server http: port: 8080 host: 0.0.0.0 cors: true # Kubernetes Client kubernetes-client: namespace: default trust-certs: true Kubernetes Resource Generation
1. Custom Resource Configuration
package com.company.config; import io.quarkus.kubernetes.client.KubernetesConfigSourceProvider; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithName; import java.util.Map; import java.util.Optional; /** * Kubernetes-specific configuration mapping */ @ConfigMapping(prefix = "k8s") public interface KubernetesConfig { @WithName("deployment") DeploymentConfig deployment(); @WithName("service") ServiceConfig service(); @WithName("ingress") Optional<IngressConfig> ingress(); @WithName("resources") ResourceConfig resources(); interface DeploymentConfig { int replicas(); Map<String, String> labels(); Map<String, String> annotations(); } interface ServiceConfig { String type(); Map<String, String> annotations(); } interface IngressConfig { String host(); String path(); Map<String, String> annotations(); } interface ResourceConfig { Memory memory(); Cpu cpu(); interface Memory { String request(); String limit(); } interface Cpu { String request(); String limit(); } } } 2. Custom Kubernetes Resources
package com.company.k8s; 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; import io.fabric8.kubernetes.model.annotation.Kind; /** * Custom Resource Definition for Application Configuration */ @Group("company.com") @Version("v1") @Kind("ApplicationConfig") public class ApplicationConfig extends CustomResource<ApplicationConfigSpec, ApplicationConfigStatus> implements Namespaced { // Custom Resource implementation } class ApplicationConfigSpec { private String environment; private DatabaseConfig database; private CacheConfig cache; private SecurityConfig security; // Getters and setters public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } 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 SecurityConfig getSecurity() { return security; } public void setSecurity(SecurityConfig security) { this.security = security; } } class ApplicationConfigStatus { private String phase; private String message; private Conditions conditions; // 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 Conditions getConditions() { return conditions; } public void setConditions(Conditions conditions) { this.conditions = conditions; } } // Supporting classes class DatabaseConfig { private String url; private String username; private String passwordSecret; // Getters and setters } class CacheConfig { private String redisUrl; private int ttl; // Getters and setters } class SecurityConfig { private boolean tlsEnabled; private String secretName; // Getters and setters } class Conditions { private boolean available; private boolean ready; // Getters and setters } Kubernetes Client Integration
1. Kubernetes Client Service
package com.company.k8s.client; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.quarkus.kubernetes.client.KubernetesClientObjectMapper; import org.eclipse.microprofile.config.inject.ConfigProperty; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import java.util.List; import java.util.Map; import java.util.Optional; /** * Service for Kubernetes API operations */ @ApplicationScoped public class KubernetesClientService { @Inject KubernetesClient kubernetesClient; @Inject @KubernetesClientObjectMapper com.fasterxml.jackson.databind.ObjectMapper objectMapper; @ConfigProperty(name = "quarkus.kubernetes-client.namespace", defaultValue = "default") String namespace; // Deployment Operations public Deployment createDeployment(Deployment deployment) { return kubernetesClient.apps() .deployments() .inNamespace(namespace) .create(deployment); } public List<Deployment> getDeployments() { return kubernetesClient.apps() .deployments() .inNamespace(namespace) .list() .getItems(); } public Optional<Deployment> getDeployment(String name) { Deployment deployment = kubernetesClient.apps() .deployments() .inNamespace(namespace) .withName(name) .get(); return Optional.ofNullable(deployment); } public Deployment updateDeployment(String name, Deployment deployment) { return kubernetesClient.apps() .deployments() .inNamespace(namespace) .withName(name) .patch(deployment); } public boolean deleteDeployment(String name) { return kubernetesClient.apps() .deployments() .inNamespace(namespace) .withName(name) .delete(); } // Pod Operations public List<Pod> getPodsByLabel(String labelSelector) { return kubernetesClient.pods() .inNamespace(namespace) .withLabelSelector(labelSelector) .list() .getItems(); } public List<Pod> getPodsForDeployment(String deploymentName) { Optional<Deployment> deployment = getDeployment(deploymentName); if (deployment.isEmpty()) { return List.of(); } Map<String, String> labels = deployment.get().getSpec().getSelector().getMatchLabels(); StringBuilder labelSelector = new StringBuilder(); labels.forEach((key, value) -> { if (!labelSelector.isEmpty()) { labelSelector.append(","); } labelSelector.append(key).append("=").append(value); }); return getPodsByLabel(labelSelector.toString()); } // ConfigMap Operations public ConfigMap createConfigMap(String name, Map<String, String> data) { ConfigMap configMap = new ConfigMapBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .endMetadata() .withData(data) .build(); return kubernetesClient.configMaps() .inNamespace(namespace) .create(configMap); } public Optional<ConfigMap> getConfigMap(String name) { ConfigMap configMap = kubernetesClient.configMaps() .inNamespace(namespace) .withName(name) .get(); return Optional.ofNullable(configMap); } // Secret Operations public Secret createSecret(String name, Map<String, String> data) { Secret secret = new SecretBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .endMetadata() .withStringData(data) .build(); return kubernetesClient.secrets() .inNamespace(namespace) .create(secret); } // Namespace Operations public List<Namespace> getNamespaces() { return kubernetesClient.namespaces() .list() .getItems(); } public Namespace createNamespace(String name) { Namespace namespaceObj = new NamespaceBuilder() .withNewMetadata() .withName(name) .endMetadata() .build(); return kubernetesClient.namespaces() .create(namespaceObj); } // Custom Resource Operations public <T extends io.fabric8.kubernetes.client.CustomResource<?, ?>> MixedOperation<T, ?, ?> getCustomResourceClient(Class<T> resourceClass) { return kubernetesClient.resources(resourceClass); } // Scale Deployment public Deployment scaleDeployment(String name, int replicas) { return kubernetesClient.apps() .deployments() .inNamespace(namespace) .withName(name) .scale(replicas); } // Get Pod Logs public String getPodLogs(String podName) { return kubernetesClient.pods() .inNamespace(namespace) .withName(podName) .getLog(); } // Execute Command in Pod public String executeCommandInPod(String podName, String containerName, String... command) { return kubernetesClient.pods() .inNamespace(namespace) .withName(podName) .inContainer(containerName) .writingOutput(System.out) .exec(command); } // Health Check - Verify cluster connectivity public boolean isClusterHealthy() { try { kubernetesClient.getApiGroups(); return true; } catch (Exception e) { return false; } } } 2. Custom Resource Watcher
package com.company.k8s.watcher; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import com.company.k8s.ApplicationConfig; import org.jboss.logging.Logger; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Watcher for ApplicationConfig custom resources */ @ApplicationScoped public class ApplicationConfigWatcher implements Watcher<ApplicationConfig> { private static final Logger LOG = Logger.getLogger(ApplicationConfigWatcher.class); @Inject MixedOperation<ApplicationConfig, ?, ?> applicationConfigClient; @Inject ApplicationConfigService applicationConfigService; private final ConcurrentMap<String, ApplicationConfig> cache = new ConcurrentHashMap<>(); public void startWatching() { LOG.info("Starting ApplicationConfig watcher..."); applicationConfigClient.inAnyNamespace().watch(this); } @Override public void eventReceived(Action action, ApplicationConfig resource) { String name = resource.getMetadata().getName(); String namespace = resource.getMetadata().getNamespace(); LOG.infof("ApplicationConfig %s in namespace %s: %s", name, namespace, action); switch (action) { case ADDED: case MODIFIED: cache.put(getCacheKey(namespace, name), resource); applicationConfigService.handleConfigUpdate(resource); break; case DELETED: cache.remove(getCacheKey(namespace, name)); applicationConfigService.handleConfigDeletion(resource); break; case ERROR: LOG.errorf("Error watching ApplicationConfig %s in namespace %s", name, namespace); break; } } @Override public void onClose(WatcherException cause) { if (cause != null) { LOG.error("Watcher closed with error", cause); // Attempt to restart watcher restartWatcher(); } else { LOG.info("Watcher closed normally"); } } private void restartWatcher() { try { Thread.sleep(5000); // Wait 5 seconds before restart startWatching(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.error("Failed to restart watcher", e); } } private String getCacheKey(String namespace, String name) { return namespace + "/" + name; } public ApplicationConfig getCachedConfig(String namespace, String name) { return cache.get(getCacheKey(namespace, name)); } } Health Checks and Metrics
1. Custom Health Checks
package com.company.health; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Liveness; import org.eclipse.microprofile.health.Readiness; import org.eclipse.microprofile.health.Startup; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; /** * Kubernetes health checks */ @ApplicationScoped public class KubernetesHealthChecks { @Inject com.company.k8s.client.KubernetesClientService kubernetesClientService; @Liveness HealthCheck liveness() { return () -> HealthCheckResponse.named("user-service-liveness") .status(isServiceAlive()) .withData("timestamp", System.currentTimeMillis()) .build(); } @Readiness HealthCheck readiness() { return () -> HealthCheckResponse.named("user-service-readiness") .status(isServiceReady()) .withData("database", checkDatabaseConnectivity()) .withData("cache", checkCacheConnectivity()) .withData("kubernetes", checkKubernetesConnectivity()) .build(); } @Startup HealthCheck startup() { return () -> HealthCheckResponse.named("user-service-startup") .status(isStartupComplete()) .withData("migrations", checkDatabaseMigrations()) .withData("config", checkConfigurationLoaded()) .build(); } private boolean isServiceAlive() { // Basic liveness check - is the application running? return true; } private boolean isServiceReady() { // Readiness check - can the application handle requests? return checkDatabaseConnectivity() && checkCacheConnectivity() && checkKubernetesConnectivity(); } private boolean isStartupComplete() { // Startup check - is the application fully initialized? return checkDatabaseMigrations() && checkConfigurationLoaded(); } private boolean checkDatabaseConnectivity() { // Implement database connectivity check return true; } private boolean checkCacheConnectivity() { // Implement cache connectivity check return true; } private boolean checkKubernetesConnectivity() { return kubernetesClientService.isClusterHealthy(); } private boolean checkDatabaseMigrations() { // Check if database migrations completed successfully return true; } private boolean checkConfigurationLoaded() { // Check if all required configuration is loaded return true; } } 2. Custom Metrics
package com.company.metrics; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.eclipse.microprofile.metrics.MetricUnits; import org.eclipse.microprofile.metrics.annotation.Gauge; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; /** * Application metrics for Kubernetes monitoring */ @ApplicationScoped public class ApplicationMetrics { @Inject MeterRegistry meterRegistry; private final Counter requestsTotal; private final Counter errorsTotal; private final Timer requestDuration; private final ConcurrentMap<String, Counter> endpointCounters; public ApplicationMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.requestsTotal = Counter.builder("app_requests_total") .description("Total number of requests") .register(meterRegistry); this.errorsTotal = Counter.builder("app_errors_total") .description("Total number of errors") .register(meterRegistry); this.requestDuration = Timer.builder("app_request_duration_seconds") .description("Request duration in seconds") .register(meterRegistry); this.endpointCounters = new ConcurrentHashMap<>(); } public void recordRequest(String endpoint, long durationMs) { requestsTotal.increment(); requestDuration.record(durationMs, TimeUnit.MILLISECONDS); // Record per-endpoint metrics String counterName = "app_endpoint_requests_total"; endpointCounters.computeIfAbsent(endpoint, ep -> Counter.builder(counterName) .tag("endpoint", ep) .register(meterRegistry) ).increment(); } public void recordError(String endpoint, String errorType) { errorsTotal.increment(); String counterName = "app_endpoint_errors_total"; endpointCounters.computeIfAbsent(endpoint + "_" + errorType, key -> Counter.builder(counterName) .tag("endpoint", endpoint) .tag("type", errorType) .register(meterRegistry) ).increment(); } @Gauge(name = "app_active_connections", unit = MetricUnits.NONE) public int getActiveConnections() { // Implement active connections tracking return 0; } @Gauge(name = "app_cache_size", unit = MetricUnits.NONE) public long getCacheSize() { // Implement cache size tracking return 0L; } @Gauge(name = "app_queue_size", unit = MetricUnits.NONE) public int getQueueSize() { // Implement queue size tracking return 0; } } REST API with Kubernetes Integration
1. REST Resource with Kubernetes Operations
package com.company.resource; import com.company.k8s.client.KubernetesClientService; import com.company.metrics.ApplicationMetrics; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; import java.util.Map; /** * REST resource exposing Kubernetes operations */ @Path("/api/k8s") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class KubernetesResource { private static final Logger LOG = Logger.getLogger(KubernetesResource.class); @Inject KubernetesClientService kubernetesClientService; @Inject ApplicationMetrics metrics; @ConfigProperty(name = "quarkus.application.name") String applicationName; @GET @Path("/deployments") public Response getDeployments() { long startTime = System.currentTimeMillis(); try { List<io.fabric8.kubernetes.api.model.apps.Deployment> deployments = kubernetesClientService.getDeployments(); metrics.recordRequest("getDeployments", System.currentTimeMillis() - startTime); return Response.ok(deployments).build(); } catch (Exception e) { metrics.recordError("getDeployments", "kubernetes_error"); LOG.error("Failed to get deployments", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve deployments")) .build(); } } @GET @Path("/deployments/{name}") public Response getDeployment(@PathParam("name") String name) { long startTime = System.currentTimeMillis(); try { var deployment = kubernetesClientService.getDeployment(name); if (deployment.isEmpty()) { return Response.status(Response.Status.NOT_FOUND) .entity(Map.of("error", "Deployment not found: " + name)) .build(); } metrics.recordRequest("getDeployment", System.currentTimeMillis() - startTime); return Response.ok(deployment.get()).build(); } catch (Exception e) { metrics.recordError("getDeployment", "kubernetes_error"); LOG.errorf("Failed to get deployment: %s", name, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve deployment: " + name)) .build(); } } @POST @Path("/deployments/{name}/scale") public Response scaleDeployment( @PathParam("name") String name, @QueryParam("replicas") int replicas) { long startTime = System.currentTimeMillis(); if (replicas < 0 || replicas > 10) { return Response.status(Response.Status.BAD_REQUEST) .entity(Map.of("error", "Replicas must be between 0 and 10")) .build(); } try { var deployment = kubernetesClientService.scaleDeployment(name, replicas); metrics.recordRequest("scaleDeployment", System.currentTimeMillis() - startTime); return Response.ok(Map.of( "message", "Deployment scaled successfully", "deployment", deployment.getMetadata().getName(), "replicas", deployment.getSpec().getReplicas() )).build(); } catch (Exception e) { metrics.recordError("scaleDeployment", "kubernetes_error"); LOG.errorf("Failed to scale deployment: %s to %d replicas", name, replicas, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to scale deployment: " + name)) .build(); } } @GET @Path("/pods") public Response getPods(@QueryParam("labelSelector") String labelSelector) { long startTime = System.currentTimeMillis(); try { List<io.fabric8.kubernetes.api.model.Pod> pods; if (labelSelector != null && !labelSelector.isEmpty()) { pods = kubernetesClientService.getPodsByLabel(labelSelector); } else { // Get pods for current application pods = kubernetesClientService.getPodsForDeployment(applicationName); } metrics.recordRequest("getPods", System.currentTimeMillis() - startTime); return Response.ok(pods).build(); } catch (Exception e) { metrics.recordError("getPods", "kubernetes_error"); LOG.error("Failed to get pods", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve pods")) .build(); } } @GET @Path("/cluster/health") public Response getClusterHealth() { long startTime = System.currentTimeMillis(); try { boolean isHealthy = kubernetesClientService.isClusterHealthy(); metrics.recordRequest("getClusterHealth", System.currentTimeMillis() - startTime); return Response.ok(Map.of( "healthy", isHealthy, "timestamp", System.currentTimeMillis() )).build(); } catch (Exception e) { metrics.recordError("getClusterHealth", "kubernetes_error"); LOG.error("Failed to check cluster health", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to check cluster health")) .build(); } } @GET @Path("/configmaps/{name}") public Response getConfigMap(@PathParam("name") String name) { long startTime = System.currentTimeMillis(); try { var configMap = kubernetesClientService.getConfigMap(name); if (configMap.isEmpty()) { return Response.status(Response.Status.NOT_FOUND) .entity(Map.of("error", "ConfigMap not found: " + name)) .build(); } metrics.recordRequest("getConfigMap", System.currentTimeMillis() - startTime); return Response.ok(configMap.get()).build(); } catch (Exception e) { metrics.recordError("getConfigMap", "kubernetes_error"); LOG.errorf("Failed to get ConfigMap: %s", name, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve ConfigMap: " + name)) .build(); } } } 2. Application Configuration Resource
package com.company.resource; import com.company.k8s.ApplicationConfig; import com.company.k8s.client.KubernetesClientService; import io.fabric8.kubernetes.client.dsl.MixedOperation; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; import java.util.Map; /** * Resource for managing ApplicationConfig custom resources */ @Path("/api/configs") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ApplicationConfigResource { @Inject KubernetesClientService kubernetesClientService; private MixedOperation<ApplicationConfig, ?, ?> getApplicationConfigClient() { return kubernetesClientService.getCustomResourceClient(ApplicationConfig.class); } @GET public Response getAllConfigs(@QueryParam("namespace") String namespace) { try { List<ApplicationConfig> configs; if (namespace != null && !namespace.isEmpty()) { configs = getApplicationConfigClient().inNamespace(namespace).list().getItems(); } else { configs = getApplicationConfigClient().inAnyNamespace().list().getItems(); } return Response.ok(configs).build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve application configs")) .build(); } } @GET @Path("/{namespace}/{name}") public Response getConfig( @PathParam("namespace") String namespace, @PathParam("name") String name) { try { ApplicationConfig config = getApplicationConfigClient() .inNamespace(namespace) .withName(name) .get(); if (config == null) { return Response.status(Response.Status.NOT_FOUND) .entity(Map.of("error", "ApplicationConfig not found")) .build(); } return Response.ok(config).build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to retrieve application config")) .build(); } } @POST public Response createConfig(ApplicationConfig config) { try { String namespace = config.getMetadata().getNamespace(); if (namespace == null) { namespace = "default"; config.getMetadata().setNamespace(namespace); } ApplicationConfig created = getApplicationConfigClient() .inNamespace(namespace) .create(config); return Response.status(Response.Status.CREATED) .entity(created) .build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to create application config")) .build(); } } @PUT @Path("/{namespace}/{name}") public Response updateConfig( @PathParam("namespace") String namespace, @PathParam("name") String name, ApplicationConfig config) { try { ApplicationConfig updated = getApplicationConfigClient() .inNamespace(namespace) .withName(name) .patch(config); return Response.ok(updated).build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to update application config")) .build(); } } @DELETE @Path("/{namespace}/{name}") public Response deleteConfig( @PathParam("namespace") String namespace, @PathParam("name") String name) { try { boolean deleted = getApplicationConfigClient() .inNamespace(namespace) .withName(name) .delete(); if (deleted) { return Response.ok(Map.of("message", "ApplicationConfig deleted successfully")).build(); } else { return Response.status(Response.Status.NOT_FOUND) .entity(Map.of("error", "ApplicationConfig not found")) .build(); } } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Failed to delete application config")) .build(); } } } Deployment and Build Configuration
1. Dockerfile Configuration
# Multi-stage Dockerfile FROM quay.io/quarkus/ubi-quarkus-native-image:22.3-java17 AS build WORKDIR /project COPY --chown=quarkus:quarkus mvnw /project/mvnw COPY --chown=quarkus:quarkus .mvn /project/.mvn COPY --chown=quarkus:quarkus pom.xml /project/ COPY --chown=quarkus:quarkus src /project/src USER quarkus RUN ./mvnw package -Dnative FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7 WORKDIR /work/ COPY --from=build /project/target/*-runner /work/application RUN chmod 775 /work EXPOSE 8080 USER 1001 CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
2. Kubernetes Deployment Manifests
# Generated by Quarkus Kubernetes extension apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: default labels: app.kubernetes.io/name: user-service app.kubernetes.io/version: 1.0.0 app.kubernetes.io/part-of: user-management annotations: description: "User Management Microservice" spec: replicas: 3 selector: matchLabels: app.kubernetes.io/name: user-service template: metadata: labels: app.kubernetes.io/name: user-service app.kubernetes.io/version: 1.0.0 spec: serviceAccountName: user-service-account securityContext: runAsUser: 1001 runAsNonRoot: true containers: - name: user-service image: quay.io/company/user-service:1.0.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: JAVA_OPTS value: "-Xmx256m -Xms128m" - name: KUBERNETES_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /q/health/live port: http initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /q/health/ready port: http initialDelaySeconds: 5 periodSeconds: 10 startupProbe: httpGet: path: /q/health/started port: http initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 30 --- apiVersion: v1 kind: Service metadata: name: user-service namespace: default labels: app.kubernetes.io/name: user-service app.kubernetes.io/version: 1.0.0 spec: ports: - name: http port: 8080 targetPort: 8080 selector: app.kubernetes.io/name: user-service type: ClusterIP --- apiVersion: v1 kind: ServiceAccount metadata: name: user-service-account namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: default name: user-service-role rules: - apiGroups: [""] resources: ["pods", "services", "configmaps", "secrets"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "patch"] - apiGroups: ["company.com"] resources: ["applicationconfigs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: user-service-role-binding namespace: default subjects: - kind: ServiceAccount name: user-service-account namespace: default roleRef: kind: Role name: user-service-role apiGroup: rbac.authorization.k8s.io
Testing
1. Kubernetes Client Test
package com.company.k8s.test; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.kubernetes.client.KubernetesTestServer; import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import javax.inject.Inject; import static org.junit.jupiter.api.Assertions.*; @QuarkusTest @WithKubernetesTestServer class KubernetesClientServiceTest { @KubernetesTestServer KubernetesServer mockServer; @Inject KubernetesClient kubernetesClient; @Inject com.company.k8s.client.KubernetesClientService kubernetesClientService; @BeforeEach void setup() { // Setup mock responses mockServer.expect().get().withPath("/api/v1/namespaces/default/pods") .andReturn(200, "{\"items\": []}") .always(); } @Test void testClusterHealth() { assertTrue(kubernetesClientService.isClusterHealthy()); } @Test void testGetDeployments() { var deployments = kubernetesClientService.getDeployments(); assertNotNull(deployments); assertTrue(deployments.isEmpty()); } } 2. Resource Test
package com.company.resource.test; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; import static org.hamcrest.Matchers.*; @QuarkusTest class KubernetesResourceTest { @Test void testClusterHealth() { RestAssured.given() .when().get("/api/k8s/cluster/health") .then() .statusCode(200) .body("healthy", is(true)); } @Test void testGetDeployments() { RestAssured.given() .when().get("/api/k8s/deployments") .then() .statusCode(200) .body("$", hasSize(greaterThanOrEqualTo(0))); } } Summary
The Quarkus Kubernetes extension provides:
- Seamless Kubernetes Integration: Automatic resource generation and deployment
- Type-Safe Kubernetes Client: Easy interaction with Kubernetes API
- Health and Metrics: Built-in health checks and Prometheus metrics
- Custom Resources: Support for Custom Resource Definitions
- Security: RBAC configuration and security context
- Development Experience: Fast development with live coding and testing
Key Benefits:
- Reduced boilerplate for Kubernetes deployments
- Native compilation for fast startup and low memory footprint
- Comprehensive monitoring and observability
- Easy configuration management
- Excellent developer experience with live reload
This setup enables rapid development of cloud-native Java applications that are optimized for Kubernetes environments.