Spring Cloud Config — обновление конфигурации
Spring Cloud Config — обновление конфигурации

Spring Cloud Config позволяет хранить настройки конфигурации сервисов в git-репозитории и управлять настройками централизованно.

В этой статье поговорим об обновлении параметров, получаемых с сервера конфигурации.

1. Свойства Spring Boot-приложений

Для работы со свойствами из внешних источников, таких как property-файлы или сервер конфигурации, Spring использует аннотацию PropertySource. А значения свойств можно получать через интерфейс Environment.

Свойства Spring Boot-приложений
Свойства Spring Boot-приложений

Как показано на приведенной выше диаграмме, приложение при старте получает источники свойств с сервера конфигурации и помещает их в начало списка доступных источников (PropertySources). Чем выше в этом списке, тем приоритет больше. Источников свойств, на самом деле, больше, чем показано на диаграмме, но, суть в том, что свойства из bootstrap-контекста, обычно приоритетнее локальных property-файлов.

После изменения значений в источниках, свойства инжектируются в бины при инстанцировании. Для этого используются аннотации @Value или @ConfigurationProperties.

2. Обновление свойств

Для обновления свойств в контексте приложения нужно выполнить те же два этапа: обновить источники свойств в Environment и атрибуты бинов.

Свойства клиентов Spring Cloud Config Server можно обновить следующими способами:

  • вызовом эндпоинта клиента /actuator/refresh, предоставляемого Spring Actuator;

  • вызовом эндпоинта клиента /actuator/bus-refresh, при интеграции со Spring Cloud Bus;

  • вызовом эндпоинта сервера конфигурации /monitor, интегрированного со Spring Cloud Bus.

3. Использование /actuator/refresh

Эндпоинт /actuator/refresh привязан к клиенту, поэтому обновляется только клиент, к которому будет сделан запрос.

На диаграмме ниже показан процесс обновления для этого случая:

Spring Actuator Refresh
Spring Actuator Refresh
  1. Например, мы изменяем property-файл и комитим его в git-репозиторий. Далее нам необходимо отразить эти изменения в нашем приложении.

  2. Для этого выполняем запрос к /actuator/refresh

curl -H "Content-Type: application/json" -d {} http://localhost:8080/actuator/refresh

3. Этот вызов инициирует обращение клиента к серверу конфигурации и получение последней версии свойств.

4. Далее обновляются источники свойств (PropertySources) в Environment и атрибуты бинов, аннотированных @RefreshScope.

5. Сразу после этого генерируется событие EnvironmentChangeEvent, на которое можно подписаться и обработать при необходимости.

3.1 Добавляем зависимость для Actuator

В pom.xml нашего клиента добавляем следующую зависимость.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

3.2 Настраиваем эндпоинты Actuator

По умолчанию большинство эндпоинтов Actuator отключены. Включенные эндпоинты можно посмотреть, обратившись по адресу /actuator. А сделать доступными все, включая /actuator/refresh, следующим образом:

management.endpoints.web.exposure.include=*

Включить эндпоинты выборочно можно, разделив их запятыми:

management.endpoints.web.exposure.include=refresh, bus-refresh, beans, env

3.3 @RefreshScope

После перезагрузки источников свойств через вызов /actuator/refresh, в бинах с аннотацией @RefreshScope происходит обновление атрибутов с @Value.

Для обновления свойств, объявленных с аннотацией @ConfigurationProperties, не требуется @RefreshScope.

Также эта аннотация не требуется для получения свойств напрямую из интерфейса Environment.

@Service
@Getter @Setter
@RefreshScope
public class MyService {

	@Value("#{'yes'.equals('${activated:false}')}")
	private Boolean activated;
}

3.4 EnvironmentChangeEvent

Иногда какой-то параметр конфигурации вычисляется в пост-конструкторе на основе других свойств. В этом случае, помимо обновления атрибута, аннотированного @Value, также необходимо выполнить это вычисление. Это можно сделать, обработав событие EnvironmentChangeEvent, как показано в примере ниже:

@Service
@RefreshScope
@Getter @Setter
public class RefreshScopedService {

	@Value("${tps}")
	Integer tps;
	
	String responseMessage;

	@PostConstruct
	public void refresh(){
		responseMessage = responseMessage = "Service is running with tps: " + tps;
	}
}

@Service
public class MyRefreshListener implements ApplicationListener<EnvironmentChangeEvent> {
	
	@Autowired RefreshScopedService refreshScopedService;

	@Override
	public void onApplicationEvent(EnvironmentChangeEvent event) {
		
		if(event.getKeys().contains("tps")) {
			refreshScopedService.refresh();
		}
	}
}

Для обновления свойств одного клиента вызов эндпоинта refresh прост и удобен. Однако, как правило, сервисов бывает несколько, и ручной вызов этого эндпоинта для каждого из них может быть трудоемким.

Один из вариантов решения этой проблемы — автоматизировать процесс с помощью bash-скриптов. Но есть и альтернативное, лучшее решение, предоставляемое еще одним проектом Spring Cloud — Spring Cloud Bus.

4. Обновление через Spring Cloud Bus

Spring Cloud Bus позволяет уведомлять сервисы об изменении состояния через брокеры сообщений, такие как Kafka или RabbitMQ.

То есть клиенты могут обмениваться информацией через брокер сообщений и обновлять себя в случае изменений конфигурации. Нам не нужно делать запрос на обновление каждого клиента — мы просто отправляем событие обновления через Spring Cloud Bus.

Давайте рассмотрим следующую диаграмму:

Процесс обновления с использованием Spring Cloud Bus
Процесс обновления с использованием Spring Cloud Bus

Процесс обновления может быть инициирован вызовом на клиенте /actuator/bus-refresh или /monitor на сервере конфигурации.

Сообщение об изменении состояния передается сервисам через брокер сообщений, и каждый сервис при получении события делает запрос к серверу конфигурации.

Эндпоинт сервера /monitor предназначен для облачных решений git, таких как Github, для создания push-уведомлений через веб-хуки git. Но можно инициировать событие обновления и вручную, отправив POST-запрос к этому эндпоинту.

4.1 Настройка Spring Cloud Config Server

Мы будем использовать Kafka в качестве брокера сообщений, поэтому добавляем соответствующую зависимость:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>

Для доступности эндпоинта /monitor добавляем на сервере конфигурации, следующую зависимость:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-monitor</artifactId>
</dependency>

И, наконец, настраиваем Spring Cloud Bus и Kafka в bootstrap.properties:

spring.cloud.bus.enabled = true
spring.cloud.bus.id=my-config-server
spring.cloud.stream.kafka.binder.zkNodes=localhost:2181
spring.cloud.stream.kafka.binder.brokers=localhost:9092

Это все, что нужно сделать для подключения сервера конфигурации к брокеру сообщений.

Подытожим, что мы сделали:

  1. Мы настроили сервер конфигурации на отправку сообщений об изменении состояния через Kafka.

  2. На сервере конфигурации настроили эндпоинт /monitor, который отправляет сообщение об изменении состояния через брокер сообщений всем клиентам.

Далее настроим клиентскую часть.

4.2 Настройка клиента Spring Cloud Config

Мы хотим, чтобы наши сервисы получали сообщения об обновлении конфигурации через брокер сообщений. Для этого в pom.xml клиента также добавляем зависимость Spring Cloud Bus:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>

И аналогично настраиваем Kafka:

spring.cloud.bus.enabled = true
spring.cloud.stream.kafka.binder.zkNodes=localhost:2181
spring.cloud.stream.kafka.binder.brokers=localhost:9092

Теперь наши клиенты готовы к обновлению с помощью Spring Cloud Bus.

4.3 Обновление через /actuator/bus-refresh

Мы настроили взаимодействие сервера конфигурации и клиентов через Spring Cloud Bus и обновление с помощью специальных эндпоинтов. Одним из них является bus-refresh, предоставляемый Spring Cloud.

Его мы уже настроили. Когда изменяется свойство конфигурации, мы можем просто вызвать /actuator/bus-refresh на любом из клиентов.

curl -X POST http://localhost:8080/actuator/bus-refresh

При этом происходит следующее:

  1. Клиент отправляет запрос на сервер конфигурации и обновляет себя.

  2. Далее клиент отправляет событие обновления другим сервисам через брокер сообщений.

  3. Сервисы, получившие событие обновления, также отправляют запрос на сервер конфигурации для получения актуальных настроек.

Обратите внимание, что, в отличие от /actuator/refresh, эндпоинт /actuator/bus-refresh не нужно вызывать для каждого клиента.

4.4 Обновление через /monitor

Есть еще один эндпоинт для обновления конфигурации через Spring Cloud Bus — /monitor. Вот некоторые его особенности:

  • Находится на сервере конфигурации и предоставляется spring-cloud-config-monitor.

  • Предназначен в основном для использования совместно с веб-хуками git и поддерживает облачные решения git, такие как Github и Bitbucket.

  • Инициировать событие обновления можно и вручную.

Пришло время эксперимента, давайте запустим пример и посмотрим, что из этого получится.

4.5 Запуск кода

  1. Склонируйте код из Github-репозитория: https://github.com/erolhira/spring-cloud-config 

  2. Установите и запустите Kafka.

  3. Запустите сервер конфигурации:

erol@ehira:~/.../case3_config-server-bus$ ./mvnw spring-boot:run

4. Проверьте работу сервера конфигурации, введя в браузере следующий адрес. http://localhost:8888/myconfig-client-app/cluster2,instance01/master

Вы увидите свойства, полученные из репозитория, который указан в bootstrap.properties:

Для изменения свойства создайте свой репозиторий или сделайте копию моего репозитория.

spring.cloud.config.server.git.uri = https://github.com/erolhira/config-repo-sample.git
Проверка работы Config Server
Проверка работы Config Server

5. Запустите клиента:

erol@ehira:~/.../case3_config-client-bus$ ./mvnw spring-boot:run

6. Проверьте клиента, введя в браузере следующий URL-адрес: http://localhost:8080/config 

Вы увидите свойства, полученные с сервера конфигурации в соответствии с профилем и именем приложения в bootstrap.properties.

Тестирование работы клиента
Тестирование работы клиента

7. Теперь запустите второй экземпляр клиента на другом порту:

erol@ehira:~/.../case3_config-client-bus$ ./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"

8. Измените значение prop-3 в myconfig-client-app.properties в репозитории.

9. Проверьте работу клиента так же, как в пункте 6. Изменений вы не увидите.

10. Для обновления клиентов вызовите эндпоинт /monitor на сервере конфигурации.

curl --location --request POST 'http://localhost:8888/monitor' --header 'X-Github-Event: push' --header 'Content-Type: application/json' --data-raw '{"commits": [{"modified": ["myconfig-client-app.properties"] }]}'

11. Этот вызов заставит сервер конфигурации отправить событие обновления через брокер сообщений. Сообщение, отправляемое через Kafka, выглядит следующим образом:

{
   "type":"RefreshRemoteApplicationEvent",
   "timestamp":1585513056017,
   "originService":"my-config-server",
   "destinationService":"myconfig-client-app-instance01:**",
   "id":"a5ede7cd-9c33-450a-938d-b414e9271c46"
}

12. Клиенты получат это сообщение и отправят подтверждение на сервер конфигурации через брокер. Сообщение выглядит следующим образом:

{
   "type":"AckRemoteApplicationEvent",
   "timestamp":1585513056471,
   "originService":"myconfig-client-app-instance01",
   "destinationService":"**",
   "id":"fcd5737d-1685-433e-a669-dc5ab2bc9540",
   "ackId":"a5ede7cd-9c33-450a-938d-b414e9271c46",
   "ackDestinationService":"myconfig-client-app-instance01:**",
   "event":"org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent"
}

13. Далее клиент обращается к серверу конфигурации и обновляется.

14. Как вы заметили в пункте 11, тип события, отправляемый сервером конфигурации, — RefreshRemoteApplicationEvent. На момент получения клиентом этого события процесс обновления еще не запущен. Поэтому если клиент в обработчике события прочитает свойство конфигурации, то получит старое значение свойства. То есть, в отличие от EnvironmentChangeEvent, мы можем выполнить некоторую предварительную обработку перед обновлением данных конфигурации. Вы также можете увидеть это в приведенном примере.

15. Мы обращались к эндпоинту /monitor вручную, но в реальной жизни, скорее всего, вы будете вызывать его через веб-хуки git при push в репозиторий с конфигурацией. Или, может быть, более подходящим вариантом будет вызов /actuator/bus-refresh клиента. Это уже зависит от вашей конкретной ситуации.

5. Примеры кода

Примеры кода вы можете найти в репозитории https://github.com/erolhira/spring-cloud-config

6. Ссылки

https://docs.spring.io/spring-cloud-config/docs/2.2.5.RELEASE/reference/html/
https://cloud.spring.io/spring-cloud-static/spring-cloud-bus/2.2.2.RELEASE/reference/html/
https://cloud.spring.io/spring-cloud-static/Hoxton.RELEASE/reference/html/spring-cloud-hoxton-configprops.html


Материал подготовлен в рамках курса «Разработчик на Spring Framework».

Комментарии (7)


  1. xeeaax
    22.11.2021 17:45
    +1

    Вопрос коллективному разуму - хорошо ли в базовом случае (когда не требуется менять параметры "на лету") использовать для передачи конфигурации Spring Cloud Config, если живешь в k8s+helm? Не является ли это лишним усложнением?


  1. stgunholy
    23.11.2021 02:11

    У меня тоже вопрос коллективному разуму :) Есть идея использовать Hashicorp Vault, чтобы туда вынести все настройки spring boot приложения, а не только токены/пароли... Так вообще можно делать? Или не стоит?


    1. grossws
      23.11.2021 14:47
      +1

      Более стандартным подходом будет использовать для конфигурации kv в consul (который часто используется совместно с vault) или etcd. IIRC, в spring cloud был стартер и для получения конфигурации из консула, и для регистрации в его service discovery.


      1. stgunholy
        23.11.2021 14:53

        А не получится масло-масляное? Spring умеет vault... И vault уже есть и работает... Консул необходим как компонент, или можно обойтись?


        1. grossws
          24.11.2021 23:28
          +1

          А поверх какого хранилища у вас работает vault? Наверняка же consul/etcd или подобная HA KV-store.

          В принципе никто не запрещает хранить в нём и конфигурацию, не уверен что есть готовый property source. Получите больше нагрузки на vault, потенциальный геморрой с бутстрапом, хотя может и небольшой. Ну и можно упереться в размеры значений в самом vault, не помню есть ли там жёсткие лимиты. Ещё возможно посложнее будет если хочется делать property refresh. А так идея имеет право на жизнь.


          1. stgunholy
            24.11.2021 23:39

            kv2 используется, я тут только потребитель пока, да и сервисов кому это надо всего 12 шутк :) На самом деле спасибо большое за развернутый комментарий. Попробуем POC сделать, посмотреть


            1. grossws
              25.11.2021 00:29

              Если я правильно вас понял, то kv2 -- это со стороны клиента vault. Вопрос был про бэкенд где сам vault хранит своё состояние. Возможно вы используете integrated storage про который я совсем забыл.

              Здесь есть информация о максимальных размерах storage entry. Начиная с 1.5.3 их можно увеличить, но вполне возможно что вам хватит 512k для конфигурации.