Springboot MSA 구성1 - Spring Cloud Config

Published: by Creative Commons Licence

Spring Cloud Config는 MSA 시스템의 환경 설정들을 중앙화해서 한 곳에서 관리를 할 수 있게 해주고 설정파일의 변경되어도 어플리케이션의 재배포 없이 적용이 가능하다.
진행할 프로젝트 구성은 아래와 같이 진행할 예정이다.

  1. Spring Cloud Config 서버
  2. Spring Cloud Zuul(API Gateway) 서버
  3. Spring Cloud Eureka 서버

1~3번까지 완료 후 시간이 된다면 인증서버까지 적용해 볼 예정이다. 오늘은 MSA 프로젝트의 첫번째 Spring Cloud Config 서버를 구성해 보려고 한다. 그리고 Config 서버에서 설정한 프로퍼티들을 Resource 서버에서 확인을 해 볼 예정이다.

PreSetting

프로젝트는 Intellij의 이전에 정리한 노트를 참고해서 Multi Module 프로젝트를 구성했다.

  • Config : Spring Cloud Config 서버
  • config-repo : Spring Cloud Config 서버가 바라보는 설정파일
  • Eureka : Spring Cloud Eureka 서버
  • Gateway : Spring Cloud Zuul(API Gateway) 서버
  • Resource : Application 서버
  • Resource2 : Application 서버

Config 서버

build.gradle

# 공통 Dependency
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

# Config Dependency
project(':Config') {
    dependencies {
        implementation 'org.springframework.cloud:spring-cloud-config-server'
    }
    
    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
}

Application.java

EnableConfigServer 어노테이션 추가

@EnableConfigServer
@SpringBootApplication
public class ConfigApplication {

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

}

bootstrap-{env}.yml

local

server:
  port: 9000
spring:
  application:
    name: configServer
  cloud:
    config:
      server:
        git:
          uri: https://github.com/sisipapa/Springboot-MSA
          search-paths: config-repo/local

prod

server:
  port: 9000
spring:
  application:
    name: configServer
  cloud:
    config:
      server:
        git:
          uri: https://github.com/sisipapa/Springboot-MSA
          search-paths: config-repo/prod

config-repo

config-repo 모듈에 application 별 active.profiles 별로 설정파일을 만든다. 여기서는 message의 내용만 조금 다르게 설정했다.

Resource 모듈의 active.profiles가 local일 경우 바라보는 config 설정

spring:
  profiles: local
  message: Resource Service Local Server!!!!!

Resource2 모듈의 active.profiles가 local일 경우 바라보는 config 설정

spring:
  profiles: local
  message: Resource2 Service Local Server!!!!!

여기까지만 하면 Spring Cloud Config 서버의 설정은 끝이다. 로컬서버 구동시 -Dspring.profiles.active=local로 설정 후 서버를 기동한다.

Config 서버 테스트

GET http://localhost:9000/resource/local

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 24 Aug 2021 09:31:28 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "name": "resource",
  "profiles": [
    "local"
  ],
  "label": null,
  "version": "5eb3f2ee05568a9878370171ccfc4a88af79a71c",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/sisipapa/Springboot-MSA/file:C:\\Users\\user\\AppData\\Local\\Temp\\config-repo-17387822938375408351\\config-repo\\local\\resource-local.yml",
      "source": {
        "spring.profiles": "local",
        "spring.message": "Resource Service Local Server!!!!!"
      }
    }
  ]
}

Response code: 200; Time: 3933ms; Content length: 401 bytes  


GET http://localhost:9000/resource2/local

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 24 Aug 2021 09:32:15 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "name": "resource2",
  "profiles": [
    "local"
  ],
  "label": null,
  "version": "5eb3f2ee05568a9878370171ccfc4a88af79a71c",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/sisipapa/Springboot-MSA/file:C:\\Users\\user\\AppData\\Local\\Temp\\config-repo-17387822938375408351\\config-repo\\local\\resource2-local.yml",
      "source": {
        "spring.profiles": "local",
        "spring.message": "Resource Service Local Server!!!!!"
      }
    }
  ]
}

Response code: 200; Time: 822ms; Content length: 403 bytes

Resource 서버

application-{env}.yml

server:
  port: 8080
spring:
  application:
    name: resource
  cloud:
    config:
      fail-fast: true
      discovery:
        service-id: config
        enabled: true
    # uri: http://localhost:9000
management:
  endpoints:
    web:
      exposure:
        include: info, refresh

Controller

GIT 에 저장된 property 값이 변경 된 경우, client에서 최신 파일을 다시 받아 값을 refresh 하기 위해서 RefreshScope 어노테이션을 추가해준다.

@RequestMapping("/v1/resource")
@RestController
@RefreshScope
public class ResourceController {

    @Value("${spring.message}")
    private String message;

    @GetMapping("/message")
    public String message() {
        return "ResourceController message : " + message;
    }
}

Resource 서버에서 Config 조회

GET http://localhost:8080/v1/resource/message

HTTP/1.1 200 
Content-Type: application/json
Content-Length: 63
Date: Tue, 24 Aug 2021 09:42:19 GMT
Keep-Alive: timeout=60
Connection: keep-alive

ResourceController message : Resource Service Local Server!!!!!

Response code: 200; Time: 327ms; Content length: 63 bytes

Resource 서버의 [GET]/message API 호출 결과로 Config property를 정상적으로 읽어 온 것을 확인했다. 이제 config-repo의 설정파일의 내용을 수정하고 동일한 API를 호출하면 변경된 결과가 나오지 않는다. 변경된 설정을 Resource 서버에서도 적용을 하기 위해서는 [POST]/actuator/refresh API를 호출해야 변경된 설정이 적용된다. API 결과로 변경된 property 목록이 리턴된다.

POST http://localhost:8080/actuator/refresh

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 24 Aug 2021 09:47:06 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  "config.client.version",
  "spring.message"
]

Response code: 200; Time: 1540ms; Content length: 42 bytes

Property value 암호화/복호화

Config 서버에서 Resource 서버에 제공하는 설정 정보 중에는 외부에 노출되어서는 안되는 정보들이 있을 수 있다. 그래서 value값을 저장 시 암호화된 값을 저장할 수 있도록 지원한다.

keypair 생성

keytool -genkeypair 옵션 설명

$ keytool -genkeypair -help
keytool -genkeypair [OPTION]...

Generates a key pair

Options:

 -alias <alias>          alias name of the entry to process
 -keyalg <alg>           key algorithm name
 -keysize <size>         key bit size
 -groupname <name>       Group name. For example, an Elliptic Curve name.
 -sigalg <alg>           signature algorithm name
 -destalias <alias>      destination alias
 -dname <name>           distinguished name
 -startdate <date>       certificate validity start date/time
 -ext <value>            X.509 extension
 -validity <days>        validity number of days
 -keypass <arg>          key password
 -keystore <keystore>    keystore name
 -storepass <arg>        keystore password
 -storetype <type>       keystore type
 -providername <name>    provider name
 -addprovider <name>     add security provider by name (e.g. SunPKCS11)
   [-providerarg <arg>]    configure argument for -addprovider
 -providerclass <class>  add security provider by fully-qualified class name
   [-providerarg <arg>]    configure argument for -providerclass
 -providerpath <list>    provider classpath
 -v                      verbose output
 -protected              password through protected mechanism

Use "keytool -?, -h, or --help" for this help message

keypair 생성
Window PC의 경우 JDK를 환경변수 Path등록을 하지 않았다면 JDK가 설치된 경로로 이동해서 아래 명령어를 실행한다.

$ keytool -genkeypair -alias config-server-key -keyalg RSA \
  -dname "CN=Config Server,OU=Spring Cloud,O=sisipapa" \
  -keypass keypass123 -keystore config-server.jks -storepass storepass123

위의 명령을 실행하면 config-server.jks 파일이 생성된다. jks 파일을 Config 모듈의 resources 디렉토리에 복사한다.

암호화 설정추가

application-{env}.yml 파일에 암호화 관련 설정을 추가한다.

encrypt:
  key-store:
    location: classpath:/config-server.jks
    password: storepass123
    alias: config-server-key
    secret: keypass123

데이터 암호화

암호화 설정을 추가하고 Config 모듈을 재기동 후 Config 서버의 /encrypt API를 통해 데이터를 암호화한다. 아래는 config-repo의 resource-local.yml db.ip, db.port

POST http://localhost:9000/encrypt

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 388
Date: Tue, 24 Aug 2021 15:06:10 GMT
Keep-Alive: timeout=60
Connection: keep-alive
        
AQBXjueGBbxNGPkv7DNYGs/zbQzJKC7rov1IgLG+Qm4l+OQ+ItN108j8Kw+bCcE57O2nD5zaH/pu8796iqxDRpZfdHJD+TS5Ej0zggdfvdaT+8nPOa3gwy9b5WfBi9PtZXgjeimqVwo9xJ4h4YE7I69dP09w4Y0FYJuciE6t+D5oCSbur1joySz8ausDFvw72YUtmpuEw1jLTwka+T3zQfgMBQsnAmV0QH+p/xnCJJ6UHHS0fu6bu+rqyViHfFbB1NRBzUn/b9sVgx9xbMWN80AeBFZebsspHBA76B+RG7N2Kksk5WtmoebsjDPVvlESCKNY0LnXtrOT2s8+7BqO6nYKF8+s/S8x350UrmC2cWSQ5fRDuGTZ05vNpBHFaGl3MP4=
AQAWG7JW48DuhyEqaFRsja62We9Fw94lGqCJBDRv1nXvolWV17nMarXqVqHQSgdD86l8WasXh6k9SlIH660fJclruOgfM0EBwJK+bXmSrGsfzqQs7UmWnJGAraH2Xe6vUL2dj9uq/nXih8glq3UPjpzoK5c4iYEr3G7xdesQhzupG9Yqu7Dqv53HzRVxQnooj2WiSRD2tiqYw1JCGR79Q4RtNs7t0d3ObpcaVOSGQLsl/8L5Rw9GFZfZbfiuYwYQfZ+ooHxJf3XjiO2qCiKuk8e64hy+PDk0WhF9zlZK0svj1HxBdDRNrYjWZI7TfIXinOmaVu5wVDkaNg14oS7y6Toa9vcTCfi1HK24r94GD0naOhDHCWmL5Caek/0JQk7ihjI=
AQBdqizzBGEwvI9n09gVVbPM9yapxvluq1nOVWSHYlfRrQXHf2ONsYsUqYtIP0TE/ijG8O6raD8L02PflI1NdUe/TCwo59VJX2wdvf041HuNVnJN0poYuwdWSg0BWZ7MiDDmID0m202u249exj91nnOjQ+fzbRp80y39QMtaZ90X74e0rAWxU+8kqePjk7z+Y4Yx+iqRTY88Cxulk05ijnyR4oG/vv6+Q4DQH/h9U5Rdcjh0Ndttc3XJGnDa7DmILGWD1gZ3+dbpXU2GisaZgEk2MRc21J20Ac0NFZWJvmSMwm9hLpaCGB4CceghDYZ/yAZJvUMgA3aZtFWd2LJ0qKekL5SSVA/4oGLi8pBVvaCSmKgqvzHS94opZz+srLHwljw=
AQCGy8uXlX8tFPHJTCEhMVYRiJFkaN3yvucHeXrsNsoxFO3l5ds3oHnnwZlbNv23HLbLCt7CmFsw1V7i8b3yqgtDSfgL8nMgmZUJ0rDRAToUEqIkcohkKS8tHreYbjFTurz632S+m4AouFsq6LaGIZU6ftctwTH6bb8v2SccCejcIuvTWoKOr8QKzJhszZH17pJLG4NJDcKqktv0+ZwA2tdr5nb+IE4Eg8RMO6sPnZUHKibqiwmle5C0hDMSBE0KlL4UvNAQg1yyJfLdk/Lm/P1tNyii6WWgG79IWwitbYDtBfSj4nm3mQyMq6zdcS1wYyCb+LvmMejp0r1LwXwYbYFqpwhFUtAkTKrvAhVnYpbQbtfJvdpyu1ZWq3WDKNseE3Y=
        
Response code: 200; Time: 316ms; Content length: 388 bytes

resource-local.yml 설정파일에 암호화된 db.ip, db.port, db.id, db.password 설정추가

spring:
  profiles: local
  message: Resource Service Local Server!!!!!=>수정1
db:
  ip: '{cipher}AQBXjueGBbxNGPkv7DNYGs/zbQzJKC7rov1IgLG+Qm4l+OQ+ItN108j8Kw+bCcE57O2nD5zaH/pu8796iqxDRpZfdHJD+TS5Ej0zggdfvdaT+8nPOa3gwy9b5WfBi9PtZXgjeimqVwo9xJ4h4YE7I69dP09w4Y0FYJuciE6t+D5oCSbur1joySz8ausDFvw72YUtmpuEw1jLTwka+T3zQfgMBQsnAmV0QH+p/xnCJJ6UHHS0fu6bu+rqyViHfFbB1NRBzUn/b9sVgx9xbMWN80AeBFZebsspHBA76B+RG7N2Kksk5WtmoebsjDPVvlESCKNY0LnXtrOT2s8+7BqO6nYKF8+s/S8x350UrmC2cWSQ5fRDuGTZ05vNpBHFaGl3MP4='
  port: '{cipher}AQAWG7JW48DuhyEqaFRsja62We9Fw94lGqCJBDRv1nXvolWV17nMarXqVqHQSgdD86l8WasXh6k9SlIH660fJclruOgfM0EBwJK+bXmSrGsfzqQs7UmWnJGAraH2Xe6vUL2dj9uq/nXih8glq3UPjpzoK5c4iYEr3G7xdesQhzupG9Yqu7Dqv53HzRVxQnooj2WiSRD2tiqYw1JCGR79Q4RtNs7t0d3ObpcaVOSGQLsl/8L5Rw9GFZfZbfiuYwYQfZ+ooHxJf3XjiO2qCiKuk8e64hy+PDk0WhF9zlZK0svj1HxBdDRNrYjWZI7TfIXinOmaVu5wVDkaNg14oS7y6Toa9vcTCfi1HK24r94GD0naOhDHCWmL5Caek/0JQk7ihjI='
  id: '{cipher}AQBdqizzBGEwvI9n09gVVbPM9yapxvluq1nOVWSHYlfRrQXHf2ONsYsUqYtIP0TE/ijG8O6raD8L02PflI1NdUe/TCwo59VJX2wdvf041HuNVnJN0poYuwdWSg0BWZ7MiDDmID0m202u249exj91nnOjQ+fzbRp80y39QMtaZ90X74e0rAWxU+8kqePjk7z+Y4Yx+iqRTY88Cxulk05ijnyR4oG/vv6+Q4DQH/h9U5Rdcjh0Ndttc3XJGnDa7DmILGWD1gZ3+dbpXU2GisaZgEk2MRc21J20Ac0NFZWJvmSMwm9hLpaCGB4CceghDYZ/yAZJvUMgA3aZtFWd2LJ0qKekL5SSVA/4oGLi8pBVvaCSmKgqvzHS94opZz+srLHwljw='
  password: '{cipher}AQCGy8uXlX8tFPHJTCEhMVYRiJFkaN3yvucHeXrsNsoxFO3l5ds3oHnnwZlbNv23HLbLCt7CmFsw1V7i8b3yqgtDSfgL8nMgmZUJ0rDRAToUEqIkcohkKS8tHreYbjFTurz632S+m4AouFsq6LaGIZU6ftctwTH6bb8v2SccCejcIuvTWoKOr8QKzJhszZH17pJLG4NJDcKqktv0+ZwA2tdr5nb+IE4Eg8RMO6sPnZUHKibqiwmle5C0hDMSBE0KlL4UvNAQg1yyJfLdk/Lm/P1tNyii6WWgG79IWwitbYDtBfSj4nm3mQyMq6zdcS1wYyCb+LvmMejp0r1LwXwYbYFqpwhFUtAkTKrvAhVnYpbQbtfJvdpyu1ZWq3WDKNseE3Y='

복호화 테스트

config-repo 모듈에서 resource 서버의 local 서버가 바라보는 설정파일에 db접속정보를 조회하는 API 호출테스트

@RequestMapping("/v1/resource")
@RestController
@RefreshScope
public class ResourceController {

    @Value("${spring.message}")
    private String message;

    @Value("${db.ip}")
    private String ip;
    
    @Value("${db.port}")
    private String port;
    
    @Value("${db.id}")
    private String id;
    
    @Value("${db.password}")
    private String password;

    @GetMapping("/message")
    public String message() {
        return "ResourceController message : " + message;
    }

    @GetMapping("/db")
    public String db() {return "ResourceController db : " + ip + ":" + port + " [" + id +" : " + password + "]";}
}
GET http://localhost:8080/v1/resource/db

HTTP/1.1 200 
Content-Type: application/json
Content-Length: 56
Date: Tue, 24 Aug 2021 15:28:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive

ResourceController db : 10.10.10.10: 2222 [dbid: dbpass]

Response code: 200; Time: 235ms; Content length: 56 bytes

Config 서버에서 암호화 되있던 property db.ip,db.port,db.id,db.password 정보가 resource 서버에서 복호화 된 데이터로 출력되는 것을 확인할 수 있다.

참고

DaddyProgrammer Spring CLoud MSA(1)
Jorten [Spring] Cloud Config 구축하기
Spring Cloud Config 에서 변경된 정보를 마이크로서비스 인스턴스에서 Spring Boot Actuator 를 이용하여 반영하기

Github

https://github.com/sisipapa/Springboot-MSA.git