Enterprise Java

Use Spring Cloud Config as externalized configuration

One of the most important practices when it comes to continuous delivery is building your binaries only once and use that one in the different environments (dev, test, acceptance, etc). This means that most likely you will need to externalize your configuration of your application. With Spring Boot applications it comes down to keep the values of your properties in your application.properties outside of your application jar. By doing this you can use the same jar for deployments on different environments (An alternative is to package the values of the properties for all possible environments with your jar but I don’t consider that as a good practice in most situations).
Especially when you are building and deploying Spring Boot applications you should have a look at the Spring Cloud Config project. In this post I will go through the steps as described here and you will see how to setup your own configuration service in just a few steps.
Before I show how to set it up first a overview of the architecture:

  • The Git repository is used to store the configuration values. Git is excellent choice for this since it capabilities for tracking and storing changes.
  • DevOps (or any system that want to make changes to the configuration) can simply change the configuration values by pushing their changes into the Git repo.
  • The Configuration Service stays up to date with the Git repo and publishes the configuration values at request.
  • The services act as Configuration Clients and can request config values from the Configuration Service.

To get this running on your local machine perform the following steps:

The easiest way is to create a directory ‘spring-config-example’ which will contain all the code necessary for this demo. This will be considered the root directory for this post.

Set up the Git repo
In this step I create a property file which I put in a (local) Git repo. The property file will then be used as source for property values for our demo client application.
First create a new directory ‘configstore’ and in that directory create a new file called ‘a-bootiful-client.properties’. Add the following content to the file:

server.port=8000
message = Hello World

management.security.enabled=false

Next create a git repo with the command
git init‘ and add and commit the file to the local repo with
git commit -a -m 'initial commit'
That is it for this step.

Set up the Configuration Service
As said before the service is just another Spring Boot project. To setup the project I created a new subdirectory and put a ‘default’ Spring Boot project in it with the following layout:

├── pom.xml
└── src
    └── main
        ├── java
        │   └── net
        │       └── pascalalma
        │           └── cloud
        │               └── ConfigServiceApplication.java
        └── resources
            └── application.properties

The ‘pom.xml’ has the following content:

<?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>net.pascalalma.cloud</groupId>
  <artifactId>configuration-service</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Camden.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

This is a fairly straightforward pom for a Spring Boot Project. The only added dependency is the one for the ‘spring-cloud-config-server’.
The Application class looks like this:

package net.pascalalma.cloud;

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

@EnableConfigServer
@SpringBootApplication
public class ConfigServiceApplication {

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

The only thing different here is the annotation ‘EnableConfigServer’ which makes this Spring Boot application act as a Configuration Server.
Finally there is the application.properties containing the following:

server.port=8888
spring.cloud.config.server.git.uri=/Users/pascalalma/projects/sandbox/spring-config-example/configstore

Besides defining the port I added the uri to the Git repo that is holding the properties to use. As said before I use a local repo here but this could of course also be a remote Git repository.
That is all there is for the server. Lets set up the client application next.

Set up a Client Service
To set up the client project I created a new subdirectory and put another ‘default’ Spring Boot project in it with the following layout:

├── pom.xml
└── src
    └── main
        ├── java
        │   └── hello
        │       └── ConfigClientApplication.java
        └── resources
            └── bootstrap.properties

As you can see this project is as simple as the other one. It contains a pom, one Java file and a property file. Lets go through each of them.
The ‘pom.xml’ contains the following:

<?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>net.pascalalma.cloud</groupId>
  <artifactId>configuration-client</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-config</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-web</artifactId>
    </dependency>
  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Camden.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

Next is the Java file. It actually contains two classes: the Application to start up Spring Boot and a Controller processing incoming HTTP requests. The file looks like this:

package net.pascalalma.cloud.client;

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

@SpringBootApplication
public class ConfigClientApplication {

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

@RefreshScope
@RestController
class MessageRestController {

    @Value("${message:Hello default}")
    private String message;

    @RequestMapping("/message")
    String getMessage() {
        return this.message;
    }
}

The RefreshScope is added to be able to reload the properties when I modify them later on in our test.
The last file is the ‘bootstrap.properties’. This file is used by the Spring Boot CLient application when the application is started to load the remote ‘application.properties’ before the app is started. The file contains the following:

spring.application.name=a-bootiful-client
spring.cloud.config.uri=http://localhost:8888

Also not very complicated I think. It contains the name of the client application. This name is used to select the correct properties file from the Configuration service. Second line contains the location of the cloud configuration server. You can read more about it here.

Test the setup
Now with all code in place I can check if it works. I open three Terminal sessions.
The first one I navigate to the ‘configuration-server’ directory and perform a ‘mvn spring-boot:run‘ to start the configuration server.
The second one I navigate to the ‘configuration-client’ directory and perform a ‘mvn spring-boot:run‘ to start the configuration client.
The third one I use to make a call to the client to test the config server. When I perform the command ‘curl localhost:8000/message‘ I expect to get the message returned that I put in the configstore at the first step:

$ curl localhost:8000/message
Hello World!

Now when I make a change to the configuration store it won’t be reflected automatically in the client. I edit the file ‘a-bootiful-client.properties’ and update the message to this:
message = Hello World from Spring Boot!\n\n. And of course commit the file to the repository with ‘git commit -a -m 'updated message'
When I perform the same curl command on the client application there won’t be any change. If I curl the message directly on the configuration server I do see the change:

$ curl localhost:8888/a-bootiful-client/default
{"name":"a-bootiful-client"
,"profiles":"default"]
,"label":"master"
,"version":"98c6f8d8dd9a9b2cb36496ca4ac54ffb35806dbc"
,"state":null
,"propertySources":[
  {"name":"/Users/pascalalma/projects/sandbox/spring-config-example/configstore/a-bootiful-client.properties"
  ,"source":{"server.port":"8000"
    ,"management.security.enabled":"false"
    ,"message":"Hello World from Spring Boot!\n\n"}
  }
  ]
}

To get this changed value from the configuration server to the client I need to refresh the client with the following command:
'curl -X POST http://localhost:8000/refresh'. If I now curl the client I see the expected updated message:

$ curl localhost:8000/message
Hello World from Spring Boot!

You might have noticed the property ‘management.security.enabled’ set to false in the ‘application.properties’ for this demo. It was to make this easy to test (by default since Spring Boot 1.5 the Actuator endpoints are secure by default)

The functionality shown in this post is really just a tip of the iceberg. Lots more is possible, in combination with other tools like Spring Cloud Consul and Spring Cloud Bus, but also with standard Spring functionality like security and Profiles. This post should be just enough to get you started to check it out yourself.

Pascal Alma

Pascal is a senior JEE Developer and Architect at 4Synergy in The Netherlands. Pascal has been designing and building J2EE applications since 2001. He is particularly interested in Open Source toolstack (Mule, Spring Framework, JBoss) and technologies like Web Services, SOA and Cloud technologies. Specialties: JEE, SOA, Mule ESB, Maven, Cloud Technology, Amazon AWS.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Bruce Edge
Bruce Edge
7 years ago

The instance-specific config inevitably requires credentials as part of the config data. There are many references declaring the saving credentials in github as a bad practice. IMO, saving credentials in a private config-specific repo is very different from simply embedding credentials alongside one’s source code in the main source repo. Do others agree that a config-specific repo negates the “Don’t ever store your credentials in github” mandate? I suppose that one has to weigh the sensitivity of the credentials vs the possible exposure, but that’s a grey area. What other back-ends does spring cloud config support that may be better… Read more »

Back to top button