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:
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.
Spring Boot allows you to use environment variables in the application.properties file and even use default values in case they are not set.
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:
And if in production you will set the MYSQL_HOST environment variable to mysql-prod the connection string will be:
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.
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:
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:
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.