Refreshing configuration at Runtime with Spring Cloud Bus and Azure Service Bus
One thing that never fails to appear when I discuss Cloud Native development is externalizing the configuration. In 12factor App, there is a chapter, "III. Config" described as a "strict separation of config from code." Spring Cloud Config Server was first introduced in 2014, which is a perfect solution for externalizing configuration from the source code. Since its inception, it is still the most popular Spring Cloud component among Spring developers, according to the survey published by Microsoft in 2021. I highly recommend reading the entire post from Asir that has a lot of insights on the usage of Spring on the cloud.
If you're not familiar with Spring Cloud Config Server, Spring documentation (as always) offers a concise explanation.
"Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. The concepts on both client and server map identically to the Spring Environment
and PropertySource
abstractions, so they fit very well with Spring applications, but Config Server can also be used with any application running in any language."
In a nutshell, Config Server is serving configuration(key-value pair) in REST API, and config client is GETing it upon startup to populate Environment
and PropertySource
in the Spring container. This aspect of using a standard HTTP interface opens the door for other languages like .NET, etc. Here is the example of Steeltoe using Config Server with .NET application.
In this article, the primary focus will be on the usage of Spring Cloud Bus with Azure Service Bus, so the basics of Spring Cloud Config won't be covered in detail. If you're entirely new to Spring Cloud Config, I would recommend you pause here and read the basics from the official Spring post before you move on. I choose Azure Spring Apps for app deployment to leverage its superior PaaS experiences for Spring applications. Even though this article is based on Azure Spring Apps, it has no platform dependency meaning it can run on any Azure platform like AKS, App Service, and Container Apps.
NOTE: Azure Spring Cloud is officially renamed to Azure Spring Apps at Microsoft Build 2022.
Create Config Server and Config Client application
Let's start with the Config client application. We need a series of dependencies like Web, Actuator, Config Client, and Cloud Bus. I named the application as config-client
.
Create a RestController, HelloRestController
returning the message from PropertySource. This message value will come from Config Server.
package io.jaylee.cloudbus.configclient;
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.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
@RestController
@RefreshScope
class HelloRestController {
@Value("${message}")
String message;
@GetMapping("/hello")
public String sayHello() {
return message;
}
}
The config-client
application is ready. The next thing is to configure Config Server. Since Config Server is provided out of the box by Azure Spring Cloud, all I need is the Git repository to store my configuration. Go to the Azure portal, and put in the Git repository URI and authentication if there is any. For the reference, I will be using my public repository, https://github.com/eggboy/cna-config-repo
In the Git repository, I have application.yml
that will be served to every application connected to the Config Server. I made it very clear that the message is from Github.
Both Config Server and client are ready. Since we have Config Client
in the pom.xml, Azure Spring Apps will help to integrate the config server with our sample app upon deployment.(Using spring.cloud.config.uri
at runtime) Run az spring-cloud app deploy
to deploy our app as a source code.
$ az spring-cloud app create -n cloud-bus-app -s asc-standard -g asc-rg --runtime-version=Java_17 --assign-endpoint true
This command usually takes minutes to run. Add '--verbose' parameter if needed.
[1/3] Creating app cloud-bus-app
[2/3] Creating default deployment with name "default"
[3/3] Updating app "cloud-bus-app" (this operation can take a while to complete)
App create succeeded
......$ pwd
/Users/jaylee/Desktop/config-client$ ls
HELP.md mvnw mvnw.cmd pom.xml src$ az spring-cloud app deploy -n cloud-bus-app -s asc-standard -g asc-rg --source-path .
This command usually takes minutes to run. Add '--verbose' parameter if needed.
[1/3] Requesting for upload URL.
[2/3] Uploading package to blob.
[3/3] Updating deployment in app "cloud-bus-app" (this operation can take a while to complete)
Trying to fetch build logs
......
If everything goes fine, config-client
should be up and running. Run az spring-cloud app show
to retrieve the URL of a new app on Azure Spring Apps and access it using curl or any internet browser.
$ az spring-cloud app show -n cloud-bus-app -s asc-standard -g asc-rg --query properties.url
"https://asc-standard-cloud-bus-app.azuremicroservices.io"
$ curl https://asc-standard-cloud-bus-app.azuremicroservices.io/hello
Config from application.yml on Github
There is no doubt from the curl output that the configuration is from the Github repository. Suppose you're curious about where the properties are coming from as they can come from various sources like property files, environmental variables, etc., Spring Actuator provides the details of PropertySource that will give you a clear answer. Exposing details requires one property, management.endpoints.web.exposure.include
. As Config Server is there already, adding it to the Git repository would make sense. Commit the property change on Git and restart an app to get the new configuration.
As we expose the entire("*) Actuator endpoint in the configuration, you will be able to access /actuator/env
and see the source of each property.
Refreshing configuration at runtime with Spring Cloud Bus and Azure Service Bus
Before I go into the details of Spring Cloud Bus, I'd like to revisit how refresh works with Spring Cloud Config. Any configuration changes in the Git repository with Spring Cloud Config don't trigger the refresh of configuration on apps at runtime. To refresh the configuration at runtime, you need to trigger the endpoint(/actuator/refresh
) manually for every single app. For example, if you have 100 apps running, you should POST 100 times to different endpoints. That would not be a trivial job, especially if you're running it on Kubernetes with an additional internal management port for Actuator. Spring Cloud Bus can be a lifesaver that can propagate the refresh event to all the running applications using a message queue, eliminating the manual refresh job. Here is the introduction from the official Spring documentation.
"Spring Cloud Bus links nodes of a distributed system with a lightweight message broker. This can then be used to broadcast state changes (e.g. configuration changes) or other management instructions. AMQP and Kafka broker implementations are included with the project. Alternatively, any Spring Cloud Stream binder found on the classpath will work out of the box as a transport."
Based on the description, three things are required to make Spring Cloud Bus work. Of course, the first thing is the dependency on Spring Cloud Bus, which we added config-client
earlier. Second is AMQP or Kafka broker, where Azure Service Bus comes into the picture. The third is the Spring Cloud Stream binder that works with Azure Service Bus. Unsurprisingly, Azure provides an excellent integration of Service Bus with Spring Cloud Stream by spring-cloud-azure-stream-binder-servicebus
dependency that is not available out of the box on Spring Initializr. We need to manually add it in our pom.xml
.
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-stream-binder-servicebus</artifactId>
<version>4.1.0</version>
</dependency>
Azure Service Bus is AMQP 1.0 compliant messaging solution with JMS 2.0 support, which makes it the best choice for Java developers who need a PaaS service for messaging queue on Azure. We will use Azure CLI to create a few resources, a namespace, a topic, and a subscription. This checks the box for the second above.
$ az servicebus namespace create --resource-group sandbox-rg --name cloudbus-test --location westus --sku Standard
...$ az servicebus topic create --resource-group sandbox-rg --namespace-name cloudbus-test --name springcloudbus
...$ az servicebus topic subscription create --resource-group sandbox-rg --namespace-name cloudbus-test --topic-name springcloudbus --name springcloudbus-subscription
...
The third one, Spring Cloud Stream binder setup with Azure Service Bus requires a bit of work, mainly the property to bind the Azure Service Bus. Take note of the namespace name and subscription name above, and check the sample configuration below.
spring:
application:
name: cloud-bus-app
cloud:
bus:
enabled: true
refresh:
enabled: true
trace:
enabled: true
azure:
servicebus:
connection-string: [REDACTED] #5
stream:
bindings:
springCloudBusInput: #1
destination: springcloudbus #2
group: springcloudbus-subscription #3
springCloudBusOutput:
destination: springcloudbus
servicebus:
bindings:
springCloudBusInput:
consumer:
auto-complete: false
springCloudBusOutput:
producer:
entity-type: topic #4
#1: Name of bindings used by Spring Cloud Bus. These are fixed as springCloudBusInput and springCloudBusOutput
#2: The name of the destination maps to the topic's name, which is the name of the Azure Service Bus topic.
#3: The name of the group maps to a consumer group which is basically an Azure Service Bus subscription
#4: Advise producer that target is a topic
#5: Connection string to connect to Azure Service Bus
This is all we need to bind Spring Cloud Stream with Azure Service Bus. I have one thing to add for #5. I'm sure you must not be comfortable having a connection string right inside your source code, and this can be easily avoided using Managed Identity. Check my previous article, Secure way to use Secrets with Java using Azure Key Vault and Managed Identity.
Test it on Azure Spring Apps
client-config
is ready for deployment. The whole purpose of writing this article is to show you how to refresh the configuration of a fleet of applications, so surely we need more than one application. For that, I will create one more app as cloud-bus-app2
.
$ pwd
/Users/jaylee/Desktop/servicebus/config-client$ az spring-cloud app create -n cloud-bus-app2 -s asc-standard -g asc-rg --runtime-version=Java_17 --assign-endpoint true
...$ az spring-cloud app deploy -n cloud-bus-app2 -s asc-standard -g asc-rg --source-path .
...
It’s all set for the test. I opened two tabs for cloud-bus-app
and cloud-bus-app2
. you can see the name of the URL below as "asc-standard-cloud-bus-app" and "asc-standard-cloud-bus-app2"
All we need to do is commit changes to the Github repository and refresh the configuration at runtime. To trigger the refresh to all the Cloud Bus connected instances, send one POST request to one of the instances. I POST it to cloud-bus-app2
below.
$ curl -X POST https://asc-standard-cloud-bus-app.azuremicroservices.io/actuator/busrefresh
Go back to the previous tabs, and refresh each page.
Both apps show the refreshed value of the message as "Need some fun!". This is working great. But if you're an inquisitive person like I am, you should wonder what is going on. Here is what's going on under the hood. The moment you trigger /actuator/busrefresh
endpoint, it enqueues the event in Azure Service Bus with this payload. It has the basic information on who created this event with originService, when with a timestamp, and destinationService.
{
"type": "RefreshRemoteApplicationEvent",
"timestamp": 1653442315150,
"originService": "cloud-bus-app:1025:d58f42abdbad0832c1722be54ecfd0e3",
"destinationService": "**",
"id": "aec4dfc5-33da-4293-a9de-61c424f0230d"
}
Each refresh event is acknowledged by services with AckRemoteApplicationEvent
.
{
"type": "AckRemoteApplicationEvent",
"timestamp": 1653442316258,
"originService": "cloud-bus-app2:1025:329dbed833014f492f699242fbef348a",
"destinationService": "**",
"id": "1778b410-2d55-4e5a-b6be-75aa09ccb966",
"ackId": "aec4dfc5-33da-4293-a9de-61c424f0230d",
"ackDestinationService": "**",
"event": "org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent"
}
Wrapping Up
Spring Cloud Bus and Spring Cloud Stream work hands-in-hand without much coding, giving excellent infrastructure to refresh the configuration at runtime without any downtime. Thanks to Azure Spring Apps, it doesn't take much effort to build and test it end to end.
For Java developers who don't want to have an additional overhead of having a Config Server, especially customers using App Service or AKS, I'm preparing the next article, "Managing Java configuration with Azure Application Config" Stay tuned!
Sample source code is at https://github.com/eggboy/SpringCloudBusAzureServiceBus
If you like my article, please leave some claps here or maybe even start following me. You can hit me up on Linkedin. Thanks!