2018년 5월 31일 목요일

Spring Cloud Config

Spring Boot 를 이용한 Spring Cloud Config 구성

Spring Cloud Config란?

Spring Cloud Config는 분산 시스템에서 외부화 된 구성(configuration) 에 대한 서버 및 클라이언트 측 지원을 제공한다.
이는 환경 설정등의 외부화를 통해 중앙에서 어플리케이션의 속성등을 관리하는 용도로 쓰인다.
시스템의 환경설정이나 Properties 설정등을 중앙으로 분리하여 관리하게 함으로써 설정정보를 배포하지 않아도 실시간으로 설정을 적용할 수 있다.
기존 어플리케이션에서 Profile 설정 -Dspring.profiles.active을 이나 properties 의 Key통해 Production 환경과 Develop, Local 환경 값들을 관리하던것을 분리하고 쉽게 변경하기 위해 사용된다.
공식 사이트 Spring Cloud Config

주요 특징

  • 외부 구성 (이름 - 값 쌍 또는 이와 동등한 YAML 내용) 을위한 HTTP, 자원 기반 API
  • 속성 값 암호화 및 해독 (대칭 또는 비대칭)
  • @EnableConfigServer를 사용하여 Spring Boot 응용 프로그램에 쉽게 임베드 가능
  • 원격으로 설정을 적용하고 수정할 수 있음
기본적으로 private 한 환경의 Git이나 SVN과 같은 버전 관리 시스템에 설정을 올려놓고 클라이언트가 설정 정보를 읽어오는 Config Server에 접근하여 설정값들을 읽어오는 구조라고 이해할 수 있다.
대형 분산 환경에서 서버의 테스트 환경도 다양해지고 Service 하는 도메인들도 MSA 구조로 바뀌면서 설정 정보 변경은 잦을수 밖에 없으며, 바뀔 때 마다 Production 환경에 빌드&배포를 한다는 것은 굉장히 비효율적이고 부담이 되는 작업일 수 밖에 없다. 특히나 클라우드 환경에서 머신들이 Auto Scaling 하는 환경이라면 더더욱 부담이 될 수 밖에 없을것이다.
이런 상황에서 중앙 제어를 통한 설정의 외부화는 장애를 사전에 방지하고 재빠르게 설정을 적용하는 등의 잇점이 있는 것은 분명한 일이다.
Spring Cloud Config Server는 git이나 file system 을 통해 설정을 외부화 하여 repository를 구성하고 각 MSA (Client Application) 도메인들은 Config Server로 부터 감지된 정보들을 적용하는 형태가 된다.

기본 정보 구성하기

기본적인 설정 정보를 Git 에 올려 놓고 Config 서버를 구성해 보자.
(설정 정보만 구성하는것은 아니고 실제 서비스의 domain 별 구성정보라던가 서버별 구동 profile 이라던가 다양한 구성을 할 수 있다.)
github 에서 환경 설정 값을 저장하기 위한 새 repository 를 생성한다.
여기서는 spring-cloud-config 라고 명명하였다.
리파지토리가 생성되면 에디터등을 통해 두개의 파일 config-dev.yml, config-production.yml 을 생성한다.
여기서는 IntelliJ의 Check out from Version Control 의 git 을 통해 파일을 생성한 후 Push 하였다.
Git url 을 지정하고 디렉토리에 Clone 한다. (github에서 생성해도 상관없다)
enter image description here
enter image description here
config-dev.yml, config-production.yml 파일에 각각 mode: "develop" , mode: "production" 을 입력 한 후 Push 한다.

Github 에 적용된 파일이 올라갔다면 이제 이 파일을 읽어들이는 Config Server를 구성해보자.
enter image description here
IntelliJ의 Create New Project 를 통해서 Spring Initializr 를 선택한 후 항목들을 입력한다.
enter image description here
enter image description here
우리가 구성할 것은 Spring Cloud Config 이므로 아래와 같이 Boot 2.x 의 Cloud Config 항목에서 Config Server 번들을 선택한다.
enter image description here
실제 Config server 에 입력해야 할 항목은 @EnableConfigServer 어노테이션을 추가한 후 application.properties 에 아래와 같이 Git Repository 를 입력하는 것으로 설정은 끝이 난다.
server.port=8080  
spring.cloud.config.server.git.uri=https://github.com/gliderwiki/spring-cloud-config.git
SpringCludeApplication의 전체 소스는 아래와 같다.
package com.example.springcloud;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.cloud.config.server.EnableConfigServer;  
  
@SpringBootApplication  
@EnableConfigServer  
public class SpringcloudApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(SpringcloudApplication.class, args);  
  }  
}
boot 어플리케이션을 기동한 후 http://localhost:8080/config/dev 혹은 http://localhost:8080/config-dev/default 로 접속하면 Git 의 초기 설정 정보가 아래와 같이 JSON으로 내려오는 것을 확인할 수 있다.
enter image description here
현 화면에서는 8080 포트로 작성하였으나 추후 여러 클라이언트가 붙는 상황을 가정하여 8888 포트로 변경하였다.
위와 같은 구조로 클라리언트 어플리케이션 들이 붙는다고 가정하면 local, dev, stage, production 과 같은 구조의 설정 파일들을 분리 하여 적용할 수 도 있고, 각 도메인 별 설정파일 안에 다양한 환경 profile 들을 구성해서 적용할 수 있다는 것을 예상할 수 있을 것이다.
실제 config yml 파일들을 아래와 같이 변경한 후 push 하고 재 접속해보도록 하자.

config-dev.yml
service:  
 mode: "develop"  
  params:  
    args1 : Hello  
    args2 : World
config-production.yml
service:  
 mode: "production"  
  params:  
    args1 : Real  
    args2 : Service
enter image description here
간단한 Config server 구성만으로 적용된 내용이 갱신되는 것을 확인할 수 있다.
Git 에 저장한 설정파일은 yml이나 properties 둘 다 적용이 가능하다.
이제 Client 레벨에서 Config Server 에 접근하는 두개의 어플리케이션을 통해 하나는 개발용 Config를, 나머지 하나의 서비스는 리얼 Config의 설정된 내용들을 적용해보도록 한다.
초기 세팅은 http://start.spring.io/ 의 Spring Initializr를 사용하여 프로젝트를 제너레이션 해도 무방하다. 여기서는 Config Client 와 Web 를 선택한다.
enter image description here
IntelliJ에서는 File > New > Project 를 이용해 신규 클라이언트 프로젝트를 설정한다. 마찬가지로 Spring initializr 를 선택 한 후 다음을 클릭한다.
enter image description here

enter image description here
member-service 로 Artifact를 지정하고 다음을 클릭하고 마찬가지로 Config Client 와 Web 번들을 선택한다.

enter image description here
member-service 프로젝트는 Config Server 정보를 바라봐야 하기 때문에 아래와 같이 bootstrap.yml 에 Config Server 정보를 연결해준다.
server:  
  port: 8081  
  
spring:  
 application: 
   name: member-service  
   cloud:  
     config: 
       url: http://localhost:8888  
       name: config
8888 포트의 Config 서버 정보를 url 로 기술하고 현재 프로젝트의 명, 접근 하는 서버의 명 정도를 기술한 후 8081 포트로 어플리케이션을 실행할 것이다.
속성값을 정의 할 때 application.properties 보다 bootstrap의 설정값을 먼저 읽어온다.
이는 Config Server의 정보와 이름 정도만 기술하면 어플리케이션의 구동시 cloud name 을 통해 HTTP 로 호출을 해서 구성을 검색하게 된다.
따라서 구동시에 application.properties 보다 먼저 읽어와야 할 속성이 있을 경우 bootstrap 에 설정하도록 한다.


마지막으로 Config 가 읽어오는 설정 파일중에 dev 설정을 읽어와야 하므로 VM Option으로 -Dspring.profiles.active={profile} 을 추가 해준다. 여기서는 config-dev.yml을 읽을 예정이므로 dev로 설정하였다.
필요에 따라 config 구성 파일 내에 local, dev, stage, production 을 구분 지어 놓아도 무방하다.
이제 어플리케이션을 실행해보자.
아래와 같이 Fetching config from server at: http://localhost:8888 으로 Config Server에 연결한 것을 확인할 수 있으며 profile 정보는 dev로 active 된것을 확인할 수 있다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)

2018-06-01 10:08:22.687  INFO 54538 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2018-06-01 10:08:23.424  INFO 54538 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=member-service, profiles=[dev], label=null, version=21a6b130c5b4f2f2546ee776656d7c394b60a653, state=null
2018-06-01 10:08:23.424  INFO 54538 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}]}
2018-06-01 10:08:23.427  INFO 54538 --- [           main] c.e.s.m.MemberServiceApplication         : The following profiles are active: dev
2018-06-01 10:08:23.435  INFO 54538 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:23.705  INFO 54538 --- [           main] o.s.cloud.context.scope.GenericScope     : BeanFactory id=ddc33e78-033f-3c04-8011-781be6cad530
2018-06-01 10:08:23.737  INFO 54538 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$278e6290] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2018-06-01 10:08:23.876  INFO 54538 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)
2018-06-01 10:08:23.891  INFO 54538 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-06-01 10:08:23.891  INFO 54538 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.31
2018-06-01 10:08:23.895  INFO 54538 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/Users/kakao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]
2018-06-01 10:08:23.966  INFO 54538 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-01 10:08:23.966  INFO 54538 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 531 ms
2018-06-01 10:08:24.052  INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2018-06-01 10:08:24.054  INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-01 10:08:24.054  INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-01 10:08:24.054  INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-01 10:08:24.054  INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-01 10:08:24.116  INFO 54538 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.244  INFO 54538 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:24.279  INFO 54538 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-01 10:08:24.279  INFO 54538 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-01 10:08:24.295  INFO 54538 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.295  INFO 54538 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.423  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-01 10:08:24.429  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'configurationPropertiesRebinder' has been autodetected for JMX exposure
2018-06-01 10:08:24.429  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'environmentManager' has been autodetected for JMX exposure
2018-06-01 10:08:24.430  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'refreshScope' has been autodetected for JMX exposure
2018-06-01 10:08:24.432  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'environmentManager': registering with JMX server as MBean [org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager]
2018-06-01 10:08:24.439  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'refreshScope': registering with JMX server as MBean [org.springframework.cloud.context.scope.refresh:name=refreshScope,type=RefreshScope]
2018-06-01 10:08:24.445  INFO 54538 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'configurationPropertiesRebinder': registering with JMX server as MBean [org.springframework.cloud.context.properties:name=configurationPropertiesRebinder,context=2609b277,type=ConfigurationPropertiesRebinder]
2018-06-01 10:08:24.487  INFO 54538 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2018-06-01 10:08:24.490  INFO 54538 --- [           main] c.e.s.m.MemberServiceApplication         : Started MemberServiceApplication in 2.589 seconds (JVM running for 3.514)

이제 member-service 프로젝트에서 설정 정보를 정상적으로 읽어오는지 Controller를 작성해서 확인해보자.
package com.example.springcloud.memberservice;  
  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class MemberController {  
  
    @Value("${service.params.args1}")  
    private String arguments1;  
  
  
    @Value("${service.params.args2}")  
    private String arguments2;  
  
    @GetMapping("/member")  
    public String member() {  
        return arguments1 + arguments2;  
    }   
}
Git 에 올려놓은 설정파일중에 config-dev.yml 내에는 아래와 같은 정보들을 구성해놨기 때문에 이를 읽어와서 화면에 뿌려주는 간단한 어플리케이션이다.
service:  
  mode: "develop"  
  params:  
    args1 : Hello  
    args2 : World
어플리케이션을 재구동하고 localhost:8081/member 에 들어가면 원격지 git 의 설정중에 config-dev.yml을 ConfigServer가 읽어와서 클라이언트에 연결된 상태임을 알 수 있다.
하지만 여기서 중요한 것은 @Value 로 주입한 정보는 Static 한 값이므로 Git 설정 값을 변경해서 Push 한다고 해도 변경 사항이 바로 반영되지는 않는다. (왜??? 인지는 이후에 설명한다.)
이 부분은 다른 방식으로 처리해야 하는데 (profile 옵션을 여러개를 운용할 경우 profile parameter 값은 제대로 물고 오는 것을 확인할 수 있다.) 바로 @RefreshScope 어노테이션을 해당 클라이언트에 추가해주어야 한다.
그래도 바로 반영되는 것은 아니고, Spring boot 2.x 부터는 Spring boot actuator 를 이용해 POST 방식으로 end point 를 호출을 해주어야 적용이 된다. (여기서는 http://localhost:8081/actuator/refresh 를 curl이나 postman 등의 rest client 툴을 활용해 post 방식으로 호출해주어야 적용이 된다)
Spring boot 1.x 에서는 /refresh url 을 Post 로 호출해주었어야 했다.
아마도 한번 더 확인하고 반영을 할 수 있도록 하나의 단계를 더 마련한것이 아닌가 추측된다.
실제로 공식문서에는 아래와 같이 설명하고 있다
.
The EnvironmentChangedEvent covers a large class of refresh use cases, as long as you can actually make a change to the Environment and publish the event (those APIs are public and part of core Spring). You can verify the changes are bound to @ConfigurationProperties beans by visiting the /configprops endpoint (normal Spring Boot Actuator feature). For instance a DataSource can have its maxPoolSize changed at runtime (the default DataSource created by Spring Boot is an @ConfigurationProperties bean) and grow capacity dynamically. Re-binding @ConfigurationProperties does not cover another large class of use cases, where you need more control over the refresh, and where you need a change to be atomic over the whole ApplicationContext. To address those concerns we have @RefreshScope.
이제 @RefreshScope 어노테이션을 통해 동적으로 설정이 변경되는 작업을 진행해보자.
위의 과정과 마찬가지로 movie-service 라는 프로젝트를 생성한다. 이번에는 Spring Initializr 선택 시 하나 더 추가해야 하는데, Actuator 번들을 반드시 포함 해야 한다.
bootstrap.yml의 port는 8082로 설정하고 VM option 도 -Dspring.profiles.active=production으로 변경 한다.
server:  
 port: 8082  
  
spring:  
 application: 
   name: movie-service  
 cloud:  
   config: 
     url: http://localhost:8888  
     name: config
어플리케이션이 정상적으로 동작하는지 확인한 후 controller 를 아래와 같이 작성해보자.
@RestController  
@RefreshScope  
public class MovieController {  
  
    @Value("${service.params.args1}")  
    private String arguments1;  
  
  
    @Value("${service.params.args2}")  
    private String arguments2;  
    
    @GetMapping("/movie")  
    public String movie() {  
        return arguments1 + arguments2;  
  }  
}
@RefreshScope 어노테이션을 추가해주었다.
http://localhost:8082/movie 로 브라우저에 접근해서 config-production.yml 에 있는 arg 값이 출력되는지 확인한다.
이제 spring-cloud-config 프로젝트에 있는 git yml 의 arg1, 2 값을 적당히 수정해서 푸시 해보자.
service:  
 mode: "production"  
  params:  
    args1 : Real  
    args2 : Service for KAKAO !!
위의 주소로 다시 접근해도 메세지는 변경 되지 않을 것이다.
터미널 혹은 rest client 를 사용하여 http://localhost:8082/actuator/refresh 를 POST 방식으로 호출한 후 다시 접근해본다.
$ curl -X POST http://localhost:8082/actuator/refresh 
혹은 아래와 같이 입력해도 된다.
$ curl localhost:8082/actuator/refresh -d {} -H "Content-Type: application/json"
응답이 제대로 오는지 확인한 후 페이지를 호출하면 서버의 리로드 없이도 설정된 정보들이 출력되는 것을 확인할 수 있다.
이로써, 우리는 어플리케이션 내에서 속성값이 변경되거나 환경 설정값이 변경될때 마다 빌드 후 재 배포의 부담에서 보다 더 자유로워졌다.
관심사의 분리에서 시작된 어플리케이션 내의 성격이 다른 횡적 기능들에 대한 분리와 CoC와 더불어 향후 MSA 지향으로 대량의 트래픽을 처리하기 위해 환경이 비대해지고 중첩되는 것들을 별도로 분리한다거나, Profile 별 설정이나 도메인별 속성 설정등을 어플리케이션 빌드&배포와 상관없이 관리할 수 있다는 점만으로도 충분히 매력적이라고 생각한다.
Spring Cloud Config는 같은 번들의 Eureka나 Zuul 을 연결함으로써 좀 더 다이나믹한 서버 구조를 활용할 수 있다.
= FIN =