DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • How to Setup the Spring Cloud Config Server With Git
  • A New Era Of Spring Cloud
  • Externalize Microservice Configuration With Spring Cloud Config
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)

Trending

  • Modern Test Automation With AI (LLM) and Playwright MCP
  • Event-Driven Microservices: How Kafka and RabbitMQ Power Scalable Systems
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  • Start Coding With Google Cloud Workstations
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. Dynamic Configuration Management in Microservice Architecture With Spring Cloud

Dynamic Configuration Management in Microservice Architecture With Spring Cloud

Improve your own dynamic configuration management with Spring Cloud while learning how easy it is to reconfigure your application live without rebuilding or rebooting it.

By 
Bartłomiej Słota user avatar
Bartłomiej Słota
·
Feb. 05, 18 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
61.8K Views

Join the DZone community and get the full member experience.

Join For Free

In the scary world of monolithic applications, we succeeded in working out numerous design patterns and good practices. When we move towards distributed systems, we should apply them carefully, bearing in mind that in microservice infrastructure we need to deploy, scale, and reconfigure our systems much quicker. In this article I will focus on configuration issues, emphasizing why keeping properties/yml files with your codebase is good, and when it might not suffice. I will also give you an overview of how you can improve your configuration management with Spring Cloud, and how easy it is to reconfigure your application live without rebuilding or rebooting it.

Configuration Files


It seems to be obvious now to keep your configuration outside of the code so that anyone can update it without the struggle to find a proper line to be modified. However, I find it not so evident for people to build applications independently from the environments they are running on. Anyway, making your build environment-dependent is not a good idea, as you won't know what environment is a particular package supposed to be used for (unless you provide some crazy naming strategy for your artifacts, but don't tell me about them, I'm frightened enough while thinking of it). It should be a natural thing to create profiles related to your environments and split the config among profile-dependent .properties files (or separate them within the .yml file), building one profile-agnostic deployment package, passing a proper profile/config file to your application while running it. If you do so, you will see a light in the tunnel. But the light is dimmed as there is still a couple of things you should be aware of.

Please note that environment is not the only thing that may define the profile. As an example: in projects that I was working on, we had to deploy the same application for three different countries, where each country had its own configuration. Then we had 9 combinations of profiles: dev, test, prod for each of the three countries.

First of all, having config files bounded with the deployment package, you need to rebuild and redeploy it each time some property changes. It is a horror when your configuration changes more frequently than the code. Although you can always keep some of your configuration outside of the jar file, it will usually require rebooting your app after updating your properties there.

Moreover, having a distributed system, where scalability is one of its key features you need to be able to react to configuration changes quickly and apply them to all instances that are currently running. Redeploying them one by one can be a tough task if it is not properly automated.

It might also be a case that developers have restricted access to credentials to production databases, external services, etc. In one of the monolithic applications that I was working on, the production deployment required client's administrator to set particular properties in an external config file before starting the application. No one had any trace about what was changed there, when it happened and by whom. I hope you don't need any more arguments to see that this is a very bad idea to do so.

You can see now how complicated and confusing configuration issues might be. Microservices are by definition small independent applications realizing some business logic within a bounded context. Each microservice, before being deployed, must be a bundled unit that can be run on any desired environment: dev, test, prod, etc. without the need of rebuilding it for each of them. We should be able to scale and reconfigure them at will and keep all sensitive settings secured if needed. This is the moment when Spring Cloud Config comes to the rescue. But before that, let's have a look at our simple infrastructure example, that you can find on my GitHub account.

Example Overview


We have two simple microservices: Customer Manager and Product Manager, that realize some business logic regarding Customers and Products respectively. The access to both of our applications is available from the WWW only through the Gateway, which is an implementation of API Gateway pattern with the help of Zuul Router. Each application registers itself (we are using client-side service discovery) in Eureka Discovery Server, which maintains service registry. In our case, all microservices communicate with each other via HTTP protocol, and all of them are, of course, SpringBoot applications.


When we set up our environment we can take a look at Discovery Server dashboard, where we can see that all three microservices successfully registered themselves in Eureka. This, in turn, means that they will be able to communicate with each other without preconfiguring their exact URLs, but using application names to find them out from Eureka Server instead.



Spring Cloud Config


Having our example in place, and bearing in mind all configuration issues that we've discussed, let's get straight to the solution. Spring Cloud Config is a tool that provides both server and client-side support for managing configuration in a distributed environment in a centralized manner.


EnvironmentRepository


Config server can be treated as a remote property source. It can be backed by numerous storage providers, like Git (which is the default setting), SVN, or distributed file system. You can even have more than one storage at the same time. With Git we can easily manage configuration files for any application and any environment. Moreover, we gain access control, which means we can decide who can modify particular files (for example restricting access to production properties). We also have full traceability - we know who changed what, and when, and we can easily revert those changes.

The way we want to store our data is defined by  EnvironmentRepository interface:

public interface EnvironmentRepository {
  Environment findOne(String application, String profile, String label);
}


It tells us a lot about how things are organized on both config server and backing service. You can see that in order to get a particular environment, we need to locate it with the following variables:

  • application - application name
  • profile - application profile
  • label - additional parameter used by the backing service (branch name, tag, etc. in case of Git repository)

In order to get proper configuration clients can use following endpoints:

  • /{application}-{profiles}.[properties|yml|yaml|json] 
  • /{label}/{application}-{profiles}.[properties|yml|yaml|json] 
  • /{application}/{profile}[/{label}] 


Embedded Config Server


Benefiting from spring's convention over configuration approach, we can set up our Config Server in just a few steps. First of all, you need to declare following dependency:

org.springframework.cloud:spring-cloud-config-server


and mark the config class with @EnableConfigServer  annotation:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

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


We will also need to define some properties inside our application.yml  file, like below:

spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/bslota/config-repository.git
          clone-on-start: true
          search-paths: 'config/{application}'

server:
  port: 8888


You can see here that we set port number to 8888, which is a conventional and recommended setting. We also told our application to look for config files in a GitHub repository, and clone it right after the startup. Cloning might take a while increasing the booting time, but as soon as the server is up it will be able to serve configuration quickly from the very first request. There is also a search-paths property specified, which tells that config files for each application will be placed in proper folders under the config/directory. While configuring Git as backing service, you can use {application},{profile} and {label} placeholders, so that it fits your needs. Here we have a very basic configuration, where properties for all applications are being held in one common repository, but you could easily apply one repo per profile  or   one repo per application strategy by using  {profile} and  {application} placeholders in  spring.cloud.config.server.git.uri property respectively. More details you will find in documentation.

In order to keep the Config Server consistent with our existing infrastructure, we will register it in Eureka Discovery Server. Now our system looks like this:


Here is the shot from Eureka Server dashboard:


Config Server is now the central part of our infrastructure that is supposed to both store and serve configuration for all other microservices. Thus, you should always scale it properly, so that your system stays resilient.


Spring Cloud Config Client


Now that we have our Git repository in place, and we have our Config Server up and running. It's high time we start using it. Before I show you how to connect our applications with the server, I need to tell you a few words about so-called  bootstrap context. In a big short,  a bootstrap context in Spring Cloud applications is a parent for main application context and it is used for the purpose of loading and decrypting (if needed) properties from external sources. It is defined inbootstrap.yml file unless otherwise specified. What you should put in this file is information about how to locate config server, and properties that are necessary to properly identify environment there. Do you remember application, profile, and label placeholders? This is exactly what we need to pass to the server, and we can do it with following properties:

  • spring.cloud.config.name - by default it is equal to  spring.application.name property value
  • spring.cloud.config.profile  - by default equal to comma separated list of currently active profiles
  • spring.cloud.config.label  - its default value is valuated on the server side and is equal to master if Git is used as backing service

Having such well assumed defaults, all we need to set in  bootstrap.yml  is   spring.application.name property.

In order to connect to the config server, we need to declare following dependency:

org.springframework.cloud:spring-cloud-starter-config


As soon as we have it, we need to choose one of two available bootstrap strategies. First one (the default one) is config first bootstrap. It assumes that you define config server URI  explicitly in the bootstrap.yml file with the spring.cloud.config.uri property. In this setup, the bootstrap.yml file will have the following content:

spring:
  application:
    name: # microservice name
  cloud:
    config:
      uri: http://localhost:8888


The drawback of this strategy is that we cannot benefit from Config Server being registered in service discovery as we are defining its URI explicitly (this problem won't apply if we are using server-side service discovery). We can get rid of this problem by using discovery first bootstrap strategy. In this approach, our application will resolve config server's URI by its name with the help of service discovery. Of course, this option has its costs as well. In order to get the configuration, we need an extra network roundtrip as we need to communicate with discovery server. Now our bootstrap.yml file looks like this:

spring:
  application:
    name: # microservice name
  cloud:
    config:
      discovery:
        service-id: config-server

eureka:
  client:
    service-url:
      default-zone: http://localhost:8761/eureka/


Now we can easily move the configuration of our services to our GitHub repository.


Manual Refresh


Thanks to the actuator endpoints, we can easily trigger numerous actions on each of our applications. Sending POST request to the  /refresh endpoint we can tell the service to reload the bootstrap context (updating the environment both from remote and local property sources), rebinding  @ConfigurationProperties and log levels, and refreshing all beans annotated with@RefreshScope annotation.

To give some concrete examples, let's take a look at the Customer Manager service, which has the following property set in its application.yml file in remote property source:

"premium-email-suffix": "yahoo.com"


and a service class annotated with the @RefreshScope annotation, that depends on this property's value.

@Service
@RefreshScope
class CustomerService {

  private final CustomerRepository customerRepository;
  private final String premiumEmailSuffix;

  CustomerService(CustomerRepository customerRepository,
                  @Value("${premium-email-suffix}") String premiumEmailSuffix) {
    this.customerRepository = customerRepository;
    this.premiumEmailSuffix = premiumEmailSuffix;
  }

  List<Customer> findAll() {
     return customerRepository.findAll();
  }

  List<Customer> findAllPremium() {
    return findAll()
             .stream()
             .filter(it -> it.getEmail().endsWith(premiumEmailSuffix)).collect(toList());
  }

  // ... other methods   
}


The service is used by CustomerController, so we can easily check if our change, that we will apply in a minute, works as expected.

@RestController
@RequestMapping("/customers")
public class CustomerController {

  private final CustomerService customerService;

  public CustomerController(CustomerService customerService) {
    this.customerService = customerService;
  }

  @GetMapping
  public List<Customer> customers() {
     return customerService.findAll();
  }

  @GetMapping(params = "premium")
  public List<Customer> premiumCustomers() {
     return customerService.findAllPremium();
  }

  // ... other methods
}


Now if we send the following request:

curl http://localhost:8085/customers?premium


we get this response:

[
  {
    "id": 2,
    "name": "Steve Harris",
    "email": "steve@yahoo.com"
  }
]


Let's make an update to the premium-email-suffix property so that its value is now equal to gmail.com, commit it, and push into the remote repository. As we had mentioned before, in order to make it visible by our application, we need to send the following request:

curl -X POST http://localhost:8085/refresh


And we can see the updated properties straight away in the response body:

["config.client.version","premium-email-suffix"]


To be 100% sure that the manual update worked, let's try to get all premium customers again hoping that this time their emails will end with gmail.com

[
  {
    "id": 1,
    "name": "Bruce Dickinson",
    "email": "bruce.dickinson@gmail.com"
  }
]


Note that similar behavior would be observed with @ConfigurationProperties components.

You see, it is easy, but if you have several services scaled up to a couple of instances it can make the manual refreshment a terrible experience. You need to find a proper service url and make sure that you performed the refresh to all instances. We can surely agree that it is not something that we would eagerly do. Fortunately, we can automate this process. Read on.


Dynamic Changes Propagation


The desired scenario would be that all services, whose properties get updated within a push into a remote repository, get notified and refreshed automatically. Thus, first of all, we need to find a way to propagate configuration changes to proper services. To solve this problem we will use Spring Cloud Bus, which was built to propagate management instructions. It makes use of both actuator (by adding new management endpoints) and Spring Cloud Stream (by enabling communication with AMQP message brokers). In our case, we will use Apache Kafka as a message broker, and now the desired architecture would look like this:


In order to enable Spring Cloud Bus and connect to Kafka broker, Gateway, Customer Manager, and Product Manager services need to be altered with following dependency (don't worry, this one has a transitive dependency on spring-cloud-bus):

org.springframework.cloud:spring-cloud-starter-bus-kafka


and configure Zookeeper nodes and Kafka binders (unless you are fine with defaults)

spring:
  cloud:
    stream:
      kafka:
        binder:
          zkNodes: "localhost:2181"
          brokers: "localhost:9092"


Right after the startup, our services will connect to the springCloudBus topic.

Now our applications are ready to listen and react to refresh requests coming from the message broker and determine if a particular event is dedicated to them or not.

In order enable Config Server to publish this kind of events (RefreshRemoteApplicationEvent, to be specific), we need to declare two dependencies:

 org.springframework.cloud:spring-cloud-config-monitor
 org.springframework.cloud:spring-cloud-starter-bus-kafka


and configure Zookeeper nodes and Kafka binders in the same way as we did in the other services:

spring:
  cloud:
    stream:
      kafka:
        binder:
          zkNodes: "localhost:2181"
          brokers: "localhost:9092"


You already know what  spring-cloud-starter-bus-kafka is used for. The second dependency, though,  spring-cloud-config-monitor adds something more - the  /monitor endpoint that accepts POST requests with information about what service needs to be refreshed. If this request gets to Config Server it publishes  RefreshRemoteApplicationEvent to a message broker. Then the target application consumes it and performs refreshment. Yep, you guessed - we can create webhooks that call this endpoint whenever anything changes in Git! Now here we have a few options. The first one is a simple form-based request:

curl -X POST localhost:8888/monitor -d 'path=customer-manager'


The remaining options are Git provider specific. In GitHub, for example, the request might be of the following form:

curl -X POST localhost:8888/monitor 
  | -H "X-Github-Event: push"
  | -H "Content-Type: application/json"
  | -d '{"commits": [{"modified": ["customer-manager.yml"] }]}'


If you are interested in details about how to build Github Push Webhooks, please visit this page.


Summary


In this article, you got an overview of how complex configuration issues may be. I gave you an explanation of why keeping configuration in external files is good, and when it might not be enough. You saw an evolving microservice system which we improved step-by-step by adding advanced configuration management with the use of spring-cloud-config and Git as a backing service. You can also see that a simple setup with manual refresh ability is fairly easy to provide, but in a much bigger architecture where your services are scaled up to a couple of nodes, you simply won't be able to control it. Then, a good solution would be to use a message broker like Apache Kafka and spring-cloud-bus with a proper broker abstraction (spring-cloud-stream) on the client side, and spring-cloud-config-monitor on the server side. If you add a proper Webhook to your Git platform, everything will work automatically as a commit to a proper repository/branch gets pushed.


Further Considerations


When you read articles like this, you should always keep in mind that software development is about trade-offs before you apply it in your infrastructure. You need to know that keeping configuration in a central place is helpful when it comes to maintenance, traceability, and all the stuff that we discussed at the very beginning, but it complicates the CI and CD processes as you have a second place where you keep things connected with the deployed application. Another thing is that you need to plan how to organize your Git repository and decide if you want to have a repository per application, or maybe one common repository with application-dependent folders and branches that will apply to profiles - there are lots of strategies and it is up to you to pick the one that will suit you best. If you need some more information about spring-cloud-config, I recommend visiting this page. Cheers!

Spring Cloud microservice Spring Framework application Configuration management Property (programming) kafka Git Architecture

Published at DZone with permission of Bartłomiej Słota, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Setup the Spring Cloud Config Server With Git
  • A New Era Of Spring Cloud
  • Externalize Microservice Configuration With Spring Cloud Config
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!