What is the best way to keep environment-specific variables when migrating a Spring Boot application to Kubernetes? Should you create different “application.properties” files and use profiles? Use environment variables? What about security?
A common friction point between developers and DevOps is around injecting specific environment settings to the application. Some developers don’t think ahead, and DevOps rarely provides clear guidance on what should go where.
We’re finally past the “hardcoded” connection strings in the codebase days (are we really?).
In Java Spring Boot, the application.properties, or application.yaml files, give you a few options for customization:
Profiles
One way of supporting multiple environments is to keep multiple application.properties files in the project’s “resource” folder. You will keep one copy of the file for each environment, and specify a profile when running the Spring Boot application.
applicaiton.properties
development-application.properties
staging-application.properties
To specify a profile when running the app, use the file prefix in the profile option:
java -Dspring.profiles.active=staging -jar unicornapp.jar
Once you compile the application, the files are bundled in the JAR and ready for use by specifying the profile name.
That works fine for “simple” and straightforward deployments but doesn’t cut it for modern dockerized environments.
On top of that, you don’t want to keep credentials, secrets, or any sensitive information in your source control. This is why this solution should only be used for local development environments.
Environment Variables
Spring Boot allows you to use environment variables in the application.properties file and even use default values in case they are not set.
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/dbname
I In the above example, you can see that the hostname is set to an environment variable “MYSQL_HOST” with a default value of “localhost”.
So if a developer runs the app without setting any environment variables, the connection string would be:
jdbc:mysql://localhost:3306/dbname
And if in production you will set the MYSQL_HOST environment variable to mysql-prod the connection string will be:
jdbc:mysql://mysql-prod:3306/dbname
When you assign values to properties, try to figure out if they would change in different environments and avoid hard coding them. That includes connection strings, file locations, names, etc.
External application.properties files
Next, there’s a handy feature of the Spring Boot application.properties file you can use - You can override the original values by placing an external application.propeties file next to the JAR on the server.
your-unicorn-app.jar
application.properties
When you do that, Spring Boot will override merge the two files and will give precedence to the values in the external file.
So if the bundled application.properties file has the following values:
logging.level.org.springframework=INFO
logging.level.root=WARN
logging.level.com.baeldung=TRACE
## Database
spring.datasource.url=jdbc:mysql://localhost:3306/dbname
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create
And the application.properties file you placed next to the jar has the following values:
logging.level.root=ERROR
spring.datasource.url=jdbc:mysql://msql-prod:3306/dbname
spring.datasource.username=dbusername
spring.datasource.password=FancyPassword
server.port=8085
Then the effective settings will be:
logging.level.org.springframework=INFO
logging.level.root=ERROR
logging.level.com.baeldung=TRACE
## Database
spring.datasource.url=jdbc:mysql://msql-prod:3306/dbname
spring.datasource.username=dbusername
spring.datasource.password=FancyPassword
spring.jpa.hibernate.ddl-auto=create
server.port=8085
As you can see, Spring Boot merged the values.
What’s the best way to set values when we deploy in Kubernetes?
application.properties in Kubernetes
There are various ways to set environment specific settings in Kubernetes, and my tendency is to use environment variables when possible.
I do prefer using the mounted application.properties files in the form of ConfigMaps and Secrets in the following cases:
- If there are many values to override - otherwise, the configuration can quickly get unreadable.
- I don’t like having credentials, tokens, etc., in environment variables. At one point, I even got some pushbacks from a few large enterprises.
- When you don’t have a choice - the developers didn’t allow for overriding the values from environment variables. That happens as well.
Passing environment variables to the container in Kubernetes
Let’s say that you want to update the spring.datasource.url by using an environment variable.
If the original entry in application.properties is set to:
jdbc:mysql://${MYSQL_HOST:localhost}:3306/dbname
Where MYSQL_HOST is the environment variable for the hostname. Just add the following to the deployment YAML:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demowebapp
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: demowebapp
image: registry.gitlab.com/unicorn/unicornapp:1.0
ports:
- containerPort: 8080
imagePullPolicy: Always
env:
- name: MYSQL_HOST
value: mysql-prod
Now, suppose that we want to use a secret to update the spring.datasource.password.
First, we need to create a secret:
apiVersion: v1
kind: Secret
metadata:
name: datasource-credentials
type: Opaque
data:
dbuser: ZGJ1c2VybmFtZQ==
dbpassword: RmFuY3lQYXNzd29yZA==
Note that the values in the YAML file are base64 encoded.
Apply that to your cluster to create the secret.
Then, we can mount the secrets as environment variables:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demowebapp
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: demowebapp
image: registry.gitlab.com/unicorn/unicornapp:1.0
ports:
- containerPort: 8080
imagePullPolicy: Always
env:
- name: MYSQL_HOST
value: mysql-prod
- name: MYSQK_USER
valueFrom:
secretKeyRef:
name: datasource-credentials
key: dbuser
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: datasource-credentials
key: dbpassword
Mounting an application.properties file next to the JAR
Kubernetes comes with two options we can use to store our application.properties file:
- ConfigMap
- Secret
The difference between them is that ConfigMaps are not supposed to contain sensitive information, and are a tiny bit easier to work with.
Secrets, on the other hand, are meant for storing sensitive information and offer better security.
Because in most cases, the values that you will need to inject to the application.properties files will contain sensitive environment-specific information, I would recommend choosing secrets.
If you want to use ConfigMaps instead, read this document for more information
For this example, lets assume that the applicaiton.properties file content for the production environment is:
spring.datasource.url=jdbc:mysql://msql-prod:3306/dbname
spring.datasource.username=dbusername
spring.datasource.password=FancyPassword
I will base64 encode the file and copy the result, then paste it as the value for the application.properties key:
apiVersion: v1
kind: Secret
metadata:
name: application.properties
type: Opaque
data:
application.properties: c3ByaW5nLmRhdGFzb3VyY2UudXJsPWpkYmM6bXlzcWw6Ly9tc3FsLXByb2QzMzA2L2RibmFtZQpzcHJpbmcuZGF0YXNvdXJjZS51c2VybmFtZT1kYnVzZXJuYW1lCnNwcmluZy5kYXRhc291cmNlLnBhc3N3b3JkPUZhbmN5UGFzc3dvcmQK
Don’t commit this file to your source control “as is” thinking that the values encrypted.
An alternative way of creating this secret is to load it directly from a file. You can read more about it here.
We now want to mount the secret as a file and place it next to the JAR. So assuming that the JAR is located at /opt/unicorn/your-unicorn-app.jar :
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demowebapp
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: demowebapp
image: registry.gitlab.com/unicorn/unicornapp:1.0
ports:
- containerPort: 8080
imagePullPolicy: Always
volumeMounts:
- name: application-properties
mountPath: "/opt/unicorn/application.properties"
readOnly: true
subPath: application.properties
volumes:
- name: application-properties
secret:
secretName: application.properties
I would highly recommend checking out Helm, as it allows you to use templates, value files, and other neat features that will help you create a better workflow.