Introduction
In this article, we will implement the third principle of the 12-Factor Application methodology in a Spring Boot and Kubernetes environment. Doing so allows us to avoid common problems such as:
- Applications running with incorrect configuration for a specific environment
- Delegating the responsibility for correct configuration to the DevOps role rather than developers
- Minimizing risk related to misconfiguration
When working across multiple environments with microservices, a common anti-pattern causes frequent issues — particularly in production. This pattern consists of assuming that everything has been compiled correctly with the right configuration. Instead, this tutorial demonstrates how to properly implement the third principle of the 12-Factor App methodology.
As stated in the 12-Factor App documentation:
"The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard."
Prerequisites
- A running Kubernetes cluster. If you do not have one, you can run a local cluster using tools such as kind or minikube.
- One or more Spring Boot applications deployed to Kubernetes.
Problem
Consider a business with two applications: a users service and a transactions service. Suppose there is a specific configuration for a development (dev) environment and sensitive configuration for a production (prod) environment.
Common Workaround
A typical workaround is to define multiple application.properties profiles in Spring Boot, one per environment, as shown below:
.
├── application.properties
├── dev-application.properties
└── prod-application.properties
A variable is then defined in application.properties and its value is changed manually each time a build is required for a specific environment:
spring.profiles.active=dev
Pitfalls of this approach:
This solution requires a separate compilation per environment. If, for example, there are four environments (DEV, QA, TEST, and PROD) and five microservices, the team must perform twenty compilations instead of five. More critically, this approach is error-prone: a service could be compiled for the dev environment and accidentally deployed to production, resulting in poor customer experience or financial loss.
Best Practice
The recommended approach is to store environment-specific configuration within the environment itself and have the application consume it at runtime. While Spring Boot provides a Config Server, this introduces additional infrastructure costs — particularly in cloud architectures where CPU and memory resources are at a premium. For Kubernetes-native microservices, there is a simpler and more cost-effective solution.
Overview of the solution:
- Define an
application.propertiesfile with the values for the target environment (e.g.,dev). - Transform this file into a Kubernetes
ConfigMap. - Modify the Deployment manifest to mount the
application.propertiesfile at the path/config/. - Configure Spring Boot to load its configuration from this mounted path instead of the compiled artifact.
Detailed Steps
Step 1: Define the ConfigMap
Create a properties file with the desired configuration for your environment:
spring.profiles.active=dev
server.port=8083
# JPA configuration
spring.jpa.database=POSTGRESQL
spring.jpa.hibernate.ddl-auto=auto
spring.jpa.properties.hibernate.default_schema=public
spring.jpa.generate-ddl=false
# SQL initialization configuration
spring.sql.init.platform=postgres
Then create a Kubernetes ConfigMap from the file:
kubectl create configmap application-properties --from-file=application.properties
Step 2: Configure the Deployment Manifest
According to the official Spring Boot documentation, it is possible to define an external configuration location using the SPRING_CONFIG_LOCATION environment variable. When this variable is set, Spring Boot will ignore the compiled artifact application.properties and instead load configuration from the specified path.
Additionally, the application.properties file from the ConfigMap must be mounted into the container using a Kubernetes volume. The following Deployment manifest illustrates this configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-transactions-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: api-transactions-app
template:
metadata:
labels:
app: api-transactions-app
spec:
containers:
- name: api-transactions-app
image: examplebusiness/api-transactions:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
protocol: TCP
env:
- name: SPRING_CONFIG_LOCATION
value: file:/app/conf/transactions-additional.properties
volumeMounts:
- mountPath: /app/conf/
name: application-properties
volumes:
- name: application-properties
configMap:
name: application-properties
With this configuration in place, every time the application starts — whether in development or production — it will load the environment-specific configuration that was injected at the infrastructure level.
Pros and Cons
This approach migrates the configuration pipeline from an error-prone process to a safer, more reliable workflow. However, there are trade-offs to consider.
Advantages:
- A single compiled artifact is used across all environments, eliminating redundant builds.
- Configuration is decoupled from the application code, reducing the risk of environment-specific values being committed to the repository.
- Responsibility for environment configuration is clearly delegated to the infrastructure layer.
Disadvantages:
- Configuration is mounted at container startup. Therefore, any change to the
ConfigMaprequires a pod restart, which may result in brief downtime depending on the application's restart time.
Fortunately, this limitation can be addressed by using Spring Cloud Kubernetes PropertySource Reload, which enables dynamic configuration refresh without requiring a full pod restart.
Top comments (0)