google-site-verification: google61fe8ba583a51912.html
Dynamic Configuration Updates: Implementing ConfigMap Reloading in Java Applications

In Kubernetes, ConfigMaps are essential for externalizing configuration from your Java applications. But what happens when you need to update configuration without restarting your application? ConfigMap reloading enables dynamic configuration updates while your application continues running. Let's explore various strategies to implement ConfigMap reloading in Java applications.

Understanding ConfigMap Updates in Kubernetes

When a ConfigMap is updated in Kubernetes:

  • The update is eventually propagated to mounted volumes
  • Environment variables from ConfigMaps are NOT updated - they're injected only at pod startup
  • Volume-mounted ConfigMap files ARE updated - typically within 1-2 minutes

Strategy 1: File Watching with Spring Boot

Basic Spring Boot Configuration

// src/main/java/com/company/config/ReloadableConfig.java @Configuration @ConfigurationProperties(prefix = "app") @Data public class AppConfig { private String featureFlag; private int timeout; private String apiUrl; private List<String> allowedHosts; }

File Watcher Implementation

// src/main/java/com/company/config/ConfigMapWatcher.java @Component @Slf4j public class ConfigMapWatcher { private final AppConfig appConfig; private final ObjectMapper objectMapper; private WatchService watchService; private Path configPath; public ConfigMapWatcher(AppConfig appConfig, @Value("${config.path:/etc/config}") String configPath) { this.appConfig = appConfig; this.objectMapper = new ObjectMapper(); this.configPath = Paths.get(configPath); initializeWatcher(); } @PostConstruct public void initializeWatcher() { try { this.watchService = FileSystems.getDefault().newWatchService(); configPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); Thread watchThread = new Thread(this::watchForChanges); watchThread.setDaemon(true); watchThread.setName("config-watcher"); watchThread.start(); log.info("ConfigMap watcher initialized for path: {}", configPath); } catch (IOException e) { log.error("Failed to initialize config watcher", e); } } private void watchForChanges() { while (!Thread.currentThread().isInterrupted()) { try { WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { Path changedFile = (Path) event.context(); if (changedFile.toString().equals("application.properties") || changedFile.toString().equals("app-config.json")) { log.info("Config file changed: {}", changedFile); reloadConfiguration(); } } } boolean valid = key.reset(); if (!valid) { log.error("Watch key no longer valid"); break; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.info("Config watcher interrupted"); } catch (Exception e) { log.error("Error in config watcher", e); } } } private void reloadConfiguration() { try { // Reload from mounted ConfigMap volume Properties properties = new Properties(); Path propertiesFile = configPath.resolve("application.properties"); if (Files.exists(propertiesFile)) { try (InputStream input = Files.newInputStream(propertiesFile)) { properties.load(input); } // Update configuration beans updateAppConfig(properties); log.info("Configuration reloaded successfully"); } } catch (Exception e) { log.error("Failed to reload configuration", e); } } private void updateAppConfig(Properties properties) { // Update the configuration object if (properties.containsKey("app.feature-flag")) { appConfig.setFeatureFlag(properties.getProperty("app.feature-flag")); } if (properties.containsKey("app.timeout")) { appConfig.setTimeout(Integer.parseInt(properties.getProperty("app.timeout"))); } if (properties.containsKey("app.api-url")) { appConfig.setApiUrl(properties.getProperty("app.api-url")); } log.info("Updated AppConfig - featureFlag: {}, timeout: {}", appConfig.getFeatureFlag(), appConfig.getTimeout()); } }

Strategy 2: Spring Cloud Kubernetes Reload

Dependencies Setup

<!-- pom.xml --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId> <version>2.1.7</version> </dependency>

Configuration

# application.yml spring: cloud: kubernetes: reload: enabled: true mode: event strategy: refresh config: sources: - name: ${spring.application.name} namespace: default

Application Setup

// src/main/java/com/company/Application.java @SpringBootApplication @Configuration @RefreshScope @EnableScheduling public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean @RefreshScope public RestTemplate restTemplate() { return new RestTemplate(); } } // Config class that will be refreshed @Component @RefreshScope @Data public class DynamicConfig { @Value("${app.feature.enabled:false}") private boolean featureEnabled; @Value("${app.cache.timeout:300}") private int cacheTimeout; @Value("${app.api.endpoint:http://localhost:8080}") private String apiEndpoint; }

Strategy 3: Kubernetes API Watch Implementation

Direct Kubernetes API Integration

// src/main/java/com/company/k8s/KubernetesConfigWatcher.java @Component @Slf4j public class KubernetesConfigWatcher { private final KubernetesClient kubernetesClient; private final ApplicationEventPublisher eventPublisher; private final String namespace; private final String configMapName; public KubernetesConfigWatcher(KubernetesClient kubernetesClient, ApplicationEventPublisher eventPublisher, @Value("${app.configmap.name:app-config}") String configMapName) { this.kubernetesClient = kubernetesClient; this.eventPublisher = eventPublisher; this.configMapName = configMapName; this.namespace = getCurrentNamespace(); } @PostConstruct public void startWatching() { Thread watchThread = new Thread(this::watchConfigMap); watchThread.setDaemon(true); watchThread.setName("k8s-config-watcher"); watchThread.start(); } private void watchConfigMap() { try { kubernetesClient.configMaps() .inNamespace(namespace) .withName(configMapName) .watch(new Watcher<ConfigMap>() { @Override public void eventReceived(Action action, ConfigMap configMap) { if (action == Action.MODIFIED) { log.info("ConfigMap {} modified", configMapName); eventPublisher.publishEvent(new ConfigMapUpdateEvent(this, configMap.getData())); } } @Override public void onClose(WatcherException cause) { log.warn("ConfigMap watch closed, reconnecting...", cause); // Implement reconnection logic } }); log.info("Started watching ConfigMap: {} in namespace: {}", configMapName, namespace); } catch (Exception e) { log.error("Failed to watch ConfigMap", e); } } private String getCurrentNamespace() { try { return kubernetesClient.getNamespace(); } catch (Exception e) { log.warn("Could not determine namespace, using 'default'", e); return "default"; } } } // Custom event for configuration updates public class ConfigMapUpdateEvent extends ApplicationEvent { private final Map<String, String> configData; public ConfigMapUpdateEvent(Object source, Map<String, String> configData) { super(source); this.configData = configData; } public Map<String, String> getConfigData() { return configData; } } // Event listener to handle updates @Component @Slf4j public class ConfigMapUpdateListener { private final DynamicConfig dynamicConfig; public ConfigMapUpdateListener(DynamicConfig dynamicConfig) { this.dynamicConfig = dynamicConfig; } @EventListener public void onConfigMapUpdate(ConfigMapUpdateEvent event) { log.info("Processing ConfigMap update"); Map<String, String> newData = event.getConfigData(); // Update configuration based on new data if (newData.containsKey("feature.enabled")) { dynamicConfig.setFeatureEnabled(Boolean.parseBoolean(newData.get("feature.enabled"))); } if (newData.containsKey("cache.timeout")) { dynamicConfig.setCacheTimeout(Integer.parseInt(newData.get("cache.timeout"))); } log.info("Dynamic configuration updated: featureEnabled={}, cacheTimeout={}", dynamicConfig.isFeatureEnabled(), dynamicConfig.getCacheTimeout()); } }

Strategy 4: Scheduled Configuration Reload

Periodic Config Checker

// src/main/java/com/company/config/ScheduledConfigReloader.java @Component @Slf4j @EnableScheduling public class ScheduledConfigReloader { private final ConfigMapWatcher configMapWatcher; private final File configFile; private long lastModified; public ScheduledConfigReloader(@Value("${config.file:/etc/config/app.properties}") String configFilePath) { this.configMapWatcher = configMapWatcher; this.configFile = new File(configFilePath); this.lastModified = configFile.lastModified(); } @Scheduled(fixedDelay = 30000) // Check every 30 seconds public void checkForConfigChanges() { if (!configFile.exists()) { return; } long currentModified = configFile.lastModified(); if (currentModified > lastModified) { log.info("Config file changed detected, reloading configuration..."); lastModified = currentModified; configMapWatcher.reloadConfiguration(); } } }

Complete Kubernetes Deployment Example

ConfigMap Definition

# k8s/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: java-app-config labels: app: java-app data: application.properties: | app.feature-flag=ENABLED app.timeout=5000 app.api-url=https://api.company.com/v1 app.cache.enabled=true app.retry.count=3 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>

Deployment with Volume Mounts

# k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: java-app spec: replicas: 2 selector: matchLabels: app: java-app template: metadata: labels: app: java-app spec: containers: - name: java-app image: my-registry/java-app:latest ports: - containerPort: 8080 volumeMounts: - name: config-volume mountPath: /etc/config readOnly: true - name: logging-config-volume mountPath: /app/config readOnly: true env: - name: CONFIG_PATH value: "/etc/config" - name: LOG_CONFIG_PATH value: "/app/config" resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1000m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 5 periodSeconds: 5 volumes: - name: config-volume configMap: name: java-app-config items: - key: application.properties path: application.properties - name: logging-config-volume configMap: name: java-app-config items: - key: logback.xml path: logback.xml

Advanced: Dynamic Log Level Configuration

// src/main/java/com/company/config/DynamicLoggingConfig.java @Component @Slf4j public class DynamicLoggingConfig { private final LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); @EventListener public void onConfigMapUpdate(ConfigMapUpdateEvent event) { Map<String, String> configData = event.getConfigData(); if (configData.containsKey("log.level.root")) { String logLevel = configData.get("log.level.root"); setLogLevel("ROOT", logLevel); log.info("Updated root log level to: {}", logLevel); } if (configData.containsKey("log.level.com.company")) { String logLevel = configData.get("log.level.com.company"); setLogLevel("com.company", logLevel); log.info("Updated package log level to: {}", logLevel); } } private void setLogLevel(String loggerName, String level) { Logger logger = loggerContext.getLogger(loggerName); switch (level.toUpperCase()) { case "TRACE": logger.setLevel(Level.TRACE); break; case "DEBUG": logger.setLevel(Level.DEBUG); break; case "INFO": logger.setLevel(Level.INFO); break; case "WARN": logger.setLevel(Level.WARN); break; case "ERROR": logger.setLevel(Level.ERROR); break; default: logger.setLevel(null); // Inherit from parent } } }

Testing ConfigMap Reloading

Unit Test

// src/test/java/com/company/config/ConfigMapWatcherTest.java @ExtendWith(MockitoExtension.class) class ConfigMapWatcherTest { @Mock private AppConfig appConfig; @TempDir Path tempDir; private ConfigMapWatcher configMapWatcher; @Test void testConfigReload() throws Exception { // Create test config file Path configFile = tempDir.resolve("application.properties"); Files.write(configFile, "app.feature-flag=TEST\napp.timeout=1000".getBytes()); configMapWatcher = new ConfigMapWatcher(appConfig, tempDir.toString()); // Update config file Files.write(configFile, "app.feature-flag=PROD\napp.timeout=2000".getBytes()); // Allow time for file system events Thread.sleep(100); verify(appConfig, times(1)).setFeatureFlag("PROD"); verify(appConfig, times(1)).setTimeout(2000); } }

Best Practices for ConfigMap Reloading

  1. Use Readiness Probes: Update readiness status during configuration reload
  2. Implement Circuit Breakers: Prevent issues during configuration transitions
  3. Version Your ConfigMaps: Include version information in ConfigMap metadata
  4. Graceful Fallbacks: Maintain previous configuration if reload fails
  5. Monitoring: Track configuration reload events and failures
  6. Resource Limits: Set appropriate memory and CPU limits for watch operations
// Health indicator for configuration state @Component public class ConfigHealthIndicator implements HealthIndicator { private final ConfigMapWatcher configWatcher; private volatile boolean configValid = true; @Override public Health health() { if (configValid) { return Health.up().withDetail("config", "healthy").build(); } else { return Health.down().withDetail("config", "reload failed").build(); } } }

Conclusion

ConfigMap reloading in Java applications provides powerful dynamic configuration capabilities:

  • Spring Cloud Kubernetes: Simplest approach for Spring Boot applications
  • File Watching: Universal approach that works with any Java application
  • Kubernetes API Watch: Most responsive but requires more setup
  • Scheduled Reloading: Simple and reliable for less frequent updates

Choose the strategy that best fits your application's requirements:

  • Use Spring Cloud Kubernetes for full-featured Spring Boot applications
  • Use file watching for non-Spring applications or when you need fine-grained control
  • Use Kubernetes API watch for real-time updates and complex configuration scenarios

By implementing ConfigMap reloading, your Java applications can adapt to configuration changes without downtime, improving operational flexibility and deployment velocity.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

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


Macro Nepal Helper