spring
spring-boot
spring-cloud

Dynamic configuration management with Spring Cloud Config

More than 3 years have passed since last update.

Original post is http://blog.ik.am/#/entries/290 (Japanese)

Please correct my poor English ;)


What is Spring Cloud Config?

Spring Cloud Config project provides a mechanism of configuration in a distributed system.

My most interested topic in SpringOne 2gx 2014.

Spring Cloud Config consists of Client and Server.

Sever manages the external configuration such as Git and properties file, provides the centralized configuration to all of Client.

Client holds the configuration from Server using the abstraction mechanism Spring Framework originally has such as EnvironmentandPropertySource.

It also enables reloading the configuration.

The minimum system using them is as follows:

"Normal application" requires the configuration is a Client. Even though there are many Clients, Server can mange them centrally.


How to use

Note that the following contents are for 1.0.0.M1 version, these can be changed drastically in the future.


Setup Config Server

Building Config Server is very simple. Add dependency on org.springframework.cloud:spring-cloud-config-server and just put EnableConfigServer to the entry point class.

Setting example of pom.xml is described later.

Entry point class is like as follows:

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@EnableConfigServer // important!!
@ComponentScan
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

Describe the configuration where to fetch the configuration in bootstrap.yml just under the classpath (Note that it's not application.yml).

As a default, spring-cloud-samples/config-repo is configured to fetch. In this case, because I want to change properties, I'm using my making/config-repo forked from it and set the url of Git share repository url in spring.platform.config.server.uri property like:

spring.platform.config.server.uri: https://github.com/making/config-repo

Finally, the setting of the pom.xml. Since the official version has not been released yet, it has a little redundant configuration, it would be more simple after 1.0.0.RELEASE. The important part is the dependency on org.springframework.cloud:spring-cloud-config-server.

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>demo</groupId>
<artifactId>configserver</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath/>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starters</artifactId>
<version>1.0.0.M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.App</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>${spring-loaded.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

</project>

The project structure is as follows:

Server runs on 8888 port when you run App class. Defaut configured application.yml is included in spring-cloud-config-server-1.0.0.M1.jar.

By accessing http://localhost:8888/{name}/{env}/{label}, you can get the configuration for each environment(profile) of each application.

Probably you can regard



  • name as application name


  • env as profile name (default is default)


  • label as branch name (default is master)

label can be omitted.

In the making/config-repo example, these are the following pic:

$ curl -X GET http://localhost:8888/foo/default | jq .

{
"propertySources": [
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "default"
}

Then set development to env and send a request:

$ curl -X GET http://localhost:8888/foo/development | jq .

{
"propertySources": [
{
"source": {
"bar": "spam"
},
"name": "https://github.com/making/config-repo/foo-development.properties"
},
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "development"
}

Returned both values default anddevelopment. Which to use is determined at Client side. (this case,developmentis prioritized)

Let's change foo-development.properties on Github as follows:

bar: Updated!

foo: Added!

Then send a request tohttp://localhost:8888/foo/development again

$ curl -X GET http://localhost:8888/foo/development | jq .

{
"propertySources": [
{
"source": {
"foo": "Added!",
"bar": "Updated!"
},
"name": "https://github.com/making/config-repo/foo-development.properties"
},
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "development"
}

Returned latest value though pulling Git(In the following description, the original contents are reverted with git push -f origin HEAD^:master)

How to configure authentication/authorization and encrypt/decrypt are described in the official document.


Setup Client

Let's move on to the Client side. Client is a Spring Boot application with the dependency on org.springframework.cloud:spring-cloud-config-client, which enables to connect Config Server automatically and use properties via Config Server.

Also add the dependency on org.springframework.boot:spring-boot-starter-actuator.

The application name of Client can be defined with spring.application.name key in bootstrap.yml.

spring:

application:
name: foo

By default, Config Server connects to http://localhost:8888 with env=default and label=master. To override, configure as bellow:

spring:

application:
name: foo
cloud:
config:
env: default # optional
label: master # optional
uri: http://localhost:8888 # optional

In ClientAppclass which is the entry point for Client, implement a simple controller uses bar property.

package demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@EnableAutoConfiguration
@ComponentScan
@RestController
public class ClientApp {
@Value("${bar:World!}")
String bar;

@RequestMapping("/")
String hello() {
return "Hello " + bar + "!";
}

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

pom.xml is as follows

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>demo</groupId>
<artifactId>configclient</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath/>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starters</artifactId>
<version>1.0.0.M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.ClientApp</start-class>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>${spring-loaded.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

</project>

Project structure is as follows. (application.yml is described later.)

You can find properties are obtained from Config Server by listing PropertiesSource using the feature of Spring Boot Actuator (http://localhost:8080/env).

You can also see a single property.

$ curl http://localhost:8080/env/bar

123456

When you access to the controller,

$ curl http://localhost:8080

Hello 123456!

you can find the property on Config Server is injected.


Change configuration dynamically

Then rewrite a property value on the Config Server as follows:

bar: Spring Boot

When you access a single property,

$ curl http://localhost:8080/env/bar

123456

the change on the Config Server is not reflected at this stage. In order to reflect it, accessing refresh endpoint with POST method is required.

$ curl -X POST http://localhost:8080/refresh

["bar"]
$ curl http://localhost:8080/env/bar
Spring Boot

You can find PropertySourceis reflected in the client.

Again,

$ curl http://localhost:8080

Hello 123456!

It is not possible to refresh the properties which are already "DI"ed (because this Controller is a singleton scope).

In order to re"DI", restart DI container is required by accessing restartendpoint with POST method.

$ curl -X POST http://localhost:8080/restart

{"message":"Restarting"}

You can find DI container restarted from log (but it takes a few seconds).

$ curl http://localhost:8080

Hello Spring Boot!

At last, I could refresh the configuration without restarting the application!

By the way, restartendpoint is disabled by default. To enable it application.yml is needed to set as follows:

endpoints:

restart:
enabled: true


Ad-hoc configuration change

Without rewriting the Config Server, it is possible to change the configuration in the Client side ad hoc.

POST properties to env endpoint as follows:

$ curl -X POST http://localhost:8080/env -d bar="Spring Cloud"

{"bar":"Spring Cloud"}

At this point, PropertySource of this application is re-written, updated value can be obtained by GETting the property.

$ curl http://localhost:8080/env/bar

Spring Cloud

However, the result of the controller is unchanged since re-DI is not performed.

$ curl http://localhost:8080

Hello Spring Boot!

Also refesh cannot change the result.

$ curl -X POST http://localhost:8080/refresh

[]
$ curl http://localhost:8080
Hello Spring Boot!

Again restart by restarting the DI container, it is possible to rewrite the result of the controller.

$ curl -X POST http://localhost:8080/restart

{"message":"Restarting"}
$ curl http://localhost:8080
Hello Spring Cloud!


Introduction of Refresh scope

So far, you may feel unhappy to restart DI container because it takes a long time.

So refresh scope is introduced. A bean annotated with@RefreshScopeare re-instantiated without restarting the DI container by POSTing refresh endpoint.

Modify ClientApp

package demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@EnableAutoConfiguration
@ComponentScan
@RestController
@RefreshScope // important!
public class ClientApp {
@Value("${bar:World!}")
String bar;

@RequestMapping("/")
String hello() {
return "Hello " + bar + "!";
}

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

When the application is restarted, it can be seen that ad hoc changes has been lost.

$ curl http://localhost:8080

Hello Spring Boot!

Again, change the value

$ curl -X POST http://localhost:8080/env -d bar="Spring Cloud Config"

{"bar":"Spring Cloud Config"}

The send a POST request to refresh endpoint.



$ curl -X POST http://localhost:8080/refresh

[]



Although a little while ago I did not changed this controller anything, this time @RefreshScope is anontated.

$ curl http://localhost:8080

Hello Spring Cloud Config!

It was possible to reflect the properties!

By the way,


  • The beans annotated @ConfigurationProperties

  • The properties for the log level logging.level. *

has a refresh scope from the beginning.


Sample code is here

I thought again Spring's DI container was excellent!

This feature should be prepared in Spring core.

Introduction of @Conditional (rather than Spring Boot) was revolutionary for Spring which provides like this functionality without almost any settings.