Cloud-Native Java: A Comprehensive Guide to Spring Cloud Kubernetes

Article

Spring Cloud Kubernetes bridges the gap between Spring Cloud's familiar programming model and Kubernetes' powerful container orchestration capabilities. It allows Java developers to leverage Kubernetes native features like ConfigMaps, Secrets, and service discovery while using the familiar Spring Cloud abstractions. This article explores how to build, deploy, and manage Java applications using Spring Cloud Kubernetes in production environments.


What is Spring Cloud Kubernetes?

Spring Cloud Kubernetes provides Spring Cloud common interface implementations that consume Kubernetes native services. Key features include:

  • Configuration Management: Access ConfigMaps and Secrets as Spring properties
  • Service Discovery: Discover services using Kubernetes API instead of Eureka
  • Load Balancing: Integrate with Kubernetes services for client-side load balancing
  • Pod Metadata: Access pod information and lifecycle events
  • Profile-based Configuration: Use Kubernetes namespaces as Spring profiles

Architecture Overview

Spring Boot Application
↓
Spring Cloud Kubernetes
↓
Kubernetes API Server
↓
Kubernetes Resources (ConfigMaps, Secrets, Services, Pods)

Setting Up Spring Cloud Kubernetes

1. Dependencies Setup

<properties>
<spring-cloud.version>2022.0.4</spring-cloud.version>
<spring-cloud-kubernetes.version>3.0.4</spring-cloud-kubernetes.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Cloud Kubernetes -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId>
<version>${spring-cloud-kubernetes.version}</version>
</dependency>
<!-- Configuration Support -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-config</artifactId>
<version>${spring-cloud-kubernetes.version}</version>
</dependency>
<!-- Discovery Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-discovery</artifactId>
<version>${spring-cloud-kubernetes.version}</version>
</dependency>
<!-- Load Balancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-loadbalancer</artifactId>
<version>${spring-cloud-kubernetes.version}</version>
</dependency>
<!-- For reactive support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>

2. Basic Configuration

application.yml:

spring:
application:
name: user-service
cloud:
kubernetes:
config:
name: ${spring.application.name}
namespace: default
sources:
- name: ${spring.application.name}
- name: common-config
enable-api: true
discovery:
all-namespaces: false
primary-port-name: http
reload:
enabled: true
mode: event
strategy: refresh
management:
endpoints:
web:
exposure:
include: health,info,metrics,configmaps
endpoint:
health:
show-details: always
configmaps:
enabled: true
server:
port: 8080
logging:
level:
org.springframework.cloud.kubernetes: DEBUG

Configuration Management with ConfigMaps and Secrets

1. Creating ConfigMaps

configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
name: user-service
namespace: default
data:
application.yml: |
app:
database:
url: jdbc:postgresql://postgresql:5432/users
pool-size: 10
cache:
enabled: true
ttl: 3600
features:
notification: true
analytics: false
logging:
level:
com.example.userservice: INFO
server:
servlet:
context-path: /api/v1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: common-config
namespace: default
data:
application.yml: |
spring:
jackson:
date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
time-zone: UTC
management:
health:
db:
enabled: true
diskspace:
enabled: true

2. Creating Secrets

secret.yaml:

apiVersion: v1
kind: Secret
metadata:
name: user-service-secrets
namespace: default
type: Opaque
data:
database-password: cG9zdGdyZXNfcGFzc3dvcmQ=  # base64 encoded
api-key: YXBpX2tleV9zZWNyZXQ=                # base64 encoded
jwt-secret: amJ0X3NlY3JldF9rZXk=             # base64 encoded

3. Java Configuration Classes

@Configuration
@ConfigurationProperties(prefix = "app")
@Data
public class AppConfig {
private DatabaseConfig database;
private CacheConfig cache;
private FeaturesConfig features;
@Data
public static class DatabaseConfig {
private String url;
private int poolSize;
}
@Data
public static class CacheConfig {
private boolean enabled;
private long ttl;
}
@Data
public static class FeaturesConfig {
private boolean notification;
private boolean analytics;
}
}
@Component
@Slf4j
public class SecretManager {
@Value("${database-password:}")
private String databasePassword;
@Value("${api-key:}")
private String apiKey;
@Value("${jwt-secret:}")
private String jwtSecret;
@PostConstruct
public void init() {
log.info("Secrets loaded successfully");
// Secrets are automatically injected from Kubernetes Secrets
}
}

Service Discovery and Load Balancing

1. Service Discovery Configuration

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
@Service
@Slf4j
public class OrderServiceClient {
private final WebClient webClient;
private final DiscoveryClient discoveryClient;
public OrderServiceClient(WebClient.Builder webClientBuilder, 
DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
this.webClient = webClientBuilder.build();
}
// Method 1: Using service name directly with load balancer
public Mono<Order> getOrderById(String orderId) {
return webClient.get()
.uri("http://order-service/api/v1/orders/{orderId}", orderId)
.retrieve()
.bodyToMono(Order.class)
.doOnSuccess(order -> log.info("Retrieved order: {}", order.getId()))
.doOnError(error -> log.error("Failed to retrieve order: {}", orderId, error));
}
// Method 2: Manual service discovery
public List<ServiceInstance> discoverOrderServiceInstances() {
return discoveryClient.getInstances("order-service");
}
// Method 3: Direct service instance access
public Mono<String> getOrderServiceInfo() {
List<ServiceInstance> instances = discoveryClient.getInstances("order-service");
if (!instances.isEmpty()) {
ServiceInstance instance = instances.get(0);
String url = instance.getUri().toString();
return Mono.just("Order service available at: " + url);
}
return Mono.just("Order service not available");
}
}

2. Load Balanced REST Client

@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
}
@RestController
@RequestMapping("/api/v1/users")
@Slf4j
public class UserController {
private final RestTemplate restTemplate;
private final OrderServiceClient orderServiceClient;
public UserController(@LoadBalanced RestTemplate restTemplate,
OrderServiceClient orderServiceClient) {
this.restTemplate = restTemplate;
this.orderServiceClient = orderServiceClient;
}
@GetMapping("/{userId}/orders")
public ResponseEntity<List<Order>> getUserOrders(@PathVariable String userId) {
log.info("Fetching orders for user: {}", userId);
// Using RestTemplate with service discovery
String url = "http://order-service/api/v1/orders?userId=" + userId;
Order[] orders = restTemplate.getForObject(url, Order[].class);
return ResponseEntity.ok(Arrays.asList(orders != null ? orders : new Order[0]));
}
@GetMapping("/{userId}/orders/reactive")
public Mono<ResponseEntity<List<Order>>> getUserOrdersReactive(@PathVariable String userId) {
return orderServiceClient.getOrderServiceInfo()
.doOnNext(info -> log.info("Service info: {}", info))
.thenMany(Flux.range(1, 3)
.flatMap(i -> orderServiceClient.getOrderById(userId + "-" + i))
.collectList())
.map(ResponseEntity::ok)
.onErrorReturn(ResponseEntity.internalServerError().build());
}
}

Configuration Reloading

1. Dynamic Configuration Updates

@Configuration
@Slf4j
public class DynamicConfigReload {
@Autowired
private ConfigReloadProperties reloadProperties;
@EventListener
public void onConfigUpdate(ConfigMapChangeEvent event) {
log.info("ConfigMap changed: {}", event.getConfigMapName());
log.info("Updated keys: {}", event.getUpdatedKeys());
// Perform application-specific reload logic
reloadConfiguration();
}
@EventListener
public void onSecretUpdate(SecretChangeEvent event) {
log.info("Secret changed: {}", event.getSecretName());
log.info("Updated keys: {}", event.getUpdatedKeys());
// Reload sensitive configuration
reloadSecrets();
}
private void reloadConfiguration() {
log.info("Reloading application configuration...");
// Add custom reload logic here
}
private void reloadSecrets() {
log.info("Reloading secrets...");
// Add secret reload logic here
}
}
@Component
@RefreshScope
public class RefreshableService {
@Value("${app.cache.enabled:false}")
private boolean cacheEnabled;
@Value("${app.cache.ttl:300}")
private long cacheTtl;
public void updateCacheConfiguration() {
log.info("Cache enabled: {}, TTL: {} seconds", cacheEnabled, cacheTtl);
}
}

Kubernetes Native Features

1. Pod Information and Metadata

@Component
@Slf4j
public class KubernetesMetadataService {
private final PodUtils podUtils;
private final KubernetesClient kubernetesClient;
public KubernetesMetadataService(PodUtils podUtils, 
KubernetesClient kubernetesClient) {
this.podUtils = podUtils;
this.kubernetesClient = kubernetesClient;
}
public PodInfo getCurrentPodInfo() {
try {
Pod currentPod = podUtils.currentPod().get();
return PodInfo.builder()
.name(currentPod.getMetadata().getName())
.namespace(currentPod.getMetadata().getNamespace())
.ip(currentPod.getStatus().getPodIP())
.nodeName(currentPod.getSpec().getNodeName())
.startTime(currentPod.getStatus().getStartTime())
.build();
} catch (Exception e) {
log.warn("Could not retrieve current pod information", e);
return PodInfo.builder().build();
}
}
public List<PodInfo> getApplicationPods() {
String appName = System.getenv("APP_NAME");
if (appName == null) {
appName = "user-service";
}
return kubernetesClient.pods()
.inNamespace("default")
.withLabel("app", appName)
.list()
.getItems()
.stream()
.map(pod -> PodInfo.builder()
.name(pod.getMetadata().getName())
.namespace(pod.getMetadata().getNamespace())
.ip(pod.getStatus().getPodIP())
.phase(pod.getStatus().getPhase())
.build())
.collect(Collectors.toList());
}
}
@Data
@Builder
class PodInfo {
private String name;
private String namespace;
private String ip;
private String nodeName;
private String phase;
private String startTime;
}

2. Custom Health Indicators

@Component
@Slf4j
public class KubernetesHealthIndicator implements HealthIndicator {
private final KubernetesClient kubernetesClient;
private final DiscoveryClient discoveryClient;
public KubernetesHealthIndicator(KubernetesClient kubernetesClient,
DiscoveryClient discoveryClient) {
this.kubernetesClient = kubernetesClient;
this.discoveryClient = discoveryClient;
}
@Override
public Health health() {
try {
// Check Kubernetes API server connectivity
kubernetesClient.pods().inNamespace("default").list();
// Check service discovery
List<String> services = discoveryClient.getServices();
Map<String, Object> details = new HashMap<>();
details.put("kubernetesApi", "reachable");
details.put("discoveredServices", services.size());
details.put("timestamp", Instant.now().toString());
return Health.up()
.withDetails(details)
.build();
} catch (Exception e) {
log.error("Kubernetes health check failed", e);
return Health.down()
.withException(e)
.build();
}
}
}
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection connection = dataSource.getConnection()) {
// Try to execute a simple query
try (Statement statement = connection.createStatement()) {
statement.execute("SELECT 1");
}
return Health.up()
.withDetail("database", "connected")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("database", "disconnected")
.withException(e)
.build();
}
}
}

Advanced Features

1. Leader Election

@Component
@Slf4j
public class KubernetesLeaderElection {
private final KubernetesClient kubernetesClient;
private volatile boolean isLeader = false;
public KubernetesLeaderElection(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
}
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
startLeaderElection();
}
private void startLeaderElection() {
String podName = System.getenv("HOSTNAME");
if (podName == null) {
podName = "unknown-pod";
}
LeaderElector leaderElector = kubernetesClient.leaderElector()
.withConfig(new LeaderElectionConfigBuilder()
.withName("user-service-leader")
.withLeaseDuration(Duration.ofSeconds(15))
.withRenewDeadline(Duration.ofSeconds(10))
.withRetryPeriod(Duration.ofSeconds(2))
.withLeaderElectorBuilder(new LeaderElectorBuilder()
.withConfig(new io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfigBuilder()
.withName("user-service-leader")
.withNamespace("default")
.withLeaseDuration(15000)
.withRenewDeadline(10000)
.withRetryPeriod(2000)
.withLeaderCallbacks(new LeaderCallbacks(
() -> onNewLeader(podName),
() -> onStartedLeading(),
() -> onStoppedLeading()
))
.build()))
.build());
// Start leader election in background
CompletableFuture.runAsync(leaderElector::run);
}
private void onNewLeader(String leaderId) {
log.info("New leader elected: {}", leaderId);
}
private void onStartedLeading() {
isLeader = true;
log.info("This pod is now the leader");
startLeaderTasks();
}
private void onStoppedLeading() {
isLeader = false;
log.info("This pod is no longer the leader");
stopLeaderTasks();
}
private void startLeaderTasks() {
// Start tasks that should only run on the leader
log.info("Starting leader-specific tasks...");
}
private void stopLeaderTasks() {
// Stop leader-specific tasks
log.info("Stopping leader-specific tasks...");
}
public boolean isLeader() {
return isLeader;
}
}

2. Custom Resource Definitions (CRD) Support

@Group("example.com")
@Version("v1")
public class ApplicationConfig extends CustomResource {
private ApplicationConfigSpec spec;
public ApplicationConfigSpec getSpec() {
return spec;
}
public void setSpec(ApplicationConfigSpec spec) {
this.spec = spec;
}
public static class ApplicationConfigSpec {
private String environment;
private int replicas;
private Map<String, String> config;
// getters and setters
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public int getReplicas() { return replicas; }
public void setReplicas(int replicas) { this.replicas = replicas; }
public Map<String, String> getConfig() { return config; }
public void setConfig(Map<String, String> config) { this.config = config; }
}
}
@Component
@Slf4j
public class ApplicationConfigWatcher {
private final KubernetesClient kubernetesClient;
public ApplicationConfigWatcher(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
}
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
watchApplicationConfigs();
}
private void watchApplicationConfigs() {
kubernetesClient.resources(ApplicationConfig.class)
.inNamespace("default")
.watch(new Watcher<ApplicationConfig>() {
@Override
public void eventReceived(Action action, ApplicationConfig resource) {
log.info("ApplicationConfig {}: {}", action, resource.getMetadata().getName());
handleApplicationConfigChange(action, resource);
}
@Override
public void onClose(WatcherException cause) {
log.warn("ApplicationConfig watcher closed", cause);
// Implement reconnection logic
}
});
}
private void handleApplicationConfigChange(Watcher.Action action, ApplicationConfig config) {
switch (action) {
case ADDED, MODIFIED -> applyApplicationConfig(config);
case DELETED -> removeApplicationConfig(config);
}
}
private void applyApplicationConfig(ApplicationConfig config) {
log.info("Applying ApplicationConfig: {}", config.getMetadata().getName());
// Apply configuration changes to the application
}
private void removeApplicationConfig(ApplicationConfig config) {
log.info("Removing ApplicationConfig: {}", config.getMetadata().getName());
// Handle configuration removal
}
}

Deployment Manifests

1. Deployment Configuration

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: default
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
serviceAccountName: user-service-account
containers:
- name: user-service
image: myregistry/user-service:1.0.0
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: APP_NAME
value: "user-service"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: default
labels:
app: user-service
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: user-service
type: ClusterIP

2. RBAC Configuration

rbac.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
name: user-service-account
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: user-service-role
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: user-service-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: user-service-role
subjects:
- kind: ServiceAccount
name: user-service-account
namespace: default

Best Practices

1. Configuration Management

  • Use ConfigMaps for non-sensitive configuration
  • Use Secrets for sensitive data (with proper encryption)
  • Implement configuration reloading for dynamic updates
  • Use namespaces for environment isolation

2. Service Discovery

  • Leverage Kubernetes native service discovery
  • Implement proper health checks and readiness probes
  • Use circuit breakers for inter-service communication

3. Security

  • Implement proper RBAC rules
  • Use service accounts with minimal permissions
  • Secure API server communication
  • Regularly rotate secrets

Conclusion

Spring Cloud Kubernetes provides a powerful bridge between Spring Boot applications and Kubernetes-native features. By leveraging this integration, Java developers can:

  • Simplify configuration management using ConfigMaps and Secrets
  • Implement robust service discovery without additional service registry
  • Enable dynamic configuration updates with zero downtime
  • Access Kubernetes metadata for application logic
  • Implement leader election for distributed coordination
  • Monitor application health with Kubernetes-native probes

This combination allows Java applications to fully embrace cloud-native principles while maintaining the developer productivity and ecosystem benefits of the Spring framework. The integration provides a production-ready foundation for building scalable, resilient, and maintainable microservices on Kubernetes.

Leave a Reply

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


Macro Nepal Helper