Kubernetes doesn’t provide built-in support for ConfigMap
or Secret
versioning. Sometimes such a feature may be useful, when we are deciding to rollback current version of our application. In Kubernetes we are able to rollback just a version of Deployment
without any additional configuration properties stored in ConfigMap
or Secret
.Although Kubernetes does not support versioning, we may achieve it there using some third-party solutions. One of them is Spinnaker. Spinnaker is an open source, multi-cloud continuous delivery platform for releasing software changes with high velocity and confidence created by Netflix. Configuration versioning is just a one of many features offered by that tool. For more details you may refer to its documentation site, because today I would not discuss it.
In this article I’m going to describe my custom versioning mechanism created for Spring Boot applications. I have mentioned Spinnaker, since my concept is very close to the concept used there. We are just creating dedicated ConfigMap
per each new version of Deployment
. My solution is based on the project Spring Cloud Kubernetes Config. It makes Kubernetes ConfigMap
instances available during application bootstrapping, and allow them to be used as property sources by our Spring Boot application. For more details you may refer to my previous article about Spring Cloud Kubernetes Microservices with Spring Cloud Kubernetes.
Source Code
By default, Spring Cloud Kubernetes also does not support ConfigMap
or Secret
versioning. That feature is available in my forked version of this library. For accessing it go to my GitHub repository: https://github.com/piomin/spring-cloud-kubernetes.
Concept
The concept around Kubernetes configuration versioning is pretty easy. Especially with Spring Cloud Kubernetes. We are creating ConfigMap
that needs to be labelled with app
and version. The label app
contains the name of Spring Boot application, that is configured by property spring.application.name
. The label version
indicates a number of application version, which is fetched by the library from property info.app.version
. Both these properties need to be available inside application.yml
orbootstrap.yml
. Here’s sample version 1.0
of ConfigMap
for application api-test
.
apiVersion: v1 kind: ConfigMap metadata: name: api-test-1 labels: version: "1.0" app: api-test data: application.yaml: |- property1: v1.0
We may maintain many versions of ConfigMap
used by a given application just by changing name, labels and content. Here’s version 1.1
of ConfigMap
for the same api-test
application.
apiVersion: v1 kind: ConfigMap metadata: name: api-test-2 labels: version: "1.1" app: api-test data: application.yaml: |- property1: v1.1
By default, Spring Cloud Kubernetes Config is able to inject ConfigMap
to the application basing on its metadata.name
. I redefined this mechanism to base on label app
. Let’s take a look on some details.
Implementation
Now the question is – how to use that feature in our application. Of course, you need to include the required library to your Maven dependencies. Remember to use my forked version Spring Cloud Kubernetes Config instead of the official one.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency>
Then the bootstrap.yaml
file in your application needs to contains at least the following properties to enable ConfigMap
versioning mechanism.
spring: application: name: api-test cloud: kubernetes: config: enableVersioning: true info: app: version: 1.0
Let’s take a look on the key fragment of versioning mechanism implementation inside Spring Cloud Kubernetes Config. If it is not enabled with property spring.cloud.kubernetes.config.enableVersioning
it works basing on metadata.name
– the same as before changes. We are also checking if property info.app.version
is set inside the application settings. If not versioning mechanism is being disabled. Otherwise we are checking all ConfigMaps
in the selected namespace, and filtering them by app
and version
labels.
String version = environment.getProperty("info.app.version"); LOG.info("Get Config: versioning->" + enableVersioning + ", name->" + name + ", version->" + version); Map result = new HashMap(); ConfigMap map = null; if (!enableVersioning || version == null) { map = StringUtils.isEmpty(namespace) ? client.configMaps().withName(name).get() : client.configMaps().inNamespace(namespace).withName(name).get(); } else { Optional optMap = StringUtils.isEmpty(namespace) ? client.configMaps().list().getItems().stream() .filter(configMap -> configMap.getMetadata().getLabels() .containsKey("app") && configMap.getMetadata().getLabels().get("app").equals(name) && configMap.getMetadata().getLabels().containsKey("version") && configMap.getMetadata().getLabels().get("version").equals(version)) .findFirst() : client.configMaps().inNamespace(namespace).list().getItems().stream() .filter(configMap -> configMap.getMetadata().getLabels() .containsKey("app") && configMap.getMetadata().getLabels().get("app").equals(name) && configMap.getMetadata().getLabels().containsKey("version") && configMap.getMetadata().getLabels().get("version").equals(version)) .findFirst(); if (optMap.isPresent()) { map = optMap.get(); } }
Demo
Our sample Spring Boot application is very simple. It just exposes a single HTTP endpoint that display value of property injected from ConfigMap
as shown below.
@RestController public class ApiController { @Value("${property1}") private String property1; @GetMapping("/property") public String getProperty1() { return property1; } }
Of course it is using currently described versioning mechanism. First let’s run instance of Kubernetes locally using Kind (Kubernetes IN Docker). Here’s the command that creates local cluster.
$ kind create cluster --config=cluster.yaml
To make it available under virtual address 192.168.99.100
we need to provide the following configuration file cluster.yaml
.
kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 networking: apiServerAddress: 192.168.99.100 apiServerPort: 6443
After running the following command kind is ready to use.
Now, let’s create two test ConfigMaps
for our application with kubectl apply
command. Let’s take a look on the result.
Then we should deploy our sample app. The only difference between two subsequent deployments is in info.app.version
property, that is 1.0
for first deployment, and 1.1
for the second deployment. Here’s the Deployment
manifest. As you see below it does not inject any ConfigMap
or Secret
using Kubernetes mechanisms.
apiVersion: apps/v1 kind: Deployment metadata: name: api-test spec: selector: matchLabels: app: api-test template: metadata: labels: app: api-test spec: containers: - name: api-test image: piomin/api-test ports: - containerPort: 8080
In the newest version of our sample application I just changed the value of property info.app.version
from 1.0
to 1.1
.
spring: application: name: api-test cloud: kubernetes: config: enableVersioning: true info: app: version: 1.1
Let’s call our test endpoint. It returns value of property1
taken from ConfigMap
labelled with 1.1
.
$ curl http://localhost:8080/property v1.1
Here’s the history of deployments for api-test
application. We may rollback version of deployment to the previous one (1
) by using command kubectl rollout undo
.
Now we may call our test endpoint one more time. As you can see below it returns v1.0
, which is set as a property1
value in ConfigMap
labelled with version 1.0
.
$ curl http://localhost:8080/property v1.0
Conclusion
To use described versioning mechanism on Kubernetes you just need to create ConfigMap
labelled with properly, include forked Spring Cloud Kubernetes Config from my repository and add property info.app.version
to the Spring Boot application properties.