Spring Boot and Kubernetes go hand in hand when building modern microservices. Spring Boot is a widely used JVM-based framework which allows you to build out stand-alone, production grade applications. Kubernetes is an open source container management and orchestration system, which makes it quick and easy to deploy and manage those production grade applications.
Setting The Scene
I’ve recently been working on a project where we’re writing Spring Boot microservices, which are being deployed into a Kubernetes cluster. My previous experience has primarily been with Docker Swarm up until this project, so it’s been an interesting switch with different challenges. One of those challenges was how to externalize the Spring Boot configuration within the Kubernetes cluster, I’d thought it would be similar to Docker Swarm, but there were several differences I found along the way.
In Docker Swarm, you deploy services via a stack file, this is very similar to docker-compose for those familiar with that approach. You can define environment variables in the stacks files which are then passed to the running containers.
In Kubernetes, everything is a resource, and you manage those resources through manifest files. Similarly to Docker Swarm, you can define environment variables for your Kubernetes Pods and Deployments within those manifest files. Defining environment variables within a manifest file is really useful, as it allows you to abstract configuration values away from the application you’re building. However, it can become difficult when you have a lot of configuration that you want to abstract, on the one hand, you want to abstract that configuration to separate the concerns between the application and the resource management, on the other, you don’t want the resource definition (manifest file in Kubernetes) becoming overly verbose and unreadable.
Enter ConfigMaps.
The Humble ConfigMap
ConfigMaps in Kubernetes are a great way of abstracting your configuration values away from your application, and also decoupling your configuration from your image, which ensures your application is more portable.
A great usage of ConfigMaps is to externalize your Spring Boot configuration away from your application. If we’re building applications in the right way, most production grade services will have gone through various environments before finally being deployed into production. Within each environment, resources are likely to be different, even from the most basic aspects such as datasource endpoints. One of the best things about building containerised applications, is it gives you the ability to ensure your application operates consistently in each environment. One key enabler of this is to externalize your configuration. That way, the image you build doesn’t change throughout each environment, only the configuration that gets injected into it.
By using a ConfigMap, you ensure you keep your application image the same in every environment, you abstract your application configuration away from the application itself and you have a specific, separated resource to manage that configuration.
A Datasource Story
A good example (I find) of externalized configuration is for a datasource. I’m sure many of you reading this have, at some point, had multiple configuration entries in your application for your datasource in each environment, I know I have. From experience, I find this bad practice for several reasons; it bloats your application configuration in your service, it ties your configuration to your service code and it means you have to update that application code (and therefore your image) for each environment you want to deploy into.
An alternative approach, and an approach I’ve taken on my current project, is to utilise Kubernetes ConfigMaps to hold the configuration for our Spring Boot services.
To show the approach I’ve taken, I’ll run through an example of configuring a datasource for a Spring Boot application and externalizing that configuration with a Kubernetes ConfigMap.
The Datasource Configuration
Let’s say we have a simple Spring Boot application, which connects to a MySQL (other databases are available) instance. We have the following configuration in our application.yml
file:
example:
datasource:
url: jdbc:mysql://localhost/example
username: local-user
password: local-password
When the application is running, the above configuration will allow us to connect to a local MySQL instance, great!
Now let’s say we know we’ve got at least 2 other environments we’ll be deploying to before production. We could update our configuration as follows:
example:
datasource:
url: jdbc:mysql://localhost/example
username: local-user
password: local-password
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:mysql://dev/example
username: dev-user
password: dev-password
---
spring:
config:
activate:
on-profile: pre-prod
datasource:
url: jdbc:mysql://pre-prod/example
username: pre-prod-user
password: pre-prod-password
The above is fine, it’ll likely work and get the job done. However, what happens if the endpoint changes in dev? Or the username gets changed in pre-prod? We’d need to make application code changes, re-build the image and then re-deploy to each environment. Another major flaw to this approach is that you’re keeping sensitive data such as usernames and passwords in your application code, which is a major security issue for production grade applications.
An alternative approach would be to use Spring Boot externalized configuration using property place-holders to abstract the actual property values away from the application code, like so:
example:
datasource:
url: jdbc:mysql://localhost/example
username: local-user
password: local-password
---
spring:
config:
activate:
on-profile: dev
datasource:
url: ${example.datasource.url}
username: ${example.datasource.username}
password: ${example.datasource.password}
---
spring:
config:
activate:
on-profile: pre-prod
datasource:
url: ${example.datasource.url}
username: ${example.datasource.username}
password: ${example.datasource.password}
This approach gives you far greater flexibility; you don’t have to change application code each time your configuration changes, the same image can be promoted through each environment and you abstract your configuration values away from your application code.
Creating The Image
Now that we’ve created the configuration file for our application, we want to build it as a Docker image. This is really simple, but you can make it as sophisticated or complex as you like, depending on the requirements you have for the image you’re building.
In this example, I’ve used Maven to build the application, so assuming we’ve run a mvn clean install
prior to building the image, we have the built jar file ready to go.
We can create a very simple Dockerfile:
FROM openjdk:11-slim
COPY target/*.jar app.jar
CMD java -jar app.jar
The above Dockerfile uses the openjdk:11-slim
base image, copies the jar file we created earlier from the mvn clean install
and runs a Docker CMD
to execute that jar file.
Now we can run a docker build to create the above image ready for use:
docker build -t config-demo .
The ConfigMap
As mentioned previously, Kubernetes has a specific resource type for managing config resources: the ConfigMap. In order for us to abstract our datasource configuration from above in the Kubernetes space, we can create a ConfigMap with the desired values. It’s worth noting there are several different ways this can be achieved, in this example though, I’ve used the spring application json method.
There are multiple ways of creating a ConfigMap in Kubernetes, most commonly using kubectl. In this example we create the ConfigMap from a file called dev-configmap.yaml
with the following contents:
apiVersion: v1
kind: ConfigMap
metadata:
name: spring-config
data:
dev-config.json:
'{
"example.datasource.url": "jdbc:mysql://dev-endpoint/example",
"example.datasource.username": "dev-root",
"example.datasource.password": "dev-pass"
}'
In the file above, we create a resource kind of ConfigMap
called spring-config
- the name is important as we’ll need to know this in order to refer to it later. Every ConfigMap has a data
section which can contain anything you like, in our example, it contains a JSON entry with a key of dev-config.json
.
In order to create this ConfigMap in our Kubernetes cluster, we can run the following command to apply
the file:
kubectl apply -f dev-configmap.yaml
Bringing it together
By this point, we have our application ready to accept injected configuration properties and a Docker image created from that application. We also have a ConfigMap deployed into our Kubernetes cluster, so now we can bring that all together by deploying a Pod into our cluster, that uses our newly built image and ConfigMap.
Kubernetes Pods are the smallest deployable units in Kubernetes. We’re going to create a very simple Pod definition (example-pod.yaml
) that deploys our image and uses our ConfigMap as an environment variable for the running container.
apiVersion: v1
kind: Pod
metadata:
labels:
app: config-demo
name: config-demo
spec:
containers:
- image: config-demo:latest
name: config-demo
ports:
- containerPort: 8080
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILE
value: dev
- name: SPRING_APPLICATION_JSON
valueFrom:
configMapKeyRef:
name: spring-config
key: dev-config.json
restartPolicy: Never
Okay, so what exactly is the above Pod definition doing I hear you ask. Firstly, we know it’s a Pod because of the Kind
- which is Pod. Secondly, we’ve given it a name
of config-demo, this will be the name of the Pod in the Kubernetes cluster. Next, we define a spec
, which is the Pod specification, it defines everything needed about this Pod to Kubernetes. We define a single container, which uses the image we built earlier (config-demo:latest
) and defines some key information such as ports to be used and the environment (env
) variables to be used.
We’re going to focus on the env
definition. As you can see from the Pod definition above, we’ve defined 2 variables, firstly the SPRING_PROFILE
which has a value of dev
. This indicates to the running Spring Boot application which active profile to use. As we defined several configuration entries in our application.yml
file, it’s important we set this value so that Spring knows which config set to pick up. Secondly, we define a SPRING_APPLICATION_JSON
variable, which is referencing an entry from our ConfigMap we created earlier.
Focussing on the SPRING_APPLICATION_JSON
variable, defining an env
variable this way in the Pod, allows us to pull data from a ConfigMap running in the cluster via the configMapKeyRef
value type. What this is doing, is looking in our Kubernetes cluster for a ConfigMap called spring-config
(that’s why the naming is important, these have to be the same!) and specifically within that map, looking for a key
called dev-config.json
.
Furthermore, this combination of ConfigMap name and key, will pick up the following entry from our ConfigMap:
dev-config.json:
'{
"example.datasource.url": "jdbc:mysql://dev-endpoint/example",
"example.datasource.username": "dev-root",
"example.datasource.password": "dev-pass"
}'
When we run this Pod, the configuration values from the json structure above will get injected into our running Spring Boot service!
Demo in Action
Now with all of the above in place, we have everything we need to create the new Pod and see if it’s all working as expected.
As a simple check, I’ve created a controller in the Spring Boot service, that when invoked, will just print out those configuration values we’ve passed to the application.
First off, let’s create the Pod using the kubectl apply method:
kubectl apply -f example-pod.yaml
Now we can check the Pod is running by using some other kubectl commands (I always do this to sanity check what I’ve created):
kubectl get pods
The output should contain an entry for the newly created Pod:
NAME READY STATUS RESTARTS AGE
config-demo 1/1 Running 0 3s
We can also check our ConfigMap is present
kubectl get configmaps
Which should show us something similar to
NAME DATA AGE
spring-config 1 3d23h
So our Pod is running, our ConfigMap is present, we should now be able to invoke the service to see if everything has worked as expected. In this scenario, we’ll need to use Kubernetes port-forwarding as we don’t have a running Kubernetes Service in front of our Pod. In order to do this, we simply run the following command:
kubectl port-forward pod/config-demo 8080:8080
Which just tells Kubernetes to port-forward
the Pod config-demo
which is running on container port 8080
to port 8080
running on my machine.
Now we can open up a browser (or using your favourite REST client) and hit our endpoint localhost:8080/hello
When doing so, we should see (or get a response of) Hello World!
This is great, as we know the application is running, but we want to see if our configuration has been injected properly from our ConfigMap. We can now check the logs for the running pod with the following command:
kubectl logs pod/config-demo
All being well, we should see the following output at the end of the logs
2020-12-08 08:51:28.525 INFO 7 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
DB URL: jdbc:mysql://dev-endpoint/example
DB Username: dev-root
DB Password: dev-pass
Ta-daa! It’s all working as expected, our application has picked up the dev-config.json
entry from our ConfigMap and injected it into our running Spring Boot application!
Every Good Story Needs a Sequel
So there we have a simple, but useful example of how Kubernetes ConfigMaps can be used to externalize Spring Boot configuration. That’s not the end of the story though, and you likely have some questions.
One question I definitely have is, what about sensitive information (such as usernames & passwords)?
ConfigMaps are great at abstracting configuration away from your application code. Ensuring you have a flexible, maintainable, isolated pattern for storing and updating application configuration. However, by themselves they’re only part of the solution. For storing and utilising more sensitive information within Kubernetes, you have Secrets, but that’s another post for another day…
Example Project Code
If you’ve gotten this far, great stuff! I hope you’ve learnt something from this post and found the content and example project useful. If you want to see the project I’ve used to accompany this post, it can be found in the example project repository.