If you are working with a Spring based micro-services application then having a centralized service to manage configuration of all your micro-services at a central place is very important. Spring Cloud Config supports multiple backends such as filesystem, git repository, s3 bucket, relational databases via JDBC, etc.

Spring Cloud Config Server and Client

Almost all the tutorials you find on the internet are about the git backend. However, sometimes, you want to use a JDBC backend for your config as relational databases are universally available and well supported. The aim of this tutorial is to show how to use Spring Cloud config with JDBC as a backend. We will use the PostgreSQL Database for this purpose.

Spring Cloud Config Server Example

So let’s start by creating a new project with Spring Initializer. Let’s go to https://start.spring.io/ and create a new Spring Project with Maven, Spring Boot 3.0.1 and JDK 17.
Please enter the following data:

  • Project: Maven
  • Language: Java
  • Group: com.mi
  • Artifact: cloud-config-demo
  • Name: cloud-config-demo
  • Description: Demo project for Spring Boot
  • Package Name: com.mi.cloudconfigdemo
  • Packaging: Jar
  • Java: 17
  • Dependencies:
    • Config Server
    • Spring Data JDBC SQL
    • Spring Web
    • Spring Security
    • PostgreSQL Driver

Then click on Generate Project.

To enable the cloud config server we need to use the @EnableConfigServer annotation to our class which has @SpringBootApplication.

File: ./src/main/java/com/mi/cloudconfigdemo/CloudConfigDemoApplication.java

package com.mi.cloudconfigdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class CloudConfigDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(CloudConfigDemoApplication.class, args);
	}

}

Now we need to add the required properties to our application.properties file.

File: ./src/main/resources/application.properties

server.port=8888
spring.security.user.name=configUser
spring.security.user.password=configPass

spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/cloud-config?useSSL=false
spring.datasource.username=postgres
spring.datasource.password=mobisoft
spring.jpa.show-sql=true

spring.profiles.include=jdbc
logging.level.root=DEBUG

Here note that we have used the line spring.profiles.include=jdbc. This line enables the JDBC backend for the Spring Cloud Config server. Rest of the properties are related to the Spring Security and Spring JDBC.

Now to store our application properties, we will need to create a table in our databases. Also we will need to add a couple of entries to this table:

create table PROPERTIES (
  "ID" serial primary key, 
  "CREATED_ON" timestamp ,
  APPLICATION text, 
  PROFILE text, 
  LABEL text, 
  "KEY" text, 
  "VALUE" text
 );

INSERT INTO PROPERTIES ("CREATED_ON", APPLICATION, PROFILE, LABEL, "KEY", "VALUE") VALUES (NULL,'clientapp','dev','latest','prop1','value1');
INSERT INTO PROPERTIES ("CREATED_ON", APPLICATION, PROFILE, LABEL, "KEY", "VALUE") VALUES (NULL,'clientapp','dev','latest','prop2','value2');

To run the project we need to run:

./mvnw spring-boot:run

This should start the cloud config server on the address http://localhost:8888/. Now let’s access the config via an API call. We will use the curl utility to call the API.

The format for the API URL is: {base-path}/{application-name}/{profile}/{label}

So in our case the URL will be: http://localhost:8888/client-app/dev/latest. So the command is:

curl --location --request \
GET 'http://localhost:8888/clientapp/dev/latest' \
-u 'configUser:configPass'

Here note that we are using the Spring Security Basic Auth to access the API. That’s why we need to use the -u 'configUser:configPass' option in the command. This username and password is defined in our application.properties file.

{
  "name": "clientapp",
  "profiles": [
    "dev"
  ],
  "label": "latest",
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "clientapp-dev",
      "source": {
        "prop1": "value1",
        "prop2": "value2"
      }
    }
  ]
}

At this point our setup for the Spring Cloud Config Server is ready. However as you may have noted we are storing the config values as plain text in the DB. It’s better to store sensitive config values encrypted in the DB.

To do this we need add the encryption key in the application.properties file.

File: ./src/main/resources/application.properties

encrypt.key=6hjH65aVFMlouK9Y

Spring Cloud Config provides 2 endpoints to encrypt and decrypt data. However, these are POST endpoints. Since we are using default Spring Security Configuration, CSRF token protection gets applied to these APIs. However, since we are using these for service to service communication without any browser, the CSRF protection is meaningless for us. So let’s disable it. Add the following class to your project:

file: ./src/main/java/com/mi/cloudconfigdemo/WebSecurityConfig.java

package com.mi.cloudconfigdemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .csrf()
          .disable();
        return http.build();
    }

}

Now we can run and use the encrypt and decrypt endpoints. Let’s use curl command for the same. Let’s say we want to encrypt the text Value+=1$% I am purposefully adding some symbols there as passwords tend to use special characters.

curl -X POST 'http://localhost:8888/encrypt' \
--header 'Content-Type: text/plain' \
--data-raw 'Value+=1$%' \
-u 'configUser:configPass'

This should give output:

be68c61f97816e4ba611e12c7604950dd9b37520edbba351fbfab25aa0564879

Please note that on Mac ZSH I get a ‘%’ character at the end of the encrypted value. We need to ignore this.

Now let’s decrypt this value to check if we get our original data back:

curl -X POST 'http://localhost:8888/decrypt' \
--header 'Content-Type: text/plain' \
--data-raw 'be68c61f97816e4ba611e12c7604950dd9b37520edbba351fbfab25aa0564879' \
-u 'configUser:configPass'

This should give output:

Value+=1$%

So it’s working for us. Now let’s store this value in the DB. Use the following SQL for the same:

UPDATE PROPERTIES SET "VALUE" = '{cipher}cf73ca5363b85abfbb3961bb1c30562154d6f1894d25c9ba0c81005e1ad68cf4' WHERE "KEY" = 'prop2';

Here note that we are prefixing the value with {cipher}. This tells Spring Cloud Config that this value is encrypted.

Now let’s start the application and call the config API to check if we get the proper config value in the output:

curl --location --request \
GET 'http://localhost:8888/clientapp/dev/latest' \
-u 'configUser:configPass'
{
  "name": "clientapp",
  "profiles": [
    "dev"
  ],
  "label": "latest",
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "clientapp-dev",
      "source": {
        "prop1": "value1",
        "prop2": "Value+=1$%"
      }
    }
  ]
}

As you can see we are getting the expected output from the API for the prop2 property.

Spring Cloud Config Client Example

Now that we have the server application working, let’s now create the client service and get it’s config from the server we have set up.

Let’s again go to https://start.spring.io/ and create a new Spring Project with Maven, Spring Boot 3.0.1 and JDK 17.

  • Project: Maven
  • Language: Java
  • Group: com.mi
  • Artifact: cloud-config-client
  • Name: cloud-config-client
  • Description: Demo project for Spring Boot
  • Package Name: com.mi.cloudconfigclient
  • Packaging: Jar
  • Java: 17
  • Dependencies:
    • Config Client
    • Spring Web
    • Spring Boot Actuator

Let’s set the application properties first.

File: ./src/main/resources/application.properties

spring.application.name=clientapp
spring.profiles.active=dev
spring.cloud.config.label=latest

spring.config.import=configserver:http://localhost:8888
spring.cloud.config.username=configUser
spring.cloud.config.password=configPass

management.endpoints.web.exposure.include=health,info,refresh

Here note that the spring.application.name must be the same as that of the application in in your Cloud Config on the server side. Same should be the case for spring.profiles.active and spring.cloud.config.label.

Next we set the parameters to access the Cloud Config Server. These properties are: spring.config.import, spring.cloud.config.username and spring.cloud.config.password. These properties should also match their corresponding values on the server side.

At the last we specify that we want to expose the health, info and refresh endpoints from the Spring Boot Actuator project.

Next let’s add a TestConfig class.

File: ./src/main/java/com/mi/cloudconfigclient/TestConfig.java

package com.mi.cloudconfigclient;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class TestConfig {
	
	@Value("${prop1}")
	private String prop1;

	public String getProp1() {
		return prop1;
	}

	public void setProp1(String prop1) {
		this.prop1 = prop1;
	}
}

Here note that we are using @Value annotation to load the prop1 value from the Spring Cloud Config Server. Next let’s create an API where we can use this value.

File: ./src/main/java/com/mi/cloudconfigclient/TestController.java

package com.mi.cloudconfigclient;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
	
	@Autowired
	private Environment env;
	
	@Autowired
	private TestConfig testConfig;
	
	@GetMapping
  public String props() {
	  String prop = env.getProperty("prop1");
    return prop + ":" + testConfig.getProp1();
  }

}

Here we have created a Rest Controller and mapped the GET '/test' path to it. We have 2 autowired members TestConfig and Environment. This is to show you the 2 ways to access the properties from the config. First is the @Value annotation and the second is using Environment.getProperty method.

Now first run the server and the client using the same command ./mvnw spring-boot:run in the respective directories.

./mvnw spring-boot:run

Now let’s access the API and check the output:

curl --location --request GET 'http://localhost:8080/test'

It should output:

value1:value1

Changing the remote config

Now let’s change the value of Prop 1 from the server application and see if gets reflected in the client service. Run the following query to change the value:

UPDATE PROPERTIES SET "VALUE" = 'ChangedValue' WHERE "KEY" = 'prop1';

Now you can call the config API using curl to check the output of the API.

curl --location --request \
GET 'http://localhost:8888/clientapp/dev/latest' \
-u 'configUser:configPass'
{
  "name": "clientapp",
  "profiles": [
    "dev"
  ],
  "label": "latest",
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "clientapp-dev",
      "source": {
        "prop1": "ChangedValue",
        "prop2": "Value+=1$%"
      }
    }
  ]
}

So here we can see that in the config API output the value of the prop1 has changed to ChangedValue.

Now let’s we call the client test service:

curl --location --request GET 'http://localhost:8080/test'

It still outputs

value1:value1

To refresh the config, we need to call Spring Boot Actuator ‘refresh’ API.

curl --location --request POST 'http://localhost:8080/actuator/refresh'

It outputs:

["prop1"]

This shows that the value of `prop1` has changed.

So let’s call the client test service again.

curl --location --request GET 'http://localhost:8080/test'

It outputs

ChangedValue:value1

So there is a point to note that property loaded with Environment.getProperty updated at the runtime, however the property loaded with @Value annotation didn’t update. The reason for this is that the TestConfig bean is a singleton. So its instance is already created when the application starts.

To download the complete code for this tutorial please click here.

Conclusion

In this tutorial we saw how to implement Spring Cloud Config Server and Spring Cloud Config Client. We also saw how to encrypt the sensitive property values at rest. Hope this should help you to use Spring Cloud Config in your next microservices project.

Author's Bio:

mobisoft-pritam
Pritam Barhate

Pritam Barhate, with an experience of 14+ years in technology, heads Technology Innovation at Mobisoft Infotech. He has a rich experience in design and development. He has been a consultant for a variety of industries and startups. At Mobisoft Infotech, he primarily focuses on technology resources and develops the most advanced solutions.