1. Overview
In this article, we’re going to look at the new Spring Cloud Bus project. Spring Cloud Bus uses lightweight message broker to link distributed system nodes. The primary usage is to broadcast configuration changes or other management information. We can think about it as a distributed Actuator.
The project uses AMQP broker as the transport, but Apache Kafka or Redis can be utilized instead of RabbitMQ. Other transports are not supported yet.
Over the course of this tutorial, we’re going to use RabbitMQ as our main transport – which we’ll naturally have running already.
2. Prerequisites
Before we begin, it’s recommended to have already completed “Quick Intro to Spring Cloud Configuration“. We’re going to take an existing cloud config server and client to extend them and add automatic notifications about configuration changes.
2.1. RabbitMQ
Let’s start with RabbitMQ, which we recommend running as RabbitMQ as a docker image. This is quite simple to set up – to get RabbitMQ running locally, we need to install Docker and run following commands once Docker is installed successfully:
docker pull rabbitmq:3-management
This command pulls RabbitMQ docker image together with management plugin installed and enabled by default.
Next, we can run RabbitMQ:
docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management
Once we executed the command, we can go to the web browser and open http://localhost:15672, which will show the management console login form. The default username is: ‘guest’; password: ‘guest’. RabbitMQ will also listen on port 5672.
3. Adding Actuator to Cloud Config Client
We should have cloud config server and cloud config client both running. To refresh configuration changes a restart of the client is required every time – which is not ideal.
Let’s stop config client and annotate ConfigClient controller class with @RefreshScope:
@SpringBootApplication
@RestController
@RefreshScope
public class SpringCloudConfigClientApplication {
// Code here...
}
Finally, let’s update the pom.xml file and add Actuator:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
The latest version can be found here.
By default, all sensitive endpoints added by the actuator are secured. This includes ‘/refresh’ endpoint. For simplicity, we will turn off security by updating application.yml:
management:
security:
enabled: false
Additionally, starting with Spring Boot 2, actuator endpoints are not exposed by default. To make them available for access, we need to add this in an application.yml:
management:
endpoints:
web:
exposure:
include: "*"
Let’s start the client first and update user role from existing ‘Developer’ to ‘Programmer’ in the properties file on GitHub. Config server will show updated values straight away; however, the client won’t. To make client see new files we just need to send an empty POST request to ‘/refresh’ endpoint, which was added by actuator:
$> curl -X POST http://localhost:8080/actuator/refresh
We will get JSON file back showing updated properties:
[
"user.role"
]
Finally, we can check if the user role was updated:
$> curl http://localhost:8080/whoami/Mr_Pink
Hello Mr_Pink! You are a(n) Programmer and your password is 'd3v3L'.
The user role was updated successfully and by calling ‘/refresh’ endpoint. Client updated configuration without restarting.
4. Spring Cloud Bus
By using Actuator, we can refresh clients. However, in the cloud environment, we would need to go to every single client and reload configuration by accessing actuator endpoint.
To solve this problem, we can use Spring Cloud Bus.
4.1. Client
We need to update cloud config client so that it can subscribe to RabbitMQ exchange:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
<version>4.1.1</version>
</dependency>
The latest version can be found here.
To complete config client changes we need to add RabbitMQ details and enable cloud bus in an application.yml file:
---
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
cloud:
bus:
enabled: true
refresh:
enabled: true
Please note that we are using default username and password. This needs to be updated for real life, production applications. For this tutorial this is fine.
Now, the client will have another endpoint ‘/bus-refresh’. Calling this endpoint will cause:
- get the latest configuration from the config server and update its configuration annotated by @RefreshScope
- send a message to AMQP exchange informing about refresh event
- all subscribed nodes will update their configuration as well
This way, we don’t need to go to individual nodes and trigger configuration update.
4.2. Server
Finally, let’s add two dependencies to config server to automate configuration changes fully.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-monitor</artifactId>
<version>4.1.1</version>
</dependency>
The latest version can be found here.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<version>4.1.1</version>
</dependency>
The most recent version can be found here.
We will use spring-cloud-config-monitor to monitor configuration changes and broadcast events using RabbitMQ as transport.
We just need to update application.properties and give RabbitMQ details:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
4.3. GitHub Webhook
Everything is set now. Once the server gets notified about configuration changes, it will broadcast this as a message to RabbitMQ. The client will listen to messages and update its configuration when configuration change event is transmitted. However, how a server will now about the modification?
We need to configure a GitHub Webhook. Let’s go to GitHub and open our repository holding configuration properties. Now, let’s select Settings and Webhook. Let’s click on Add webhook button.
Payload URL is the URL for our config server ‘/monitor’ endpoint. In our case the URL will be something like this:
http://root:s3cr3t@REMOTE\_IP:8888/monitor
We just need to change Content type in the drop-down menu to application/json. Next, please leave Secret empty and click on Add webhook button – after that, we are all set.
5. Testing
Let’s make sure all applications are running. If we go back and check client it will show user.role as ‘Programmer’ and user.password as ‘d3v3L‘:
$> curl http://localhost:8080/whoami/Mr_Pink
Hello Mr_Pink! You are a(n) Programmer and your password is 'd3v3L'.
Previously, we had to use ‘/refresh’ endpoint to reload configuration changes. Let’s open properties file, change user.role back to Developer and push the changes:
user.role=Programmer
If we check the client now, we will see:
$> curl http://localhost:8080/whoami/Mr_Pink
Hello Mr_Pink! You are a(n) Developer and your password is 'd3v3L'.
Config client updated its configuration without restarting and without explicit refresh almost simultaneously. We can go back to GitHub and open the recently created Webhook. At the very bottom, there are Recent Deliveries. We can select one on the top of the list (assuming this was the first change – there will be only one anyway) and examine JSON that has been sent to config server.
We can also check config and server logs, and we will see entries:
o.s.cloud.bus.event.RefreshListener: Received remote refresh request. Keys refreshed []
6. Conclusion
In this article, we took existing spring cloud config server and client and added actuator endpoint to refresh client configuration. Next, we used Spring Cloud Bus to broadcast configuration changes and automate client updates. We also configured GitHub Webhook and tested the whole setup.
As always, the code used during the discussion can be found over on GitHub.