This post will be included in a chapter from a forthcoming book Building Microservices for the Cloud, A guide for busy Java developers I'm writing. If you enter your email address here, I'll send you the previous chapters as well as each new chapter when it gets published!
This post has been featured on https://spring.io/blog/2017/01/03/this-week-in-spring-january-3rd-2017.

CENTRALIZED AND VERSIONED CONFIGURATION USING SPRING CLOUD CONFIG SERVER AND GIT

The previous blog post covered Service Registration and Discovery as an infrastructure service used in a microservice architecture. In this post I’ll review another infrastructure microservice, the Configuration management service.

Configuration is used to prevent hard-coding values in the applications. But it’s also used to specify what is expected to be different between deployment environments (qa, staging, production, …) such as host names, mail and database credentials and other backing services. The latter type of configuration is identified as one of the elements in The Twelve-Factor App.

The benefits of this practice are that it promotes building only one artifact, only one binary to be executed in all the environments. You should never have to build a different executable for each environment, this will most-likely cause troubleshooting and debugging headaches.

As part of the Spring Cloud Series, this post describes the Spring Cloud Config’s server and client usage, the configuration storage options and its relation with the Discovery Server in a registration-first and configuration-first approaches.

Spring Cloud Series:

CONFIGURATION FIRST vs REGISTRATION FIRST

The Configuration server could be used as a standalone server, without any other infrastructure service, all is needed to know is its URL. However it could also be used with the Discovery server in two ways:

  • In a configuration-first approach, the client application connects to the Config server via the property spring.cloud.config.uri either set in a bootstrap file (bootstrap.yml or bootstrap.properties) or as a environment variable. The Eureka server URL would be configured in a property served by the Config server and the client application can now discover dependent services.

  • In a registration-first approach, the Config server registers with the Discovery server similarly to as other services do and the client application can bootstrap the Config server metadata using the applicable discovery settings. The benefits of this approach is that the client application only need to know the service name of the Config service. The disadvantage is that a extra network round trip is needed to get the Config service metadata before the client application can connect to it to get the properties.

Spring Cloud Config - Registration First Registration first


REQUIREMENTS

  • Java 8 or Java 7. For Java 7, java.version property inside pom.xml needs to be updated accordingly.
  • Maven 3.3.x
  • Familiarity with Spring Framework.
  • A Eureka server instance for the Demo Config client and the Config server in a registration-first approach to register with.

CREATE THE CONFIG SERVER BACKED BY A GIT REPOSITORY

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.2.RELEASE -d dependencies=actuator,cloud-config-server,cloud-eureka -d language=java -d type=maven-project -d baseDir=configserver -d groupId=com.asimio.cloud -d artifactId=config-server -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named configserver with most of the dependencies used in the accompanying source code for this post.

Alternatively, it can also be generated using Spring Initializr tool then selecting Actuator, Config Server and Eureka Discovery dependencies as shown below:

Spring Initializr - Generate Config Server

The most relevant sections of Config server’s pom.xml are shown below:

 1 ...
 2 <parent>
 3   <groupId>org.springframework.boot</groupId>
 4   <artifactId>spring-boot-starter-parent</artifactId>
 5   <version>1.4.2.RELEASE</version>
 6   <relativePath /> <!-- lookup parent from repository -->
 7 </parent>
 8 ...
 9 <properties>
10 ...
11   <start-class>com.asimio.cloud.config.ConfigServerApplication</start-class>
12   <spring-cloud.version>Camden.SR3</spring-cloud.version>
13 ...
14 </properties>
15 ...
16 <dependencyManagement>
17   <dependencies>
18     <dependency>
19       <groupId>org.springframework.cloud</groupId>
20       <artifactId>spring-cloud-dependencies</artifactId>
21       <version>${spring-cloud.version}</version>
22       <type>pom</type>
23       <scope>import</scope>
24     </dependency>
25   </dependencies>
26 </dependencyManagement>
27 ...
28 <dependencies>
29   <dependency>
30     <groupId>org.springframework.boot</groupId>
31     <artifactId>spring-boot-starter-actuator</artifactId>
32   </dependency>
33   <dependency>
34     <groupId>org.springframework.cloud</groupId>
35     <artifactId>spring-cloud-config-server</artifactId>
36   </dependency>
37   <dependency>
38     <groupId>org.springframework.cloud</groupId>
39     <artifactId>spring-cloud-starter-eureka</artifactId>
40   </dependency>
41 </dependencies>
42 ...
43 <build>
44 ...
45   <plugins>
46     <plugin>
47       <groupId>org.springframework.boot</groupId>
48       <artifactId>spring-boot-maven-plugin</artifactId>
49     </plugin>
50 ...

This pom file inherits from Spring Boot and defines Spring Cloud dependencies management as a bom. spring-cloud-config-server brings the corresponding dependencies for this artifact to function as a Config server while spring-cloud-starter-eureka brings Eureka client dependencies for the Config server to register with a Discovery server. spring-boot-maven-plugin allows to package the application as a jar or war file and to run it from command line using Maven as shown here.

ConfigServerApplication.java, the entry point of this application looks like:

package com.asimio.cloud.config;
...
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(ConfigServerApplication.class);
  }

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

and EurekaClientConfiguration.java:

package com.asimio.cloud.config;

@Profile("registration-first")
@Configuration
@EnableDiscoveryClient
public class EurekaClientConfiguration {

}

Splitting the configuration in two classes is needed because the same Config server artifact could be used as a standalone service or as a service that registers with Eureka. No Eureka-related bean will be auto-configured if the registration-first Spring profile is not used.

@EnableConfigServer allows this application to run as a Config server aided by Spring Boot’s auto-configuration capabilities. @SpringBootApplication and SpringBootServletInitializer have been already explained in Developing Microservices using Spring Boot, Jersey, Swagger and Docker while @EnableDiscoveryClient was reviewed in Service Registration and Discovery using Spring Cloud Eureka.

The last relevant piece of the Spring Cloud Config server would be its configuration properties:

application.yml:

 1 spring:
 2   cloud:
 3     config:
 4       server:
 5         git :
 6           uri: https://bitbucket.org/asimio/demo-config-properties
 7 
 8 ---
 9 spring:
10   profiles: registration-first
11 eureka:
12   client:
13     registerWithEureka: true
14     fetchRegistry: true
15     serviceUrl:
16       defaultZone: http://localhost:8000/eureka/
17   instance:
18     hostname: ${hostName}
19     statusPageUrlPath: ${management.context-path}/info
20     healthCheckUrlPath: ${management.context-path}/health
21     preferIpAddress: true
22     metadataMap:
23       instanceId: ${spring.application.name}:${server.port}

spring.cloud.config.server.git.uri property points to the Git repo backing the Spring Cloud Config server where files with properties would be stored. When using the registration-first Spring profile, the Config server will register with a Eureka server located at eureka.client.serviceUrl.defaultZone’s value.

Notes:
Passing spring.cloud.config.server.git.uri, eureka.client.serviceUrl.defaultZone, … as VM args, will override defaults from these files.
If the Git repo is private, its credentials would to be passed using spring.cloud.config.server.git.username and spring.cloud.config.server.git.password properties.

CREATE THE DEMO CONFIG CLIENT

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.2.RELEASE -d dependencies=actuator,cloud-config-client,cloud-eureka,web -d language=java -d type=maven-project -d baseDir=demo-config-client -d groupId=com.asimio.demo.api -d artifactId=demo-config-client -d version=0-SNAPSHOT | tar -xzvf -

Similarly to Create the Config server, this command will create a Maven project in a folder named demo-config-client and alternatively, it can also be generated using Spring Initializr tool then selecting Actuator, Config Client, Eureka Discovery and Web dependencies as shown below:

Spring Initializr - Generate Config Client

Demo Config client’s pom.xml is similar to Config server’s except it includes spring-cloud-starter-config to connect to the Config server and spring-boot-starter-web to implement an API and provide actuator endpoints instead of spring-cloud-config-server:

 1 ...
 2 <properties>
 3 ...
 4   <start-class>com.asimio.api.demo.main.Application</start-class>
 5 ...
 6 </properties>
 7 ...
 8 <dependencies>
 9   <dependency>
10     <groupId>org.springframework.boot</groupId>
11     <artifactId>spring-boot-starter-actuator</artifactId>
12   </dependency>
13   <dependency>
14     <groupId>org.springframework.boot</groupId>
15     <artifactId>spring-boot-starter-web</artifactId>
16   </dependency>
17   <dependency>
18     <groupId>org.springframework.cloud</groupId>
19     <artifactId>spring-cloud-starter-config</artifactId>
20   </dependency>
21   <dependency>
22     <groupId>org.springframework.cloud</groupId>
23     <artifactId>spring-cloud-starter-eureka</artifactId>
24   </dependency>
25 </dependencies>
26 ...

Application.java, the entry point of the Demo Config client looks like:

package com.asimio.api.demo.main;
...
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(value = { "com.asimio.api.demo.rest" })
public class Application extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(Application.class);
  }

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

Basically autoconfigures the application beans, enables service registration with the Discovery server and scans components in com.asimio.api.demo.rest package, where the resource implementation is found.

ActorResource.java, the class implementing a simple endpoint:

package com.asimio.api.demo.rest;
...
@RefreshScope
@RestController
@RequestMapping(value = "/actors", produces = "application/json")
public class ActorResource {

  @Value("${app.message:Default}")
  private String msg;

  @RequestMapping(value = "/{id}", method = RequestMethod.GET)
  public Actor getActor(@PathVariable("id") String id) {
    return this.buildActor(id, String.format("First%s", id), String.format("Last%s. %s", id, this.msg));
  }
...
}

This class implements /actors/{id} which sends back a response with the msg value coming from a Git file fronted by the Config server. @RefreshScope is used in conjunction with the refresh actuator endpoint to reload the msg value once it gets updated in the Git repo.

Relevant Demo Config client configuration files look like:

bootstrap.yml:

spring:
  profiles:
    active: known-config-server,development

---
spring:
  profiles: known-config-server
  cloud:
    config:
      uri: http://localhost:8100

---
spring:
  profiles: registered-config-server
  cloud:
    config:
      discovery:
        enabled: true
        serviceId: configuration-server

This application can be run using four Spring profiles, known-config-server, where the location of the Config server is known or registered-config-server, where the Config server metadata is first retrieved via the Discovery server and the other two profiles are development and production.

and application.yml:

...
endpoints:
  enabled: false
  info:
    enabled: true
  health:
    enabled: true
  metrics:
    enabled: true
  refresh:
    enabled: true
...
eureka:
  client:
    fetchRegistry: true
    registerWithEureka: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
  instance:
    hostname: ${hostName}
    statusPageUrlPath: ${management.context-path}/info
    healthCheckUrlPath: ${management.context-path}/health
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${server.port}

For security reasons, actuator endpoints are disabled, so it requires manually enabling the ones needed. In this case /refresh endpoint is used to reload the msg value.
This application also registers with the Eureka server regardless of how the Eureka server metadata is found out.

GIT REPO PROPERTIES FILES

The Git repo backing the Config service properties is available at https://bitbucket.org/asimio/demo-config-properties/src which includes these files:

demo-config-client-development.properties:

app.message=App message from development profile

demo-config-client-known-config-server.properties:

eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/

demo-config-client-production.properties:

app.message=App message from production profile

RUNNING THE CONFIG SERVER IN CONFIG-FIRST MODE

These Maven commands could be used to start the Spring Cloud Config server:

cd <path to>/ConfigServer
mvn spring-boot:run
mvn spring-boot:run -DappPort=8101

Or building the Maven artifact and run it using Java:

mvn clean package
java target/config-server.jar

Or even running it in a Docker container since Asimio’s Config server is publicly hosted in the Docker Hub at https://hub.docker.com/r/asimio/config-server:

sudo docker pull asimio/config-server:1.0.23
sudo docker run -idt -p 8101:8101 -e appPort=8101 asimio/config-server:1.0.23
READING APPLICATION CONFIGURATION PROPERTIES

The Config server could be queried using these formats:

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

For instance:

curl -v http://localhost:8101/demo-config-client-development.properties
*   Trying ::1...
* Connected to localhost (::1) port 8101 (#0)
> GET /demo-config-client-development.properties HTTP/1.1
> Host: localhost:8101
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: Configuration-Server:8101
< Content-Type: text/plain
< Content-Length: 49
< Date: Fri, 09 Dec 2016 04:23:05 GMT
<
* Connection #0 to host localhost left intact
app.message: App message from development profile

RUNNING THE EUREKA SERVER

Since the Demo Config client and the Config server (when running in registration-first approach) will register with the Eureka service, lets start one instance first.
The Eureka server source code could be downloaded from here and run as:

cd <path to>/DiscoveryServer
mvn clean package
java -Dspring.profiles.active=standalone -DappPort=8001 -jar target/discovery-server.jar

RUNNING THE CONFIG CLIENT IN CONFIG-FIRST MODE

Please refer to Running the Eureka server for the commands included in this section to work properly.

cd <path to>/demo-config-client
mvn spring-boot:run -Dspring.profiles.active=known-config-server,development -Dspring.cloud.config.uri=http://localhost:8101 -DhostName=localhost

Sending a request to /actors{id} endpoint:

curl -v http://localhost:8700/actors/1
*   Trying ::1...
* Connected to localhost (::1) port 8700 (#0)
> GET /actors/1 HTTP/1.1
> Host: localhost:8700
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-config-client:known-config-server,development:8700
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 09 Dec 2016 04:36:13 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1. App message from development profile","lastUpdate":null}

The output includes App message from development profile which is a property value coming from demo-config-client-development.properties.
The Demo Config client also registers with a Eureka service located at http://localhost:8001/eureka/, value coming from demo-config-client-known-config-server.properties when using the known-config-server and pictured next:

Demo Config client registration in config-first approach

RUNNING THE CONFIG SERVER IN REGISTRATION-FIRST MODE

Please refer to Running the Eureka server for the commands included in this section to work properly.

Using spring-boot-maven-plugin plugin to start the Spring Cloud Config server:

cd <path to>/ConfigServer
mvn spring-boot:run -DappPort=8101 -Dspring.profiles.active=registration-first -DhostName=localhost -Deureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/

Or starting the resulting artifact using Java:

java -Dspring.profiles.active=registration-first -DhostName=localhost -Deureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/ target/config-server.jar

Or starting multiple containers of asimio/config-server Config server Docker image available at https://hub.docker.com/r/asimio/config-server:

sudo docker pull asimio/config-server:1.0.23
sudo docker run -idt -p 8101:8101 -e appPort=8101 -e spring.profiles.active=registration-first -e hostName=localhost -e eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/ asimio/config-server:1.0.23
sudo docker run -idt -p 8102:8102 -e appPort=8102 -e spring.profiles.active=registration-first -e hostName=localhost -e eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/ asimio/config-server:1.0.23
READING APPLICATION CONFIGURATION PROPERTIES

Once again, demo-config-client application properties for development Spring profile are reachable through:

curl -v http://localhost:8101/demo-config-client-development.properties
*   Trying ::1...
* Connected to localhost (::1) port 8101 (#0)
> GET /demo-config-client-development.properties HTTP/1.1
> Host: localhost:8101
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: Configuration-Server:registration-first:8101
< Content-Type: text/plain
< Content-Length: 49
< Date: Fri, 09 Dec 2016 05:14:35 GMT
<
* Connection #0 to host localhost left intact
app.message: App message from development profile

And the Config server is also registered with the Eureka service.

RUNNING THE CONFIG CLIENT IN REGISTRATION-FIRST MODE

Please refer to Running the Eureka server for the commands included in this section to work properly.

cd <path to>/demo-config-client
mvn spring-boot:run -Dspring.profiles.active=registered-config-server,development -DhostName=localhost -Deureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/

Sending a request to /actors{id} endpoint:

curl -v http://localhost:8700/actors/2
*   Trying ::1...
* Connected to localhost (::1) port 8700 (#0)
> GET /actors/2 HTTP/1.1
> Host: localhost:8700
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-config-client:registered-config-server,development:8700
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 09 Dec 2016 05:32:12 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"2","firstName":"First2","lastName":"Last2. App message from development profile","lastUpdate":null}

In this case, both, the Config server and Demo Config client service are registered with a Eureka instance at http://localhost:8001/eureka/ as shown next:

Config server and Demo Config client registration in registration-first approach

DYNAMIC CONFIGURATION

What happens if a properties file located in the Git repository is changed? Lets say demo-config-client-production.properties is updated to:

-app.message=App message from production profile
+app.message=Updated app message from production profile

In order for the Demo Config client application to reflect this change, someone would have to send a POST request to /refresh actuator endpoint to the client application. Support for this behavior was added in ActorResource by adding the @RefreshScope annotation and enabling /refresh endpoint.

Start the Discovery server:

java -Dspring.profiles.active=standalone -DappPort=8001 -jar target/discovery-server.jar

Start the Config server:

java -DappPort=8101 -jar target/config-server.jar

Start the Demo Config client:

java -Dspring.profiles.active=known-config-server,production -Dspring.cloud.config.uri=http://localhost:8101 -DhostName=localhost -jar target/demo-config-client.jar

Send request to /actors/{id} endpoint:

curl -v http://localhost:8700/actors/1
*   Trying ::1...
* Connected to localhost (::1) port 8700 (#0)
> GET /actors/1 HTTP/1.1
> Host: localhost:8700
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-config-client:known-config-server,production:8700
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 13 Dec 2016 03:46:07 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1. App message from production profile","lastUpdate":null}

Update property in Git repo:

-app.message=App message from production profile
+app.message=Updated app message from production profile

Reload properties values:

curl -v -d {} -v http://localhost:8700/admin/refresh
*   Trying ::1...
* Connected to localhost (::1) port 8700 (#0)
> POST /admin/refresh HTTP/1.1
> Host: localhost:8700
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Length: 2
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 2 out of 2 bytes
< HTTP/1.1 200
< X-Application-Context: demo-config-client:known-config-server,production:8700
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 13 Dec 2016 04:05:23 GMT
<
* Connection #0 to host localhost left intact
["app.message","config.client.version"]

Send another request to /actors/{id} endpoint:

curl -v http://localhost:8700/actors/1
*   Trying ::1...
* Connected to localhost (::1) port 8700 (#0)
> GET /actors/1 HTTP/1.1
> Host: localhost:8700
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-config-client:known-config-server,production:8700
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 13 Dec 2016 04:05:52 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1. Updated app message from production profile","lastUpdate":null}

As it can be seen, the new property value has been updated without the need to restart the Demo Config client application, but wouldn’t it be nice if the changes are automatically reloaded without the need of hitting /refresh? Stay tuned, go ahead and sign up to the newsletter to receive updates from this blog because in the next post from this series, I’ll cover Refreshable Configuration using Spring Cloud Config Server, Spring Cloud Bus, RabbitMQ and Git.

Thanks for reading and feedback is always appreciated.

SOURCE CODE

Accompanying source code for this blog post can be found at:

REFERENCES