This article shows another way in which Configurations within SpringBoot Applications can be externalized when running on Kubernetes, using a ConfigMap Custom Watcher.
This example uses Kubernetes Java Client to read from ConfigMap instead of SpringBoots Native method. This allows one spring application to read from Multiple ConfigMap instead of just one by default.
Here we have two application app1 and app2 each read from their own ConfigMap which are also names app1 and app2 respectively , and both applications also read from a Shared ConfigMap called Toggle. This example is helpful when designing applications which can externalize their configurations in multiple ConfigMap objects in Kubernetes.
Pre-Requisites
You will need to following for this tutorial:
- helm
- A Kubernetes flavor is accessible, it can either be a local setup via Minikube, Docker Desktop, or even a cloud-based setup using GKE or EKS. This article will mostly focus on using Docker Desktop to demonstrate, but the configuration and the setup should stay the same across all Kubernetes flavors.
- IDE for ease of use, you can use any editor of your choice. In this example we have used IntelliJ IDEA from jetbrains.
- kubectl CLI .
- Please also go through previous articles on SpringBoot Application setup to run in K8s.
1. Create SpringBoot Applications
Create two SpringBoot applications , lets call them app1 and app2 and also create Helm charts for each application with the same name. Here is how the setup should look like:
You can refer to the code here for more details.
2. Deploy Spring Cloud Watcher Controller
Spring Cloud Watcher controller calls refresh endpoint of the SpringBoot application on Kubernetes. This controller essentially watches for changes in the ConfigMap object and then via service discovery calls the /refresh endpoint. Refer to the official documentation on this for more details.
For this article we will use the sample configuration provided here, but this can be easily customized for production usage. Save the above yaml and apply it you cluster.
kubectl apply -f spring-watcher-controller.yaml
3. ConfigMap Configuration
Lets now create a ConfigMap object that will be shared between these two applications app1 and app2. Create a ConfigMap with the following ConfigMap object in k8s cluster.
apiVersion: v1
kind: ConfigMap
metadata:
name: shared
namespace: default
labels:
spring.cloud.kubernetes.config: "true"
annotations:
spring.cloud.kubernetes.configmap.apps: "app1, app2"
data:
shared.toggleA: FromConfigMap
Notice that this configmap has the annotation spring.cloud.kubernetes.configmap.apps
. This annotation tells the Spring Watcher controller deployed in the earlier step to call refresh endpoints of all pods associated with app1 and app2 SpringBoot Applications. For more details on how this works, please refer to the Spring Documentation here.
4. Spring ConfigMap Custom Watcher Configuration
By Default a SpringBoot application will read from Configmap defined in spring.cloud.kubernetes.config.name
property. The limitation of this way is only one ConfigMap can be read from a SpringBoot application at a time. Lets create a custom configuration that reads from the Shared configmap object that we created above.
Create file SharedConfig.java and put it in both app1 and app2 application under src/main/java/com/example/<app-name>
@Configuration
@RefreshScope
public class SharedConfig {
private String toggleA;
public SharedConfig() throws IOException, ApiException {
ApiClient client = Config.defaultClient();
io.kubernetes.client.openapi.Configuration
.setDefaultApiClient(client);
CoreV1Api api = new CoreV1Api();
V1ConfigMap configMap=api.readNamespacedConfigMap(
"shared",
"default",
null);
Map<String, String> configmapData = configMap.getData();
if(configmapData!=null){
this.setToggleA(configmapData.get("shared.toggleA"));
}
}
}
5. Deploy Helm Charts
Now lets build deploy both app1 and app2 to our k8s cluster. Run the following series of commands to create an image using jib and then deploy the chart to k8s cluster. Make sure that the image version you are building is the same version that the chart is using.
cd configmap-watcher-example
cd app1 &&./gradlew jibDocker
helm install app1 helm/app1/
cd ../
cd app2 && ./gradlew jibDocker
helm install app2 helm/app2/
cd ../
6. Test ConfigMap Custom Watcher
Lets first see if both applications were able to read the data from Shared ConfigMap at startup
$ export SERVICE_IP_APP2=$(kubectl get svc --namespace default app2 --output jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ curl http://$SERVICE_IP_APP2:8082/propertyValues
Timeout=500, Message=Overriden Message from Configmap, Shared Message=FromConfigMap
$ export SERVICE_IP_APP1=$(kubectl get svc --namespace default app1 --output jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ curl http://$SERVICE_IP_APP1:8081/propertyValues
Timeout=500, Message=Overriden Message from Configmap, ToggleA=FromConfigMap
So both applications were able to read the Shared ConfigMap object at startup. Now lets change this ConfigMap and verify if it automatically reflects.
Change the shared.toggleA value to something else, here I have modified it to FromConfigMap354 with update. You will notice now that the value as indeed changed within both apps.
$ curl http://$SERVICE_IP_APP2:8082/propertyValues
Timeout=500, Message=Overriden Message from Configmap, Shared Message=FromConfigMap354
$ curl http://$SERVICE_IP_APP1:8081/propertyValues
Timeout=500, Message=Overriden Message from Configmap, ToggleA=FromConfigMap354
7. Troubleshooting
- If something goes wrong in helm and you want to start over just delete the chart by running
helm delete app1
- YAML is very sensitive to indentation, so please ensure that the indentation is proper
- Java 17 version at the time of writing this article was not compatible with the latest version of SpringBoot, so ensure you don’t change the spring boot version to a higher than 2.6.3
- if all else fails you can still download the code from the GitHub repo and try comparing it with your setup