Introduction
In Kubernetes, ConfigMaps and Secrets are used to manage configuration data and sensitive information. However, when these resources are updated, applications don't automatically reload the new configuration. This guide covers multiple strategies to implement ConfigMap and Secret reloading in Java applications running in Kubernetes.
Architecture Overview
[Kubernetes] → [ConfigMap/Secret] → [File System] → [File Watcher] → [Application] → [Configuration Reload] ↓ ↓ ↓ ↓ ↓ ↓ kubectl Volume Mount Config Files Watch Service Spring Beans Dynamic Updates Operator Environment Vars Symlinks Polling Configuration Bean Refresh GitOps External Secrets Sidecar Inotify Properties Cache Clear
Step 1: Project Dependencies
Maven Configuration
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>config-reload</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.version>3.2.0</spring-boot.version> <kubernetes-client.version>6.8.1</kubernetes-client.version> <micrometer.version>1.12.0</micrometer.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Configuration Processor --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- Kubernetes Client --> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java</artifactId> <version>${kubernetes-client.version}</version> </dependency> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java-api-fluent</artifactId> <version>${kubernetes-client.version}</version> </dependency> <!-- Metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>${micrometer.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>${micrometer.version}</version> </dependency> <!-- JSON --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.0</version> </dependency> <!-- YAML --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> <version>2.15.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> </plugin> </plugins> </build> </project> Step 2: Kubernetes Manifests
ConfigMap and Secret Definitions
k8s/configmap.yaml
apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: default labels: app: java-app data: application.yaml: | app: name: "Config Reload Demo" version: "1.0.0" database: url: "jdbc:postgresql://localhost:5432/mydb" pool: min-size: 2 max-size: 10 timeout: 30000 features: dark-mode: true experimental-features: false rate-limiting: enabled: true requests-per-minute: 100 logging: level: com.example: INFO org.springframework: WARN feature-flags.yaml: | features: new-ui: enabled: false rollout-percentage: 10 payment-v2: enabled: true rollout-percentage: 50 search-v3: enabled: false rollout-percentage: 0
k8s/secret.yaml
apiVersion: v1 kind: Secret metadata: name: app-secrets namespace: default labels: app: java-app type: Opaque data: database-password: c3VwZXJzZWNyZXRwYXNzd29yZA== # base64 encoded api-key: YXBpLWtleS1zZWNyZXQ= jwt-secret: anN0LXNlY3JldC1rZXk=
Deployment with Volume Mounts
k8s/deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: java-app namespace: default labels: app: java-app spec: replicas: 2 selector: matchLabels: app: java-app template: metadata: labels: app: java-app annotations: # Trigger redeploy when config changes config.hash: "${CONFIG_HASH}" secret.hash: "${SECRET_HASH}" spec: containers: - name: java-app image: myapp:latest ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "kubernetes" - name: CONFIG_RELOAD_ENABLED value: "true" - name: CONFIG_RELOAD_INTERVAL value: "30000" # Mount ConfigMap and Secret as volumes volumeMounts: - name: config-volume mountPath: /etc/app/config readOnly: true - name: secret-volume mountPath: /etc/app/secrets readOnly: true - name: temp-volume mountPath: /tmp # Resource limits resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" # Liveness and readiness probes livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 15 periodSeconds: 5 timeoutSeconds: 3 # Security context securityContext: runAsNonRoot: true runAsUser: 1000 allowPrivilegeEscalation: false capabilities: drop: - ALL # Volumes from ConfigMap and Secret volumes: - name: config-volume configMap: name: app-config items: - key: application.yaml path: application.yaml - key: feature-flags.yaml path: feature-flags.yaml - name: secret-volume secret: secretName: app-secrets items: - key: database-password path: database-password - key: api-key path: api-key - key: jwt-secret path: jwt-secret - name: temp-volume emptyDir: {} # Security context securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 Step 3: Configuration Models
Application Configuration
AppConfig.java
package com.example.configreload.model; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.util.Map; @ConfigurationProperties(prefix = "app") @Validated public class AppConfig { @NotBlank private String name; @NotBlank private String version; @NotNull private DatabaseConfig database; @NotNull private FeaturesConfig features; @NotNull private LoggingConfig logging; // 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 DatabaseConfig getDatabase() { return database; } public void setDatabase(DatabaseConfig database) { this.database = database; } public FeaturesConfig getFeatures() { return features; } public void setFeatures(FeaturesConfig features) { this.features = features; } public LoggingConfig getLogging() { return logging; } public void setLogging(LoggingConfig logging) { this.logging = logging; } public static class DatabaseConfig { @NotBlank private String url; @Positive private int minSize; @Positive private int maxSize; @Positive private long timeout; // Getters and Setters public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } 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 long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } } public static class FeaturesConfig { private boolean darkMode; private boolean experimentalFeatures; @NotNull private RateLimitingConfig rateLimiting; // Getters and Setters public boolean isDarkMode() { return darkMode; } public void setDarkMode(boolean darkMode) { this.darkMode = darkMode; } public boolean isExperimentalFeatures() { return experimentalFeatures; } public void setExperimentalFeatures(boolean experimentalFeatures) { this.experimentalFeatures = experimentalFeatures; } public RateLimitingConfig getRateLimiting() { return rateLimiting; } public void setRateLimiting(RateLimitingConfig rateLimiting) { this.rateLimiting = rateLimiting; } public static class RateLimitingConfig { private boolean enabled; @Positive private int requestsPerMinute; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getRequestsPerMinute() { return requestsPerMinute; } public void setRequestsPerMinute(int requestsPerMinute) { this.requestsPerMinute = requestsPerMinute; } } } public static class LoggingConfig { @NotNull private Map<String, String> level; // Getters and Setters public Map<String, String> getLevel() { return level; } public void setLevel(Map<String, String> level) { this.level = level; } } } Feature Flags Configuration
FeatureFlags.java
package com.example.configreload.model; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.util.Map; @ConfigurationProperties(prefix = "features") @Validated public class FeatureFlags { private Map<String, FeatureFlag> features; // Getters and Setters public Map<String, FeatureFlag> getFeatures() { return features; } public void setFeatures(Map<String, FeatureFlag> features) { this.features = features; } public static class FeatureFlag { private boolean enabled; private int rolloutPercentage; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getRolloutPercentage() { return rolloutPercentage; } public void setRolloutPercentage(int rolloutPercentage) { this.rolloutPercentage = rolloutPercentage; } } } Secrets Configuration
AppSecrets.java
package com.example.configreload.model; import jakarta.validation.constraints.NotBlank; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @ConfigurationProperties(prefix = "secrets") @Validated public class AppSecrets { @NotBlank private String databasePassword; @NotBlank private String apiKey; @NotBlank private String jwtSecret; // Getters and Setters public String getDatabasePassword() { return databasePassword; } public void setDatabasePassword(String databasePassword) { this.databasePassword = databasePassword; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getJwtSecret() { return jwtSecret; } public void setJwtSecret(String jwtSecret) { this.jwtSecret = jwtSecret; } } Step 4: File System Watcher Strategy
File System Configuration Watcher
FileSystemConfigWatcher.java
package com.example.configreload.watcher; import com.example.configreload.event.ConfigReloadEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.File; import java.io.IOException; import java.nio.file.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Component public class FileSystemConfigWatcher { private static final Logger logger = LoggerFactory.getLogger(FileSystemConfigWatcher.class); private final ApplicationEventPublisher eventPublisher; private final ScheduledExecutorService scheduler; private WatchService watchService; private boolean watching = false; @Value("${app.config.path:/etc/app/config}") private String configPath; @Value("${app.secrets.path:/etc/app/secrets}") private String secretsPath; @Value("${app.config.reload.enabled:true}") private boolean reloadEnabled; @Value("${app.config.reload.interval:30000}") private long reloadInterval; public FileSystemConfigWatcher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "config-watcher"); t.setDaemon(true); return t; }); } @PostConstruct public void init() { if (!reloadEnabled) { logger.info("Config reload is disabled"); return; } try { startWatching(); startPolling(); logger.info("Started config and secret watcher"); } catch (IOException e) { logger.error("Failed to start config watcher", e); } } @PreDestroy public void destroy() { watching = false; scheduler.shutdown(); if (watchService != null) { try { watchService.close(); } catch (IOException e) { logger.error("Error closing watch service", e); } } } private void startWatching() throws IOException { watchService = FileSystems.getDefault().newWatchService(); Path configDir = Path.of(configPath); Path secretsDir = Path.of(secretsPath); if (Files.exists(configDir)) { configDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); logger.info("Watching config directory: {}", configDir); } if (Files.exists(secretsDir)) { secretsDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); logger.info("Watching secrets directory: {}", secretsDir); } watching = true; // Start watch service in background thread scheduler.execute(this::watchFiles); } private void startPolling() { // Fallback polling mechanism scheduler.scheduleAtFixedRate(this::checkForChanges, reloadInterval, reloadInterval, TimeUnit.MILLISECONDS); } private void watchFiles() { while (watching) { try { WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { continue; } @SuppressWarnings("unchecked") WatchEvent<Path> ev = (WatchEvent<Path>) event; Path filename = ev.context(); logger.info("File change detected: {} - {}", kind.name(), filename); // Determine what type of file changed if (filename.toString().endsWith(".yaml") || filename.toString().endsWith(".yml")) { publishReloadEvent("CONFIG", filename.toString()); } else { publishReloadEvent("SECRET", filename.toString()); } } boolean valid = key.reset(); if (!valid) { logger.warn("Watch key no longer valid"); break; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.info("Config watcher interrupted"); break; } catch (ClosedWatchServiceException e) { logger.info("Watch service closed"); break; } catch (Exception e) { logger.error("Error in file watcher", e); } } } private void checkForChanges() { try { // Check config files checkDirectoryForChanges(configPath, "CONFIG"); // Check secret files checkDirectoryForChanges(secretsPath, "SECRET"); } catch (Exception e) { logger.error("Error checking for config changes", e); } } private void checkDirectoryForChanges(String directoryPath, String type) { File dir = new File(directoryPath); if (!dir.exists() || !dir.isDirectory()) { return; } File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { long lastModified = file.lastModified(); Long previousModified = FileModificationCache.getLastModified(file.getAbsolutePath()); if (previousModified == null || lastModified > previousModified) { logger.info("{} file changed: {}", type, file.getName()); FileModificationCache.updateLastModified(file.getAbsolutePath(), lastModified); publishReloadEvent(type, file.getName()); } } } private void publishReloadEvent(String configType, String fileName) { ConfigReloadEvent event = new ConfigReloadEvent(this, configType, fileName); eventPublisher.publishEvent(event); } // Cache for file modification times private static class FileModificationCache { private static final java.util.Map<String, Long> cache = new java.util.concurrent.ConcurrentHashMap<>(); public static Long getLastModified(String filePath) { return cache.get(filePath); } public static void updateLastModified(String filePath, long lastModified) { cache.put(filePath, lastModified); } public static void remove(String filePath) { cache.remove(filePath); } } } Configuration Reload Event
ConfigReloadEvent.java
package com.example.configreload.event; import org.springframework.context.ApplicationEvent; public class ConfigReloadEvent extends ApplicationEvent { private final String configType; // CONFIG or SECRET private final String fileName; private final long timestamp; public ConfigReloadEvent(Object source, String configType, String fileName) { super(source); this.configType = configType; this.fileName = fileName; this.timestamp = System.currentTimeMillis(); } // Getters public String getConfigType() { return configType; } public String getFileName() { return fileName; } public long getTimestamp() { return timestamp; } public boolean isConfigChange() { return "CONFIG".equals(configType); } public boolean isSecretChange() { return "SECRET".equals(configType); } } Step 5: Kubernetes API Watcher Strategy
Kubernetes ConfigMap Watcher
KubernetesConfigWatcher.java
package com.example.configreload.watcher; import com.example.configreload.event.ConfigReloadEvent; 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.V1ConfigMapList; import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.util.Config; import io.kubernetes.client.util.Watch; import okhttp3.OkHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; 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; @Component public class KubernetesConfigWatcher { private static final Logger logger = LoggerFactory.getLogger(KubernetesConfigWatcher.class); private final ApplicationEventPublisher eventPublisher; private final ScheduledExecutorService scheduler; private boolean watching = false; @Value("${app.configmap.name:app-config}") private String configMapName; @Value("${app.secret.name:app-secrets}") private String secretName; @Value("${app.namespace:default}") private String namespace; @Value("${app.config.reload.enabled:true}") private boolean reloadEnabled; @Value("${kubernetes.watch.enabled:true}") private boolean kubernetesWatchEnabled; private CoreV1Api api; public KubernetesConfigWatcher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; this.scheduler = Executors.newScheduledThreadPool(2, r -> { Thread t = new Thread(r, "k8s-watcher"); t.setDaemon(true); return t; }); } @PostConstruct public void init() { if (!reloadEnabled || !kubernetesWatchEnabled) { logger.info("Kubernetes config watcher is disabled"); return; } try { initializeKubernetesClient(); startWatching(); logger.info("Started Kubernetes config and secret watcher"); } catch (Exception e) { logger.error("Failed to initialize Kubernetes watcher", e); // Fall back to file system watching } } @PreDestroy public void destroy() { watching = false; scheduler.shutdown(); } private void initializeKubernetesClient() throws IOException { // Try to load from cluster first, then fall back to default try { ApiClient client = Config.fromCluster(); Configuration.setDefaultApiClient(client); } catch (Exception e) { logger.warn("Cannot load in-cluster config, falling back to default"); ApiClient client = Config.defaultClient(); Configuration.setDefaultApiClient(client); } // Configure timeout OkHttpClient httpClient = Configuration.getDefaultApiClient().getHttpClient() .newBuilder() .readTimeout(0, TimeUnit.SECONDS) // No timeout for watch .build(); Configuration.getDefaultApiClient().setHttpClient(httpClient); this.api = new CoreV1Api(); } private void startWatching() { watching = true; // Watch ConfigMap in separate thread scheduler.execute(this::watchConfigMap); // Watch Secret in separate thread scheduler.execute(this::watchSecret); } private void watchConfigMap() { while (watching) { try { logger.info("Starting ConfigMap watch: {}/{}", namespace, configMapName); Watch<V1ConfigMap> watch = Watch.createWatch( Configuration.getDefaultApiClient(), api.listNamespacedConfigMapCall( namespace, null, // pretty false, // allowWatchBookmarks null, // _continue null, // fieldSelector "metadata.name=" + configMapName, // labelSelector null, // limit null, // resourceVersion null, // resourceVersionMatch null, // timeoutSeconds true, // watch null // callback ), new Watch.ResponseType<V1ConfigMap>() {} ); for (Watch.Response<V1ConfigMap> item : watch) { if (!watching) break; V1ConfigMap configMap = item.object; String type = item.type; logger.info("ConfigMap {}: {}", type, configMap.getMetadata().getName()); if ("MODIFIED".equals(type)) { publishReloadEvent("CONFIG", "ConfigMap modified"); } } } catch (ApiException e) { logger.error("Kubernetes API error watching ConfigMap", e); sleepBeforeRetry(); } catch (Exception e) { if (watching) { logger.error("Error watching ConfigMap", e); sleepBeforeRetry(); } } } } private void watchSecret() { while (watching) { try { logger.info("Starting Secret watch: {}/{}", namespace, secretName); Watch<V1Secret> watch = Watch.createWatch( Configuration.getDefaultApiClient(), api.listNamespacedSecretCall( namespace, null, // pretty false, // allowWatchBookmarks null, // _continue null, // fieldSelector "metadata.name=" + secretName, // labelSelector null, // limit null, // resourceVersion null, // resourceVersionMatch null, // timeoutSeconds true, // watch null // callback ), new Watch.ResponseType<V1Secret>() {} ); for (Watch.Response<V1Secret> item : watch) { if (!watching) break; V1Secret secret = item.object; String type = item.type; logger.info("Secret {}: {}", type, secret.getMetadata().getName()); if ("MODIFIED".equals(type)) { publishReloadEvent("SECRET", "Secret modified"); } } } catch (ApiException e) { logger.error("Kubernetes API error watching Secret", e); sleepBeforeRetry(); } catch (Exception e) { if (watching) { logger.error("Error watching Secret", e); sleepBeforeRetry(); } } } } private void sleepBeforeRetry() { try { Thread.sleep(30000); // Wait 30 seconds before retry } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void publishReloadEvent(String configType, String message) { ConfigReloadEvent event = new ConfigReloadEvent(this, configType, message); eventPublisher.publishEvent(event); } } Step 6: Configuration Reload Service
Configuration Reload Service
ConfigReloadService.java
package com.example.configreload.service; import com.example.configreload.event.ConfigReloadEvent; import com.example.configreload.model.AppConfig; import com.example.configreload.model.AppSecrets; import com.example.configreload.model.FeatureFlags; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.lang.reflect.Field; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @Service public class ConfigReloadService { private static final Logger logger = LoggerFactory.getLogger(ConfigReloadService.class); private final ApplicationContext applicationContext; private final Environment environment; private final MeterRegistry meterRegistry; private final AtomicInteger configReloadCount = new AtomicInteger(0); private final AtomicInteger secretReloadCount = new AtomicInteger(0); private final Map<String, Long> lastReloadTime = new ConcurrentHashMap<>(); @Autowired @Qualifier("appConfig") private AppConfig appConfig; @Autowired(required = false) @Qualifier("featureFlags") private FeatureFlags featureFlags; @Autowired(required = false) @Qualifier("appSecrets") private AppSecrets appSecrets; public ConfigReloadService(ApplicationContext applicationContext, Environment environment, MeterRegistry meterRegistry) { this.applicationContext = applicationContext; this.environment = environment; this.meterRegistry = meterRegistry; // Initialize metrics initializeMetrics(); } @Async @EventListener public void handleConfigReload(ConfigReloadEvent event) { String configType = event.getConfigType(); String fileName = event.getFileName(); logger.info("Processing {} reload event for: {}", configType, fileName); try { switch (configType) { case "CONFIG": reloadConfiguration(); break; case "SECRET": reloadSecrets(); break; default: logger.warn("Unknown config type: {}", configType); } // Update metrics updateReloadMetrics(configType, true); } catch (Exception e) { logger.error("Failed to reload {}: {}", configType, fileName, e); updateReloadMetrics(configType, false); } } private void reloadConfiguration() { try { // Reload AppConfig if (appConfig != null) { AppConfig newConfig = createNewAppConfig(); copyProperties(newConfig, appConfig); logger.info("AppConfig reloaded successfully"); } // Reload FeatureFlags if (featureFlags != null) { FeatureFlags newFlags = createNewFeatureFlags(); copyProperties(newFlags, featureFlags); logger.info("FeatureFlags reloaded successfully"); } // Refresh Spring beans that use @ConfigurationProperties refreshConfigurationPropertiesBeans(); configReloadCount.incrementAndGet(); lastReloadTime.put("CONFIG", System.currentTimeMillis()); logger.info("Configuration reload completed successfully"); } catch (Exception e) { logger.error("Failed to reload configuration", e); throw new RuntimeException("Configuration reload failed", e); } } private void reloadSecrets() { try { if (appSecrets != null) { AppSecrets newSecrets = createNewAppSecrets(); copyProperties(newSecrets, appSecrets); logger.info("Secrets reloaded successfully"); } secretReloadCount.incrementAndGet(); lastReloadTime.put("SECRET", System.currentTimeMillis()); logger.info("Secrets reload completed successfully"); } catch (Exception e) { logger.error("Failed to reload secrets", e); throw new RuntimeException("Secrets reload failed", e); } } private AppConfig createNewAppConfig() { // This would typically re-read from the file system // For simplicity, we're recreating from environment AppConfig config = new AppConfig(); // In a real implementation, you would parse the YAML file again return config; } private FeatureFlags createNewFeatureFlags() { FeatureFlags flags = new FeatureFlags(); // Re-parse feature flags YAML return flags; } private AppSecrets createNewAppSecrets() { AppSecrets secrets = new AppSecrets(); // Re-read secrets from file system return secrets; } private void copyProperties(Object source, Object target) throws Exception { Field[] fields = source.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Object value = field.get(source); if (value != null) { field.set(target, value); } } } private void refreshConfigurationPropertiesBeans() { String[] beanNames = applicationContext.getBeanNamesForAnnotation(ConfigurationProperties.class); for (String beanName : beanNames) { Object bean = applicationContext.getBean(beanName); try { // Trigger re-initialization applicationContext.getAutowireCapableBeanFactory().autowireBean(bean); logger.debug("Refreshed configuration properties bean: {}", beanName); } catch (Exception e) { logger.warn("Failed to refresh bean: {}", beanName, e); } } } private void initializeMetrics() { meterRegistry.gauge("config.reload.count", configReloadCount); meterRegistry.gauge("secret.reload.count", secretReloadCount); } private void updateReloadMetrics(String configType, boolean success) { String status = success ? "success" : "failure"; meterRegistry.counter("config.reloads", "type", configType.toLowerCase(), "status", status ).increment(); } // Public methods for monitoring public int getConfigReloadCount() { return configReloadCount.get(); } public int getSecretReloadCount() { return secretReloadCount.get(); } public Long getLastReloadTime(String configType) { return lastReloadTime.get(configType); } public Map<String, Object> getReloadStats() { return Map.of( "configReloadCount", configReloadCount.get(), "secretReloadCount", secretReloadCount.get(), "lastConfigReload", lastReloadTime.get("CONFIG"), "lastSecretReload", lastReloadTime.get("SECRET") ); } } Step 7: Spring Configuration
Configuration Beans
AppConfiguration.java
package com.example.configreload.config; import com.example.configreload.model.AppConfig; import com.example.configreload.model.AppSecrets; import com.example.configreload.model.FeatureFlags; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @Configuration @EnableAsync public class AppConfiguration { @Bean @ConfigurationProperties(prefix = "app") public AppConfig appConfig() { return new AppConfig(); } @Bean @ConfigurationProperties(prefix = "features") public FeatureFlags featureFlags() { return new FeatureFlags(); } @Bean @ConfigurationProperties(prefix = "secrets") public AppSecrets appSecrets() { return new AppSecrets(); } @Bean public String databasePassword() throws IOException { // Read password from secret file Path secretPath = Paths.get("/etc/app/secrets/database-password"); if (Files.exists(secretPath)) { return Files.readString(secretPath).trim(); } return "default-password"; // Fallback for local development } @Bean public String apiKey() throws IOException { // Read API key from secret file Path secretPath = Paths.get("/etc/app/secrets/api-key"); if (Files.exists(secretPath)) { return Files.readString(secretPath).trim(); } return "default-api-key"; // Fallback for local development } @Bean public String jwtSecret() throws IOException { // Read JWT secret from secret file Path secretPath = Paths.get("/etc/app/secrets/jwt-secret"); if (Files.exists(secretPath)) { return Files.readString(secretPath).trim(); } return "default-jwt-secret"; // Fallback for local development } } Application Properties
application.yaml
# Application configuration app: name: "Config Reload Demo" version: "1.0.0" config: reload: enabled: true interval: 30000 # Spring configuration spring: application: name: config-reload-demo config: import: - file:/etc/app/config/application.yaml - file:/etc/app/config/feature-flags.yaml profiles: active: kubernetes # Actuator endpoints management: endpoints: web: exposure: include: health,info,metrics,configprops,env,reload endpoint: health: show-details: always show-components: always reload: enabled: true info: env: enabled: true # Logging logging: level: com.example.configreload: INFO org.springframework: WARN pattern: level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" # Kubernetes kubernetes: watch: enabled: true Step 8: REST API for Configuration Management
Configuration Controller
ConfigController.java
package com.example.configreload.controller; import com.example.configreload.model.AppConfig; import com.example.configreload.model.AppSecrets; import com.example.configreload.model.FeatureFlags; import com.example.configreload.service.ConfigReloadService; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/v1/config") public class ConfigController { private final ApplicationContext applicationContext; private final ConfigReloadService configReloadService; private final AppConfig appConfig; private final FeatureFlags featureFlags; public ConfigController(ApplicationContext applicationContext, ConfigReloadService configReloadService, AppConfig appConfig, FeatureFlags featureFlags) { this.applicationContext = applicationContext; this.configReloadService = configReloadService; this.appConfig = appConfig; this.featureFlags = featureFlags; } @GetMapping("/current") public Map<String, Object> getCurrentConfig() { return Map.of( "appConfig", appConfig, "featureFlags", featureFlags ); } @GetMapping("/reload/stats") public Map<String, Object> getReloadStats() { return configReloadService.getReloadStats(); } @PostMapping("/reload") public Map<String, String> triggerReload(@RequestParam(defaultValue = "CONFIG") String type) { try { switch (type.toUpperCase()) { case "CONFIG": configReloadService.reloadConfiguration(); break; case "SECRET": configReloadService.reloadSecrets(); break; default: return Map.of("status", "error", "message", "Invalid type: " + type); } return Map.of("status", "success", "message", type + " reload triggered"); } catch (Exception e) { return Map.of("status", "error", "message", e.getMessage()); } } @GetMapping("/beans") public Map<String, String> getConfigurationBeans() { String[] beanNames = applicationContext.getBeanNamesForAnnotation(ConfigurationProperties.class); return Map.of( "configurationPropertiesBeans", String.join(", ", beanNames) ); } } Actuator Custom Endpoint
ReloadEndpoint.java
package com.example.configreload.actuator; import com.example.configreload.service.ConfigReloadService; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.stereotype.Component; import java.util.Map; @Component @Endpoint(id = "reload") public class ReloadEndpoint { private final ConfigReloadService configReloadService; public ReloadEndpoint(ConfigReloadService configReloadService) { this.configReloadService = configReloadService; } @ReadOperation public Map<String, Object> reloadInfo() { return configReloadService.getReloadStats(); } @WriteOperation public Map<String, String> triggerReload(String type) { try { if ("config".equalsIgnoreCase(type)) { configReloadService.reloadConfiguration(); return Map.of("status", "success", "message", "Configuration reloaded"); } else if ("secret".equalsIgnoreCase(type)) { configReloadService.reloadSecrets(); return Map.of("status", "success", "message", "Secrets reloaded"); } else { return Map.of("status", "error", "message", "Invalid type. Use 'config' or 'secret'"); } } catch (Exception e) { return Map.of("status", "error", "message", e.getMessage()); } } } Step 9: Usage Examples
Database Service with Config Reload
DatabaseService.java
package com.example.configreload.service; import com.example.configreload.model.AppConfig; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import javax.sql.DataSource; @Service public class DatabaseService { private static final Logger logger = LoggerFactory.getLogger(DatabaseService.class); private final AppConfig appConfig; private final String databasePassword; private volatile HikariDataSource dataSource; public DatabaseService(@Qualifier("appConfig") AppConfig appConfig, @Qualifier("databasePassword") String databasePassword) { this.appConfig = appConfig; this.databasePassword = databasePassword; this.dataSource = createDataSource(); } public DataSource getDataSource() { return dataSource; } @Async @EventListener public void handleConfigReload(com.example.configreload.event.ConfigReloadEvent event) { if (event.isConfigChange() || event.isSecretChange()) { logger.info("Recreating database connection pool due to config change"); recreateDataSource(); } } private synchronized void recreateDataSource() { if (dataSource != null && !dataSource.isClosed()) { dataSource.close(); } dataSource = createDataSource(); logger.info("Database connection pool recreated"); } private HikariDataSource createDataSource() { var dbConfig = appConfig.getDatabase(); HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(dbConfig.getUrl()); hikariConfig.setUsername("app_user"); // From config hikariConfig.setPassword(databasePassword); // From secret hikariConfig.setDriverClassName("org.postgresql.Driver"); // Connection pool configuration hikariConfig.setMinimumIdle(dbConfig.getMinSize()); hikariConfig.setMaximumPoolSize(dbConfig.getMaxSize()); hikariConfig.setConnectionTimeout(dbConfig.getTimeout()); hikariConfig.setIdleTimeout(600000); hikariConfig.setPoolName("AppConnectionPool"); logger.info("Creating database connection pool for: {}", dbConfig.getUrl()); return new HikariDataSource(hikariConfig); } } Feature Flag Service
FeatureFlagService.java
package com.example.configreload.service; import com.example.configreload.model.FeatureFlags; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @Service public class FeatureFlagService { private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class); private final FeatureFlags featureFlags; public FeatureFlagService(@Qualifier("featureFlags") FeatureFlags featureFlags) { this.featureFlags = featureFlags; } public boolean isFeatureEnabled(String featureName) { return Optional.ofNullable(featureFlags.getFeatures().get(featureName)) .map(FeatureFlags.FeatureFlag::isEnabled) .orElse(false); } public boolean isFeatureEnabledForUser(String featureName, String userId) { var feature = featureFlags.getFeatures().get(featureName); if (feature == null || !feature.isEnabled()) { return false; } // Check rollout percentage if (feature.getRolloutPercentage() < 100) { double random = ThreadLocalRandom.current().nextDouble(100.0); return random < feature.getRolloutPercentage(); } return true; } @Async @EventListener public void handleConfigReload(com.example.configreload.event.ConfigReloadEvent event) { if (event.isConfigChange()) { logger.info("Feature flags updated. New flags: {}", featureFlags.getFeatures().keySet()); } } public FeatureFlags getFeatureFlags() { return featureFlags; } } Step 10: Testing
Unit Tests
ConfigReloadServiceTest.java
package com.example.configreload.service; import com.example.configreload.event.ConfigReloadEvent; import com.example.configreload.model.AppConfig; import com.example.configreload.model.FeatureFlags; 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 org.springframework.context.ApplicationContext; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class ConfigReloadServiceTest { @Mock private ApplicationContext applicationContext; @Mock private org.springframework.core.env.Environment environment; @Mock private io.micrometer.core.instrument.MeterRegistry meterRegistry; private ConfigReloadService configReloadService; private AppConfig appConfig; private FeatureFlags featureFlags; @BeforeEach void setUp() { appConfig = new AppConfig(); featureFlags = new FeatureFlags(); configReloadService = new ConfigReloadService( applicationContext, environment, meterRegistry); // Use reflection to set the fields for testing setField(configReloadService, "appConfig", appConfig); setField(configReloadService, "featureFlags", featureFlags); } @Test void testHandleConfigReload() { ConfigReloadEvent event = new ConfigReloadEvent(this, "CONFIG", "test.yaml"); assertDoesNotThrow(() -> configReloadService.handleConfigReload(event)); assertEquals(1, configReloadService.getConfigReloadCount()); } @Test void testGetReloadStats() { var stats = configReloadService.getReloadStats(); assertNotNull(stats); assertTrue(stats.containsKey("configReloadCount")); assertTrue(stats.containsKey("secretReloadCount")); } private void setField(Object target, String fieldName, Object value) { try { var field = target.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(target, value); } catch (Exception e) { throw new RuntimeException(e); } } } Integration Test
ConfigReloadIntegrationTest.java
package com.example.configreload; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class ConfigReloadIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void testConfigEndpoint() { ResponseEntity<Map> response = restTemplate.getForEntity( "http://localhost:" + port + "/api/v1/config/current", Map.class); assertEquals(200, response.getStatusCodeValue()); assertNotNull(response.getBody()); assertTrue(response.getBody().containsKey("appConfig")); } @Test void testReloadStatsEndpoint() { ResponseEntity<Map> response = restTemplate.getForEntity( "http://localhost:" + port + "/api/v1/config/reload/stats", Map.class); assertEquals(200, response.getStatusCodeValue()); assertNotNull(response.getBody()); } } Best Practices
1. Security Considerations
- Never log sensitive configuration values
- Use Kubernetes RBAC to restrict access to Secrets
- Encrypt sensitive data at rest and in transit
- Regularly rotate secrets and certificates
2. Performance Optimization
- Use efficient file watching mechanisms
- Implement debouncing for frequent changes
- Cache configuration values appropriately
- Monitor memory usage during reloads
3. Reliability
- Implement circuit breakers for configuration reloads
- Maintain fallback configuration values
- Test configuration changes in staging first
- Implement rollback mechanisms
4. Monitoring
- Track configuration reload success/failure rates
- Monitor application behavior after configuration changes
- Set up alerts for configuration reload failures
- Log configuration changes for audit purposes
Conclusion
This comprehensive ConfigMap and Secret reloading implementation provides:
- Multiple Strategies: File system watching, Kubernetes API watching, and polling
- Spring Integration: Seamless integration with Spring Boot configuration properties
- Type Safety: Strongly typed configuration models with validation
- Monitoring: Comprehensive metrics and health checks
- Security: Proper handling of sensitive data
- Reliability: Fallback mechanisms and error handling
Key Benefits:
- Zero Downtime: Reload configuration without restarting the application
- Real-time Updates: Immediate propagation of configuration changes
- Kubernetes Native: Full integration with Kubernetes ecosystem
- Observability: Comprehensive monitoring and metrics
- Developer Experience: Simple API for configuration management
By implementing this solution, you can achieve dynamic configuration management that scales with your application's needs while maintaining security and reliability.