Bootstrapping — Spring Cloud Config Server
This story has the step by step instructions on how to create a config server using Spring Cloud Config for externalizing application configurations. Follow these simple steps to stand up your first config server.
This entire piece of code is in github.
Config Server:
Assuming you have a Spring boot 2.0 app ready to bootstrap, let’s add the following dependencies.
- Maven dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
2. add the annotation @EnableConfigServer to your boot application class.
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
These above 2 will make your spring boot app to a Spring Cloud Server. Now let’s add the real stuffs into it and make it usable.
Config server can source the properties from 2 sources, a local file system (profile = native) and external source control, in our case it is github (profile = git) . There are other sources like bitbucket which are supported as well.
3) Creating profiles
Under the src/main/resources, lets add 2 application.yml files.
- application.yml — native profile
- application-git.yml — git profile
a) native profile : Native profile is the default profile of config server. This is used to source properties from local file system and in our case, I am pointing the location to the resources project of config server. So your application.yml would like something like this.
please note the search-locations: it points to project classpath:/config folder.
In this example project, I have a folder called “config” under resources folder with 2 subfolders. Take a look at my github code.
server:
port: 8762spring:
profiles:
active: native
cloud:
client.hostname: ${spring.cloud.client.ip-address:localhost}
config:
override-system-properties: false
server:
native:
search-locations:
classpath:/config/,
classpath:/config/client-config-first,
classpath:/config/client-discovery-first
b) git profile: Git profile sources properties from github source control. This is the preferred method to achieve auto refreshable configuration. So any changes made to the files in git “search-paths” is our area of interest.
server:
port: 8762spring:
profiles:
active: git
application:
name: config-server
cloud:
config:
override-system-properties: false
server:
git:
uri: https://github.com/athulravindran87/spring-cloud-config.git
clone-on-start: true
search-paths:
config-server/src/main/resources/config/,
config-server/src/main/resources/config/client-config-first,
config-server/src/main/resources/config/client-discovery-first
username: xxx
password: yyy
Config client:
Assuming you have a Spring boot 2.0 app ready to bootstrap, let’s add the following dependencies. I would name this app as client-config-first. There is a reason why I named it this way, we will see it in next chapter.
Maven dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency><dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
spring-cloud-starter-config is the library to let client apps connect to config server.
spring-retry and aop is required for fail fast and retry.
Configurations — Bootstrap.yml
spring:
application:
name: client-config-first
profiles:
active: local
cloud:
config:
uri: http://localhost:8762
fail-fast: true
retry:
max-attempts: 20
max-interval: 15000
initial-interval: 10000
server:
port: 8763
Let’s break down the above configurations
- spring.cloud.config.uri — this is the fixed url of your config server.
- spring.cloud.discovery.enabled — by default false. So I haven’t included it here.
- spring.cloud.fail-fast — true, if you want your service to fail and retry if config server is not available yet to connect.
- spring.cloud.retry.max-attempt — Maximum attempts this service would retry to connect to config.
- spring.cloud.retry.max-interval — interval time between every retry.
- spring.cloud.retry.initial-interval — interval time for the first retry.
Let’s test this so far
Test Case1: Bring up your config server and hit the following url in your browser. http://localhost:8762/client-config-first/local
You should see blank response but definitely a 2XX response.
Now bring up your config client. You should see in the logs your config client connecting to config server at the beginning of bootstrapping.
Test Case 2:
Shut down both.
Bring up the config client first, you should see config client unable to connect to server and it retries for the number of times defined in bootstrap yml with the intervals mentioned.
After a few retries, bring up the config server and now your client should be able to connect and come up.
Taking real advantage of Config Server
So now we have a bear-bone server and client configured but the client hasn’t taken any advantage of the server yet.
Let’s go back to the config server.
- create a new directory under resources folder and call it “config”.
- create a new file “application-local.yml” inside the config directory.
- You can include any common configuration that all of the clients will share. for example “hystrix” or eureka configurations.
- Create a new directory “client-config-first” within config directory.
- Create a new file “client-config-first.yml” within “client-config-first” directory.
so the directory structure should look like this.
Now let’s edit “client-config-first.yml” and add app specific properties
server:
port: 8763
test:
name: test name 1
phone: 123-456-7893
Now let’s go back to Client app and add some code to use these properties.
Let’s add 2 classes to the client app
@Component
@ConfigurationProperties("test")
@Data
@RefreshScope
public class TestConfig {
private String name;
private String phone;
}
The above is component and a configurationProperty bean with prefix “test” and will pick up properties test.name and test.phone from the client.yml.
A simple controller to inject TestConfig and print the values
@RestController
public class TestController {
@Autowired
private TestConfig testConfig;
@GetMapping(value = "/test")
public String getConfigData() {
return "Name: " + this.testConfig.getName() + " Phone: " + this.testConfig.getPhone();
}
}
So far so good.
Now restart both config server and config client and hit the url http://localhost:8763/test and you should see the values on your browser.
Now let’s test more…
Native profile testing:
Change the name or phone number to something else but do not push the change to git yet. Your client will still not have the new value. You will need to restart config server for it take the local change into effect.
Git profile testing:
Push the changes to git. Your client will still not have the new value. You will need to restart config server for it take the local change into effect.
How do we make this work without a restart ?
The answer is “/actuator/refresh” endpoint on the client. if the endpoint is hit on the client, it will force the server to reload the configuration and provide client with the new value.
is this a good solution ?? yes and no.. lets take a real time example
Say you have 5 services running 1 instance each and you need to refresh 2 of them. Refresh endpoint is a quick solution but what if you have 100 instances of 5 services?. No way…that’s when you need spring cloud bus and a messaging broker to achieve full automation.
How does the refresh work behind the scenes ??
Not all spring beans are refreshed. Only the beans that are annotated with @RefreshScope is reloaded with latest property values. For example: Let’s assume there are 2 spring rest controllers and they have @Value annotation few properties. 1 spring controller is annotated with @RefreshScope and other is not. When the refresh endpoint is hit from the client, the bean that is annotated is refreshed with new values from git and the other one has no effect. While you can annotate every single bean in your application with @RefreshScope but thats dirty !!!
How to use @RefreshScope to take effect on entire application ??
The best way to achieve this is by creating a ConfigurationBean for handling properties and inject the bean anywhere you want in the application. We already saw this bean earlier in the example.
@Component
@ConfigurationProperties("test")
@Data
@RefreshScope
public class TestConfig {
private String name;
private String phone;
}
@ConfigurationProperties(“test”) will take care of picking up properties test.name, test.phone from property file. I have @Component that makes it a bean by itself, @Data is to handle getters and setters using Lombok and finally @RefreshScope. Now there is only one bean in the entire app to handle properties starting with test. May be you can have few more configuration beans for different prefixes to handle this situation or group them under one common prefix to stick to one bean. But refreshing only configuration beans is much cleaner than having this annotation all over the code.
Please read other stories in this series for more information
- Spring Cloud Config server — Discovery First Vs Config First
- Spring Cloud Config Server — Auto Refresh using Apache Kafka in Kubernetes
- Spring Cloud Config Server — Work around for Auto Refresh
Thank You !!!