Configuration is one of the factors of the twelve-factor app. Environment-specific configuration should never be part of the compiled code. You should never have to modify and rebuild the code to propagate it through the environments en route to production. In this article, I discuss how to create a centralised configuration server using Spring Boot and Spring Cloud.

Spring Boot and Spring Cloud makes short work of standing a configuration service up and creating REST endpoints to use that configuration. Spring Cloud Config allows the configuration to be held within a Git repository thus enabling journaling and subjecting the configuration to the same process flow (i.e. pull requests) as the code base and as such can be linked to the story/requirement for traceability.

You can find the source code for this example in GitHub.

Create a configuration service

Creating a configuration service is ridiculously easy with Spring Boot. Just go to start.spring.io, enter your Group and Artifact, enter ‘config server’ in the Search for dependencies box and select Config Server and hit Generate Project.

Create configuration sevice

You’ll get a zip file containing a Maven (or Gradle if you so choose) project. Extract this zip file and open the project in your favourite editor of choice.

Open the ConfigServiceApplication.java file and add the EnableConfigServer annotation to the class:

ConfigServiceApplication.java
12345678
@EnableConfigServer
@SpringBootApplication
public class ConfigServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServiceApplication.class, args);
    }
}
application.properties
123
spring.application.name=config-service
server.port=8888
spring.cloud.config.server.git.uri=file://${user.home}/config-service-example-config

Here we specify the name of the application, the port and the location of the property files that make up the configuration.

Note: Here we are pointing at a local Git repository in the home directory. You could specify the absolute URL to a GitHub repository such as https://github.com/iancollington/config-service-example-config.

Next, we need to create the config-service-example-config directory and initialise it as a Git repository. You will, therefore, need to have Git installed.

Open a command prompt/shell and do the following:

$ mkdir ~/config-service-example-config
$ cd ~/config-service-example-config
$ git init

Under this directory, we can create an application.properties file that will contain properties available to every service/application that requests the configuration from the configuration server.

For now lets add a global message:

application.properties
1
app.global.message=Hello Global World!

The configuration server will only read files from the Git repository that have been committed. Therefore we need to commit our application-properties file:

$ git add application.properties
$ git commit -m "First version of application.properties file"

If we now start the application, either via your favourite IDE or from the command line and go to http://localhost:8888/ you get a standard Spring Boot whitelabel error page containing a 404 not found message.

The config server doesn’t serve anything at the root of the web application.

Endpoints in the configuration server take the form of http://<server>:<port>/<application-name>/<profile>/<label>` where

  • <server> and <port> are self explanatory
  • <application-name> is that defined in the bootstrap.properties or application.properties file by the spring.application.name property
  • <profile> comes from the name of the active Spring Profile if specified via the JVM parameter spring.profiles.active. This is useful for different configuration files between environments. e.g. java -jar -Dspring.profiles.active=dev application.jar will look for a file named <application-name>-dev.properties
  • <label> is an optional Git label that defaults to ‘master’

In the next section, we will create another application named message-service as we will specify in the application.properties file. We will, therefore, be able to access the configuration at http://localhost:8888/message-service/dev.

Given this URL the config will load the following property files if they exist:

  • application.properties
  • message-service.properties
  • message-service-dev.properties

Therefore, even though we have yet to create this message-service, when a client requests the configuration from the configuration server via URL we will get a JSON payload back containing the global configuration defined in application.properties:

JSON response payload
 1 2 3 4 5 6 7 8 910111213141516
{
  "name": "message-service",
  "profiles": [
    "master"
  ],
  "label": null,
  "version": null,
  "propertySources": [
    {
      "name": "file:///Users/ian/config-service-example-config/application.properties",
      "source": {
        "app.global.message": "Hello Global World!"
      }
    }
  ]
}

If we had a message-service.properties in that directory we would also get the properties from that file as well.

Let’s create that service now.

Using the configuration service

To make use of this config service, let’s stand up a simple REST endpoint that returns a message in the payload where the message value is defined in the configuration.

Again, we go to start.spring.io, enter the Group and Artifact and select our required dependencies; Config Client and Web.

Create message sevice

After hitting the Generate Project button you’ll get a zip file. Extract the zip file and open the project in your favourite editor.

First, we need to update the application.properties file to set the application name, port and the location of the configuration server.

application.properties
123
spring.application.name=message-service
server.port=8080
spring.cloud.config.uri=http://localhost:8888/

Now in the main application class, we can add a /message endpoint by adding the following code:

MessageServiceApplication.java
 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435363738394041424344454647
@RestController
@SpringBootApplication
public class MessageServiceApplication {

    @Value("${app.global.message}")
    private String globalMessage;

    @Value("${app.local.message}")
    private String localMessage;

    @RequestMapping(value = "/message", method = RequestMethod.GET)
    public Message getMessage() {
        return new Message(localMessage, globalMessage);
    }

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

    private class Message {

        private String localMessage;

        private String globalMessage;

        public Message(final String localMessage, final String globalMessage) {
            this.localMessage = localMessage;
            this.globalMessage = globalMessage;
        }

        public String getLocalMessage() {
            return localMessage;
        }

        public void setLocalMessage(final String localMessage) {
            this.localMessage = localMessage;
        }

        public String getGlobalMessage() {
            return globalMessage;
        }

        public void setGlobalMessage(final String globalMessage) {
            this.globalMessage = globalMessage;
        }
    }
}

Here you can see that we are injecting two strings from the configuration properties.

The first is the local message from the app.local.message property and the second is from the app.global.message property. These properties are defined in our configuration service.

The app.global.message is defined in the global application.properties file. The app.local.message property is yet to be defined anywhere so let’s do that now.

In the ~/config-service-example-config directory add a new file named message-service.properties containing a single line:

message-service.properties

1
app.local.message=Hello World!

Commit the change back to Git:

$ git add message-service.properties
$ git commit -m "Add message-service properties file"

If you take another look at http://localhost:8888/message-service/dev you’ll find that it has picked up the new message-service.properties file:

 1 2 3 4 5 6 7 8 910111213141516171819202122
{
  "name": "message-service",
  "profiles": [
    "master"
  ],
  "label": null,
  "version": null,
  "propertySources": [
    {
      "name": "file:///Users/ian/config-service-example-config/message-service.properties",
      "source": {
        "app.local.message": "Hello World!"
      }
    },
    {
      "name": "file:///Users/ian/config-service-example-config/application.properties",
      "source": {
        "app.global.message": "Hello Global World!"
      }
    }
  ]
}

Note: Before you can build and run the message-service code. You’ll need to either remove the generated unit test class or add a src/main/resources/application.properties file containing the app.global.message and app.local.message properties such that the unit test can load the properties on the classpath.

Once you have built the code you can start up the application and point your browser at http://localhost:8080/message you should get the following JSON:

1234
{
  "localMessage": "Hello World!",
  "globalMessage": "Hello Global World!"
}

Live reload

So now we have the services running, how do we change the configuration? Do we have to restart the services?

The answer is no but we have to enable reloading on the REST endpoint.

This is easy to achieve by adding a simple annotation:

123456
@RefreshScope
@RestController
@SpringBootApplication
public class MessageServiceApplication {
  :
}

In order for this to work we also need to add the actuator Spring Starter project to the pom.xml file:

pom.xml
12345678
<dependencies>
  :
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  :
</dependencies>

If you now rebuild the Message Service and run it you should get the same as before when you hit the http://localhost:8080/message endpoint.

So let’s change that message in message-service.properties in our Git repository:

message-service.properties
1
app.local.message=Hello local world!

Don’t forget to commit the change!

If you take a look at the configuration service you’ll notice the message has indeed been updated.

 1 2 3 4 5 6 7 8 910111213141516171819202122
{
  "name": "message-service",
  "profiles": [
    "master"
  ],
  "label": null,
  "version": null,
  "propertySources": [
    {
      "name": "file:///Users/ian/config-service-example-config/message-service.properties",
      "source": {
        "app.local.message": "Hello Local World!"
      }
    },
    {
      "name": "file:///Users/ian/config-service-example-config/application.properties",
      "source": {
        "app.global.message": "Hello Global World!"
      }
    }
  ]
}

If you invoke the endpoint again you’ll notice that the message is still the old one. This is because we have to tell the service to refresh its configuration.

This can be done by invoking the /refresh endpoint on the Message Service via a HTTP POST. This can be done using curl at the command line:

curl -d {} http://localhost:8080/refresh

This simply instructs curl to send an empty HTTP POST to the /refresh resource on host localhost on port 8080.

If you now invoke the message endpoint again you’ll see that the message is now the new one.

1234
{
  "localMessage": "Hello Local World!",
  "globalMessage": "Hello Global World!"
}

More information on Spring Cloud Config can be found in the official documentation at http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html.