프로젝트/기술적 선택

[기술적 선택] Spring Cloud Config & Spring Cloud Bus

배발자 2023. 9. 9.
반응형

개요

분산 시스템인 MSA 환경에서 구동되는 각 애플리케이션 서버(Spring Boot 기준)들의 설정 정보들은 application.yml에 기본적으로 세팅하게 된다. 만약, 이러한 설정 정보들을 변경하고 서버에 적용하기 위한 가장 간단한 방법은 서버를 다운시켰다가 재가동하면 된다. 쉽게 말하면, 인텔리제이를 활용하여 프로젝트를 많이 진행할텐데 application.yml 파일을 수정했을 때 해당 설정 정보들을 적용하기 위해서 인텔리제이의 우측 상단에 위치한 재생 버튼을 클릭하여 서버를 껐다가 다시 실행하면 된다는 것이다. 스프링 개발 경험이 있다면 남 얘기가 아니기 때문에 모두 경험해봤을 것이라고 생각한다.  

 

필자 또한 프로젝트를 진행하면서 데이터베이스의 정보들이나 로깅 레벨을 설정하는 등 애플리케이션 서버들의 설정 정보를 수정할 때마다 서버를 재가동시켰다. 로컬에서의 이러한 방식은 가장 간편하니까 크게 문제될 것은 없지만, 실제 프로덕션(배포) 환경에서는 설정 정보의 변경 때문에 서버를 다운하고 다시 가동시키는 것은 리소스 낭비뿐만 아니라 Down Time도 고려하게될 것이다. 

 

그렇다면 각 어플리케이션의 설정 파일이 변경되었을 때 어떻게 유연하게 적용할 수 있을지 의문이 들었다. Spring과 관련된 학습을 하다보면 아직도 내가 모르는 새로운 기술들이 있었구나 생각이 들때가 많은데 오늘 포스팅할 주제가 바로 설정 파일과 관련된 새로운 학습 내용을 가지고 포스팅할 예정이다.

 

이전 포스팅에서 유레카와 게이트웨이에 대한 기본적인 내용을 정리를 했었다. 오늘은 그때의 내용에 이어서 추가적으로 알게된 Spring Cloud Config & Spring Cloud Bus 라는 주제로 포스팅 해보려고 한다. 

 

즉, 비지니스 로직의 수정은 없고, 기본적인 설정 정보를 수정하는 것을 목적으로 현재 가동되어있는 애플리케이션 서버를 다운시키지 않고 적용할 수 있는 방법을 말이다. 

 

Spring Cloud Config 

간단하게 설명하자면, 여러 서버의 설정 파일을 중앙 서버에서 관리하면서 서버를 재배포 하지 않고 설정 파일의 변경사항을 반영할 수 있다. 

 

 

[Config 저장소]

위의 그림을 살펴보자면, 설정 정보들을 저장하는 저장소가 필요하다. 흔히 알고있는 Git Repository 같은 저장소에 .yml 파일들을 저장하고 해당 리파지토리에서 설정 정보들을 불러오는 것이다. 이러한 정보들은 노출되면 안되는 중요한 설정들이 많기 때문에 기본적으로 Private으로 설정한다.

 

(처음 프로젝트를 진행하는 개발자 친구들이 .yml 파일을 Public으로 설정해서 노출돼선 안되는 Secret Key를 공개하여 AWS 과금이 청구되었다는 사례를 들어본 적이 종종 있었다. 조심하자 ^^) 

 

[Spring Cloud Config Server]

Config 저장소와 연결되어 있는 Server인데 설정 파일을 관리해준다. 마이크로 서비스 환경에서 해당 설정 파일을 이용하는 다양한 클라이언트들이 존재할텐데 클라이언트가 Spring Cloud Config Server에게 설정값을 요청하면 Server는 설정파일이 저장된 Config 저장소에 접근해 조건에 맞는 영역을 탐색하게 된다. 그 후, 가져온 최신 설정값을 가지고 클라이언트에게 전달한다. 

 

기본적으로 위의 흐름은 어플리케이션 서버가 구동될 때 진행되는 것이며, 만약 운영 중에 동적으로 설정 값을 갱신하고 싶다면, /actuator/refresh API를 호출해야한다. 즉,  /actuator/refresh API 를 사용하여 변경된 설정정보를 어플리케이션 서버에 갱신하면서 서버를 재배포하지 않고도 설정 정보를 적용할 수 있는 것이다. 

 

actuator를 알아보기 전에 서버의 구성을 알아보자. 

 

Config Server

 implementation 'org.springframework.cloud:spring-cloud-config-server'
@SpringBootApplication
@EnableConfigServer
public class ConfigServiceApplication {

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

 

server:
  port: 8888
spring:
  application:
    name: config-service
  cloud:
    config:
      server:
        git:
          uri: C:/Users/bae10/Desktop/MSA_YML/

 

ConfigServer 활용할 서버는 위의 Dependency를 추가하고 @EnableConfigServer 어노테이션 명시를 기본적으로 설정해야 한다. .yml 파일에는 어떠한 포트를 쓸 것인지와 yml 파일이 보관되어 있는 Git Repository 주소, ID, PWD를 설정해야한다. 필자는 따로 깃 리파지토리를 생성하지 않았고 로컬의 파일 시스템 절대 경로로 설정하였다.

즉, "C:/Users/bae10/Desktop/MSA_YML/" 해당 경로에 아래와 같이 여러 설정 파일들이 존재한다. 

 

 

중요한 것은 파일 이름인데, {앱이름}-{프로파일}.yml 의 구조로 작성해주면 된다. 현재 위와 같은 yml 파일들이 존재하며, Config Server가 구동되어있을 때 localhost:8888/{앱이름}/{프로파일}로 접근하면 정상적으로 동작함을 확인할 수 있다.

 

"http://localhost:8888/ecommerce/default" 로 GET 요청을 보내면 ecommerce.yml 파일의 정보들이 반환되며 ecommerce-dev의 정보를 보고 싶다면, "http://localhost:8888/ecommerce/dev" 로 요청하면 된다. 

 

"http://localhost:8888/ecommerce/default" -> ecommerce.yml 파일에 대한 정보 반환

 

Config Client 

dependencies {
    implementation('org.springframework.cloud:spring-cloud-starter-bootstrap')
    implementation('org.springframework.cloud:spring-cloud-starter-config')
    implementation('org.springframework.boot:spring-boot-starter-actuator')
}

 

[bootstrap.yml]

# ecommerce.yml 파일을 사용하겠다. 
spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce
      
# 만약 ecommerce-dev.yml 파일을 사용하고 싶으면 다음과 같이 추가 설정 해주면 된다. 
  profiles:
    active: dev

 

Config Server의 경우 application.yml만 사용했지만, 클라이언트의 경우 application.yml 이 아닌 다른 설정파일도 사용하였다. 위의 디펜던시를 보면 bootstrap을 추가하였는데, bootstrap.yml 파일의 경우 스프링부트 앱 가동시 application.yml 보다 먼저 로드되므로 위의 설정 파일을 추가적으로 설정하였다. 또한, 동적으로 yml 파일을 적용하기 위해서 actuator도 설정해줘야한다. (dependencies 참고) 

 

 

[Actuator] 

Actuator는 스프링 부트 애플리케이션의 모니터링이나 매트릭(metric)과 같은 기능을 HTTP 엔드포인트를 통해 제공한다. Endpoints에 대한 Actuator 설정을 아래 링크를 통해서 확인할 수 있다. 

 

 

Production-ready Features

You can enable recording of HTTP exchanges by providing a bean of type HttpExchangeRepository in your application’s configuration. For convenience, Spring Boot offers InMemoryHttpExchangeRepository, which, by default, stores the last 100 request-response

docs.spring.io

 

[application.yml] 속성 추가

# actuator 속성 추가 
management:
  endpoints:
    web:
      exposure:
        include: refresh, health, beans

 

위의 application.yml에 속성을 추가했는데, health 같은 경우 상태 반환, beans 같은 경우 bean 정보들을 반환하게 된다. 예를들면, beans 엔드포인트를 활용하여 GET 요청을 하면 다음과 같은 데이터들이 반환된다. (구동되어 있는 마이크로 서비스의 bean 정보들을 보여주는 것)

 

bean에 대한 정보들 반환 (http://IP주소:PORT/actuator/beans)

 

 

만약, refresh 엔드 포인트로 요청하게 된다면 현재 구동되어있는 클라이언트의 설정 파일을 말 그대로 리프레쉬한다. 쉽게말해  yml 파일이 수정되었을 때 서버의 재기동 없이도 현재 구동되어있는 클라이언트에 동적으로 적용된다는 뜻이다. 실습 예제에서 Config Client는 config server에서 보관되어있는 ecommerce.yml 설정 파일을 기반으로 구동되어져 있으며 만약 해당 yml 파일이 수정되어 깃 리파지토리에 업로드 된다면, 다음과 같은 요청으로 동적으로 수정 내용이 반영된다. 

 

[POST] http://IP주소:PORT/actuator/refresh  

-> (예시) [POST] http://localhost:4892/actuator/refresh 

 

이제 동적으로 설정 파일을 적용시키는 방법을 알았으니 서버를 재기동할 필요는 없을 것이다. 하지만 클라이언트 수가 수백개라고 가정을 해보자. 그리고 실시간으로 각 서비스간의 설정 파일이 계속해서 수정, 반영돼야 한다면, 각 서비스마다 /actuator/refresh API 를 하나하나 호출해야하는데 상당히 번거로운 일이 된다. 

 

그래서 나온 기술이 Spring Cloud Bus이다. 

 

Spring Cloud Bus

 

Spring cloud bus는 동적으로 config 변경을 적용하기 위한 MQ(Message Queue) Handler 이다. 이에따라 상태 및 구성에 대한 변경사항을 연결된 노드들(마이크로 서비스)에게 전달하는 역할이며 각각의 노드(마이크로서비스)들은 경량 메시지 브로커와 연결되어 있는 상태여야한다. 

 

이전에는 각 마이크로 서비스에서 actuator refresh 방식으로 개별적으로 수정된 설정 정보들을 적용시켜주었다. 하지만 spring cloud bus를 활용하면 Spring Cloud Bus에 연결된 누구에게라도 호출을 하게되면 다른 서비스에게까지 전달이 된다. 즉, 어떠한 서비스든 POST 방식으로 /busrefresh라는 actuator를 호출하면 변경되어진 사항을 Spring Cloud Bus에서 감지를 하고 연결되어있는 각 서비스들에 변경된 사항을 업데이트 한다.

 

* 만약 service1에 /busrefresh 요청을 해도 Service2와 Service3에도 반영이 된다는 뜻. 

 

[흐름] 

  • MQ(Message Queue)에 Publisher(=config server)와 Subscriber를 등록
  • config 변경 정보를 MQ에 전송
  • 각 마이크로서비스에서 config 동적 반영

 

Rabbit MQ

[docker-compose.yml]

version: '3'
services:
  rabbitmq:
    image: "rabbitmq:management"
    ports:
      - "5672:5672"  # AMQP 포트
      - "15672:15672"  # 관리자 대시보드 포트

 

먼저 Rabbit MQ 서버를 띄우기 위해서 Docker를 활용하였고 AMQP 포트 5672와 관리자 대시보드 포트 15672로 설정하여 구동시켰다. "http://localhost:15672" 접속 후 ID, PWD 둘 다 "guest"로 입력하면 다음과 같은 화면이 뜬다. 참고로, AMQP는 메세지 지향 미들웨어를 위한 개방형 표준 응용 계층 프로토콜로 Erlang, RabbitMQ에서 사용된다. 

 

 

 

Config Server

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'

 

[application.yml] 

server:
  port: 8888
spring:
  application:
    name: config-service

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest

  cloud:
    config:
      server:
        git:
          uri: C:/Users/bae10/Desktop/MSA_YML

management:
  endpoints:
    web:
      exposure:
        include: health, busrefresh

 

Config Client

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
# 추가
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
 
# busrefresh 추가
 management:
  endpoints:
    web:
      exposure:
        include: refresh, health, beans, busrefresh

 

Config server와 Client 모두 RabbitMQ 설정 정보들을 추가하고, busrefresh 속성을 추가하면 cloud bus를 활용할 준비가 다 되었다. 

 

 

    @GetMapping("/health_check")
    public String status() {
        return String.format("It's Working in User Service"
                + ", port(local.server.port) = " + env.getProperty("local.server.port")
                + ", port(server.port) = " + env.getProperty("server.port")
                + ", token secret = " + env.getProperty("token.secret")
                + ", token expiration time = " + env.getProperty("token.expiration_time"));
    }

 

위에 보이는 반환 값은 현재 user-service라는 마이크로 서비스(클라이언트)가 구동되어있는데, 위의 URI로 요청시 controller에서 정의되어있는대로 포트 번호, 그리고 config server에서 관리하고 .yml 파일에 대한 정보들을 출력한다. 즉, user-service가 현재 적용되어 있는 ecommerce.yml 파일에 대한 정보이다. 

 

이제 동적으로 .yml 파일을 수정할 작업이 남았는데, user-service에 /actuator/refresh 요청을 보내는 것이 아니라 다른 클라이언트인 api gateway에 /actuator/busrefresh로 요청한다.  

 

위의 반환 값을 보면 현재 token secret 마지막 숫자값은 '9' 

ecommerce.yml에서 token secret 마지막 숫자 '7' 로 수정하고 commit 한다. 

 

"[POST] http://localhost:8000/actuator/busrefresh"

API 게이트웨이(현재 8000번으로 구동 중)로 busrefresh 요청

 

 

token secret 마지막 숫자 '7'로 수정 완료

 

이후 다시 user-service에 정보를 반환하면 수정된 내용이 적용된 것을 볼 수 있다. 흐름을 다시 한 번 설명하자면, cloud bus를 활용하는 클라이언트들은 현재 'api-gateway' (클라이언트 1) 'user-service' (클라이언트 2)인데 둘 다 ecommerce.yml 정보를 기반으로 하고 있다. 

 

ecommerce.yml 파일 정보를 수정 후 깃 리파지토리에 반영시켰다고 가정하자. 이후 api-gateway (클라이언트 1)에 "[POST] http://localhost:8000/actuator/busrefresh" 요청을 보내면 config 변경 정보를 MQ에 전송하여 각 마이크로서비스(User-service)에 동적으로 반영된다는 것이다. 

 

이렇게 Message Queue 방식을 이용해서 서비스를 구축한다면 각 마이크로 서비스에 일일히 설정 정보들을 처리하지 않고도 클라우드 버스를 활용하는 각 마이크로 서비스들에게 모두 반영되므로 유지보수에 큰 장점이 있을 것이다. 

 

추가적으로 필자의 생각이지만 깃 리파지토리에서 설정 정보 파일이 변경된다면, webhook 설정을 통해 자동으로 config server로 [POST] http://localhost:8000/{config server host}/actuator/busrefresh"를 수행할 수 있는 방법이 있지 않을까 생각한다. 필자는 CI/CD 작업을 진행하면서 깃 리파지토리에 특정 리모트 브랜치에 정상적으로 Merge 되면, Webhook 설정을 통해 변경 감지를 했었고 이후 자동 빌드를 진행시켰다. 그래서 더욱더 편리한 방법을 찾고자한다면 해당 방식도 가능한지 알아볼 거 같다. 

반응형

댓글