Article
In Kubernetes environments, ConfigMaps and Secrets are essential for managing application configuration and sensitive data. However, when these resources change, applications need to detect and reload the new configuration without restarting. For Java applications, this presents a challenge that requires careful implementation to ensure zero-downtime configuration updates.
In this guide, we'll explore multiple strategies for implementing ConfigMap and Secret reloading in Java applications running on Kubernetes.
Why Dynamic Config Reloading Matters
- Zero Downtime Updates: Change configuration without restarting applications
- Security Compliance: Rotate secrets without service interruption
- DevOps Efficiency: Enable configuration changes without redeployment
- Scalability: Manage configuration across multiple pods consistently
- Emergency Response: Quickly disable features or change settings in production
Part 1: Kubernetes Fundamentals
1.1 ConfigMap and Secret Basics
# Example ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: java-app-config
namespace: default
data:
application.yaml: |
app:
name: "user-service"
version: "1.0.0"
features:
caching: true
metrics: true
database:
pool:
max-size: 20
timeout: 30s
logback.xml: |
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
# Example Secret
apiVersion: v1
kind: Secret
metadata:
name: java-app-secrets
namespace: default
type: Opaque
data:
database-password: c3VwZXJzZWNyZXRwYXNzd29yZA== # base64 encoded
api-key: YXBpLWtleS1zZWNyZXQ=
encryption-key: ZW5jcnlwdGlvbi1rZXk=
1.2 Deployment with Volume Mounts
apiVersion: apps/v1 kind: Deployment metadata: name: java-app spec: replicas: 3 selector: matchLabels: app: java-app template: metadata: labels: app: java-app annotations: # Trigger restart when config changes config.reload/auto: "true" spec: containers: - name: java-app image: my-java-app:latest ports: - containerPort: 8080 volumeMounts: - name: config-volume mountPath: /etc/app/config readOnly: true - name: secret-volume mountPath: /etc/app/secrets readOnly: true env: - name: CONFIG_DIR value: "/etc/app/config" - name: SECRET_DIR value: "/etc/app/secrets" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" volumes: - name: config-volume configMap: name: java-app-config - name: secret-volume secret: secretName: java-app-secrets
Part 2: File-Based Config Reloading
2.1 File System Watcher Service
// File: src/main/java/com/example/config/FileSystemConfigWatcher.java
package com.example.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class FileSystemConfigWatcher {
private static final Logger logger = LoggerFactory.getLogger(FileSystemConfigWatcher.class);
private final ConfigReloadService configReloadService;
private final AtomicBoolean watching = new AtomicBoolean(false);
private WatchService watchService;
private ScheduledExecutorService executorService;
// Configuration
private final String configDir = System.getenv().getOrDefault("CONFIG_DIR", "/etc/app/config");
private final String secretsDir = System.getenv().getOrDefault("SECRETS_DIR", "/etc/app/secrets");
private final long pollInterval = Long.parseLong(
System.getenv().getOrDefault("CONFIG_POLL_INTERVAL", "5000")
);
public FileSystemConfigWatcher(ConfigReloadService configReloadService) {
this.configReloadService = configReloadService;
}
@PostConstruct
public void startWatching() {
if (watching.compareAndSet(false, true)) {
logger.info("Starting file system config watcher for directories: {}, {}",
configDir, secretsDir);
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(this::checkForChanges, 0, pollInterval, TimeUnit.MILLISECONDS);
startWatchService();
}
}
@PreDestroy
public void stopWatching() {
if (watching.compareAndSet(true, false)) {
logger.info("Stopping file system config watcher");
if (executorService != null) {
executorService.shutdown();
}
if (watchService != null) {
try {
watchService.close();
} catch (IOException e) {
logger.warn("Error closing watch service", e);
}
}
}
}
private void startWatchService() {
try {
watchService = FileSystems.getDefault().newWatchService();
Path configPath = Paths.get(configDir);
Path secretsPath = Paths.get(secretsDir);
// Register for modification events
if (Files.exists(configPath)) {
configPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
}
if (Files.exists(secretsPath)) {
secretsPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
}
logger.info("Watch service registered for config and secret directories");
} catch (IOException e) {
logger.warn("Failed to start watch service, falling back to polling", e);
}
}
private void checkForChanges() {
try {
if (watchService != null) {
WatchKey key = watchService.poll(1, TimeUnit.SECONDS);
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
Path changedFile = (Path) event.context();
logger.info("Config file modified: {}", changedFile);
handleConfigChange(changedFile.toString());
}
}
key.reset();
}
} else {
// Fallback: Check file modification times
checkFileModificationTimes();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Config watcher interrupted");
} catch (Exception e) {
logger.error("Error in config watcher", e);
}
}
private void checkFileModificationTimes() {
// Implementation to check file modification times
// This is a fallback when WatchService is not available
}
private void handleConfigChange(String filename) {
try {
if (filename.endsWith(".yaml") || filename.endsWith(".yml") || filename.endsWith(".properties")) {
configReloadService.reloadApplicationConfig();
} else if (filename.endsWith(".xml")) {
configReloadService.reloadLoggingConfig();
} else {
logger.debug("Ignoring change to non-config file: {}", filename);
}
} catch (Exception e) {
logger.error("Error handling config change for file: {}", filename, e);
}
}
}
2.2 Configuration Reload Service
// File: src/main/java/com/example/config/ConfigReloadService.java
package com.example.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ConfigReloadService {
private static final Logger logger = LoggerFactory.getLogger(ConfigReloadService.class);
private final AtomicLong reloadCount = new AtomicLong(0);
private final ContextRefresher contextRefresher;
private final ConfigurableEnvironment environment;
private final SecretManager secretManager;
@Autowired
public ConfigReloadService(ContextRefresher contextRefresher,
ConfigurableEnvironment environment,
SecretManager secretManager) {
this.contextRefresher = contextRefresher;
this.environment = environment;
this.secretManager = secretManager;
}
public void reloadApplicationConfig() {
try {
long count = reloadCount.incrementAndGet();
logger.info("Reloading application configuration (reload #{})", count);
// Reload properties from files
reloadPropertiesFromFiles();
// Refresh Spring context if available
if (contextRefresher != null) {
contextRefresher.refresh();
}
logger.info("Application configuration reloaded successfully (reload #{})", count);
} catch (Exception e) {
logger.error("Failed to reload application configuration", e);
throw new ConfigReloadException("Configuration reload failed", e);
}
}
public void reloadLoggingConfig() {
try {
logger.info("Reloading logging configuration");
// Implement logging configuration reload
// This depends on your logging framework (Logback, Log4j2, etc.)
reloadLogbackConfiguration();
logger.info("Logging configuration reloaded successfully");
} catch (Exception e) {
logger.error("Failed to reload logging configuration", e);
}
}
public void reloadSecrets() {
try {
logger.info("Reloading secrets");
// Reload secrets from mounted volume
secretManager.reloadSecrets();
// Update environment properties
updateSecretProperties();
logger.info("Secrets reloaded successfully");
} catch (Exception e) {
logger.error("Failed to reload secrets", e);
}
}
private void reloadPropertiesFromFiles() {
String configDir = System.getenv().getOrDefault("CONFIG_DIR", "/etc/app/config");
try {
Path appConfigPath = Paths.get(configDir, "application.properties");
if (Files.exists(appConfigPath)) {
Properties properties = new Properties();
properties.load(Files.newInputStream(appConfigPath));
MutablePropertySources propertySources = environment.getPropertySources();
PropertiesPropertySource filePropertySource =
new PropertiesPropertySource("fileConfig", properties);
if (propertySources.contains("fileConfig")) {
propertySources.replace("fileConfig", filePropertySource);
} else {
propertySources.addFirst(filePropertySource);
}
}
} catch (IOException e) {
throw new ConfigReloadException("Failed to reload properties from files", e);
}
}
private void reloadLogbackConfiguration() {
try {
// Logback configuration reload
ch.qos.logback.classic.LoggerContext context =
(ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
String configDir = System.getenv().getOrDefault("CONFIG_DIR", "/etc/app/config");
Path logbackConfigPath = Paths.get(configDir, "logback.xml");
if (Files.exists(logbackConfigPath)) {
ch.qos.logback.classic.joran.JoranConfigurator configurator =
new ch.qos.logback.classic.joran.JoranConfigurator();
configurator.setContext(context);
configurator.doConfigure(logbackConfigPath.toFile());
}
logger.info("Logback configuration reloaded from: {}", logbackConfigPath);
} catch (Exception e) {
logger.error("Failed to reload Logback configuration", e);
}
}
private void updateSecretProperties() {
// Update Spring environment with reloaded secrets
// This would depend on your specific secret management approach
}
public long getReloadCount() {
return reloadCount.get();
}
}
class ConfigReloadException extends RuntimeException {
public ConfigReloadException(String message, Throwable cause) {
super(message, cause);
}
}
Part 3: Kubernetes API-Based Reloading
3.1 Kubernetes Client Integration
Dependencies:
<dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java</artifactId> <version>18.0.0</version> </dependency> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java-api-fluent</artifactId> <version>18.0.0</version> </dependency>
3.2 Kubernetes Config Watcher
// File: src/main/java/com/example/config/KubernetesConfigWatcher.java
package com.example.config;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.util.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class KubernetesConfigWatcher {
private static final Logger logger = LoggerFactory.getLogger(KubernetesConfigWatcher.class);
private final ConfigReloadService configReloadService;
private final SecretManager secretManager;
private CoreV1Api coreV1Api;
private ScheduledExecutorService watchExecutor;
@Value("${app.configmap.name:java-app-config}")
private String configMapName;
@Value("${app.secret.name:java-app-secrets}")
private String secretName;
@Value("${app.namespace:default}")
private String namespace;
private final AtomicReference<String> lastConfigMapResourceVersion = new AtomicReference<>();
private final AtomicReference<String> lastSecretResourceVersion = new AtomicReference<>();
public KubernetesConfigWatcher(ConfigReloadService configReloadService,
SecretManager secretManager) {
this.configReloadService = configReloadService;
this.secretManager = secretManager;
}
@PostConstruct
public void initialize() {
try {
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
coreV1Api = new CoreV1Api(client);
// Get initial resource versions
initializeResourceVersions();
// Start watching for changes
startWatching();
logger.info("Kubernetes config watcher initialized for ConfigMap: {} and Secret: {} in namespace: {}",
configMapName, secretName, namespace);
} catch (IOException | ApiException e) {
logger.warn("Failed to initialize Kubernetes client, falling back to file watching", e);
}
}
@PreDestroy
public void shutdown() {
if (watchExecutor != null) {
watchExecutor.shutdown();
}
}
private void initializeResourceVersions() throws ApiException {
// Get initial ConfigMap resource version
V1ConfigMap configMap = coreV1Api.readNamespacedConfigMap(configMapName, namespace, null);
lastConfigMapResourceVersion.set(configMap.getMetadata().getResourceVersion());
// Get initial Secret resource version
V1Secret secret = coreV1Api.readNamespacedSecret(secretName, namespace, null);
lastSecretResourceVersion.set(secret.getMetadata().getResourceVersion());
}
private void startWatching() {
watchExecutor = Executors.newSingleThreadScheduledExecutor();
watchExecutor.scheduleWithFixedDelay(this::checkForKubernetesChanges, 30, 30, TimeUnit.SECONDS);
}
private void checkForKubernetesChanges() {
try {
checkConfigMapChanges();
checkSecretChanges();
} catch (Exception e) {
logger.error("Error checking for Kubernetes configuration changes", e);
}
}
private void checkConfigMapChanges() throws ApiException {
V1ConfigMap currentConfigMap = coreV1Api.readNamespacedConfigMap(configMapName, namespace, null);
String currentResourceVersion = currentConfigMap.getMetadata().getResourceVersion();
String lastResourceVersion = lastConfigMapResourceVersion.get();
if (!currentResourceVersion.equals(lastResourceVersion)) {
logger.info("ConfigMap changed detected. Old version: {}, New version: {}",
lastResourceVersion, currentResourceVersion);
lastConfigMapResourceVersion.set(currentResourceVersion);
configReloadService.reloadApplicationConfig();
}
}
private void checkSecretChanges() throws ApiException {
V1Secret currentSecret = coreV1Api.readNamespacedSecret(secretName, namespace, null);
String currentResourceVersion = currentSecret.getMetadata().getResourceVersion();
String lastResourceVersion = lastSecretResourceVersion.get();
if (!currentResourceVersion.equals(lastResourceVersion)) {
logger.info("Secret changed detected. Old version: {}, New version: {}",
lastResourceVersion, currentResourceVersion);
lastSecretResourceVersion.set(currentResourceVersion);
configReloadService.reloadSecrets();
}
}
public String getCurrentConfigMapVersion() {
return lastConfigMapResourceVersion.get();
}
public String getCurrentSecretVersion() {
return lastSecretResourceVersion.get();
}
}
Part 4: Secret Management
4.1 Secret Manager
// File: src/main/java/com/example/config/SecretManager.java
package com.example.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Service
public class SecretManager {
private static final Logger logger = LoggerFactory.getLogger(SecretManager.class);
private final Map<String, String> secrets = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final String secretsDir = System.getenv().getOrDefault("SECRETS_DIR", "/etc/app/secrets");
@PostConstruct
public void initialize() {
reloadSecrets();
}
public void reloadSecrets() {
lock.writeLock().lock();
try {
logger.info("Reloading secrets from directory: {}", secretsDir);
Path secretsPath = Paths.get(secretsDir);
if (!Files.exists(secretsPath)) {
logger.warn("Secrets directory does not exist: {}", secretsDir);
return;
}
Map<String, String> newSecrets = new HashMap<>();
Files.list(secretsPath)
.filter(Files::isRegularFile)
.forEach(file -> {
try {
String secretName = file.getFileName().toString();
byte[] secretBytes = Files.readAllBytes(file);
// Kubernetes secrets are base64 encoded
String secretValue = new String(secretBytes);
if (isBase64(secretValue)) {
secretValue = new String(Base64.getDecoder().decode(secretValue));
}
newSecrets.put(secretName, secretValue);
logger.debug("Loaded secret: {}", secretName);
} catch (IOException e) {
logger.error("Failed to read secret file: {}", file, e);
}
});
secrets.clear();
secrets.putAll(newSecrets);
logger.info("Successfully reloaded {} secrets", secrets.size());
} catch (IOException e) {
logger.error("Failed to reload secrets", e);
} finally {
lock.writeLock().unlock();
}
}
public String getSecret(String secretName) {
lock.readLock().lock();
try {
return secrets.get(secretName);
} finally {
lock.readLock().unlock();
}
}
public String getSecret(String secretName, String defaultValue) {
String value = getSecret(secretName);
return value != null ? value : defaultValue;
}
public byte[] getSecretAsBytes(String secretName) {
String secret = getSecret(secretName);
return secret != null ? secret.getBytes() : null;
}
public boolean hasSecret(String secretName) {
lock.readLock().lock();
try {
return secrets.containsKey(secretName);
} finally {
lock.readLock().unlock();
}
}
public Map<String, String> getAllSecrets() {
lock.readLock().lock();
try {
return new HashMap<>(secrets);
} finally {
lock.readLock().unlock();
}
}
private boolean isBase64(String value) {
try {
Base64.getDecoder().decode(value);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
Part 5: Spring Boot Integration
5.1 Configuration Properties with Reload Support
// File: src/main/java/com/example/config/ReloadableConfigurationProperties.java
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app")
public class ReloadableConfigurationProperties {
@NotBlank
private String name;
private String version = "1.0.0";
@Min(1024)
@Max(65535)
private int port = 8080;
private DatabaseProperties database;
private List<String> features = new ArrayList<>();
private Map<String, String> labels = new HashMap<>();
private CacheProperties cache;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public DatabaseProperties getDatabase() { return database; }
public void setDatabase(DatabaseProperties database) { this.database = database; }
public List<String> getFeatures() { return features; }
public void setFeatures(List<String> features) { this.features = features; }
public Map<String, String> getLabels() { return labels; }
public void setLabels(Map<String, String> labels) { this.labels = labels; }
public CacheProperties getCache() { return cache; }
public void setCache(CacheProperties cache) { this.cache = cache; }
public static class DatabaseProperties {
private String host;
private int port = 5432;
private String name;
private ConnectionPoolProperties pool = new ConnectionPoolProperties();
// Getters and setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public ConnectionPoolProperties getPool() { return pool; }
public void setPool(ConnectionPoolProperties pool) { this.pool = pool; }
}
public static class ConnectionPoolProperties {
@Min(1)
private int minSize = 2;
@Min(1)
private int maxSize = 10;
@Min(1)
private int timeout = 30;
// Getters and setters
public int getMinSize() { return minSize; }
public void setMinSize(int minSize) { this.minSize = minSize; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}
public static class CacheProperties {
private boolean enabled = true;
private int ttl = 3600;
private int maxSize = 1000;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getTtl() { return ttl; }
public void setTtl(int ttl) { this.ttl = ttl; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
}
}
5.2 Actuator Endpoints for Config Management
// File: src/main/java/com/example/actuator/ConfigReloadEndpoint.java
package com.example.actuator;
import com.example.config.ConfigReloadService;
import com.example.config.KubernetesConfigWatcher;
import com.example.config.SecretManager;
import org.springframework.boot.actuate.endpoint.annotation.*;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Endpoint(id = "configreload")
public class ConfigReloadEndpoint {
private final ConfigReloadService configReloadService;
private final KubernetesConfigWatcher kubernetesConfigWatcher;
private final SecretManager secretManager;
public ConfigReloadEndpoint(ConfigReloadService configReloadService,
KubernetesConfigWatcher kubernetesConfigWatcher,
SecretManager secretManager) {
this.configReloadService = configReloadService;
this.kubernetesConfigWatcher = kubernetesConfigWatcher;
this.secretManager = secretManager;
}
@ReadOperation
public Map<String, Object> getConfigInfo() {
Map<String, Object> info = new HashMap<>();
info.put("reloadCount", configReloadService.getReloadCount());
info.put("configMapVersion", kubernetesConfigWatcher.getCurrentConfigMapVersion());
info.put("secretVersion", kubernetesConfigWatcher.getCurrentSecretVersion());
info.put("secretsCount", secretManager.getAllSecrets().size());
return info;
}
@WriteOperation
public Map<String, String> reloadApplicationConfig() {
configReloadService.reloadApplicationConfig();
return Map.of("status", "success", "message", "Application configuration reloaded");
}
@WriteOperation
public Map<String, String> reloadSecrets() {
configReloadService.reloadSecrets();
return Map.of("status", "success", "message", "Secrets reloaded");
}
@WriteOperation
public Map<String, String> reloadLoggingConfig() {
configReloadService.reloadLoggingConfig();
return Map.of("status", "success", "message", "Logging configuration reloaded");
}
@DeleteOperation
public Map<String, String> clearSecretCache() {
// This would clear any in-memory secret cache if implemented
return Map.of("status", "success", "message", "Secret cache cleared");
}
}
Part 6: Testing Config Reloading
6.1 Unit Tests
// File: src/test/java/com/example/config/ConfigReloadServiceTest.java
package com.example.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.cloud.context.refresh.ContextRefresher;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class ConfigReloadServiceTest {
@TempDir
Path tempDir;
private ConfigReloadService configReloadService;
private ContextRefresher contextRefresher;
private SecretManager secretManager;
@BeforeEach
void setUp() {
contextRefresher = mock(ContextRefresher.class);
secretManager = mock(SecretManager.class);
configReloadService = new ConfigReloadService(contextRefresher, null, secretManager);
}
@Test
void shouldIncrementReloadCount() {
// Given
long initialCount = configReloadService.getReloadCount();
// When
configReloadService.reloadApplicationConfig();
// Then
assertEquals(initialCount + 1, configReloadService.getReloadCount());
verify(contextRefresher, times(1)).refresh();
}
@Test
void shouldReloadSecrets() {
// When
configReloadService.reloadSecrets();
// Then
verify(secretManager, times(1)).reloadSecrets();
}
}
// File: src/test/java/com/example/config/SecretManagerTest.java
package com.example.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
class SecretManagerTest {
@TempDir
Path tempDir;
private SecretManager secretManager;
@BeforeEach
void setUp() throws IOException {
// Create test secret files
Path secret1 = tempDir.resolve("database-password");
Files.writeString(secret1, Base64.getEncoder().encodeToString("secret123".getBytes()));
Path secret2 = tempDir.resolve("api-key");
Files.writeString(secret2, "plain-text-secret");
// Set environment variable for test
System.setProperty("SECRETS_DIR", tempDir.toString());
secretManager = new SecretManager();
secretManager.initialize();
}
@Test
void shouldLoadSecretsFromDirectory() {
// When
String dbPassword = secretManager.getSecret("database-password");
String apiKey = secretManager.getSecret("api-key");
// Then
assertEquals("secret123", dbPassword);
assertEquals("plain-text-secret", apiKey);
assertTrue(secretManager.hasSecret("database-password"));
assertFalse(secretManager.hasSecret("non-existent-secret"));
}
@Test
void shouldReturnDefaultForMissingSecret() {
// When
String value = secretManager.getSecret("non-existent", "default-value");
// Then
assertEquals("default-value", value);
}
}
Best Practices for ConfigMap and Secret Reloading
- Graceful Degradation: Implement fallback mechanisms when Kubernetes API is unavailable
- Resource Version Tracking: Always track resource versions to avoid unnecessary reloads
- Atomic Updates: Ensure configuration changes are applied atomically
- Health Checks: Update readiness probes during configuration reloads
- Monitoring: Track reload events and failures in your monitoring system
- Security: Ensure proper RBAC permissions for Kubernetes API access
- Testing: Test configuration reloading in development and staging environments
- Documentation: Document the reload behavior and expected downtime (if any)
Conclusion
Implementing ConfigMap and Secret reloading in Java applications running on Kubernetes provides significant operational benefits by enabling dynamic configuration updates without service restarts. The strategies covered in this guide include:
- File-based watching for simple volume-mounted configurations
- Kubernetes API integration for real-time change detection
- Spring Cloud Context for dynamic bean refresh
- Comprehensive secret management with secure reloading capabilities
By combining these approaches, Java applications can achieve true zero-downtime configuration updates, improved security through secret rotation, and greater operational flexibility in Kubernetes environments. The implementation should be tailored to your specific security requirements, performance needs, and operational constraints.
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.