Configuration Service
외부 환경 설정 저장소
분산 시스템에서 yaml이나 properties를 통해 환경 설정 파일을 관리하는 경우 yaml 파일의 내용이 변경되면 애플리케이션 전체가 다시 빌드 및 배포 되어야하는 문제가 있다. 이런 문제를 개선하기 위해 환경 설정 파일을 애플리케이션 내부에 가지고 있는것이 아니라, 외부에 있는 시스템을 통해서 구성 파일 정보를 관리하는 것이 Configuration Service이다.
강의에서는 Spring Cloud Config와 Git이나 암호화된 파일 등의 형상관리를 이용하여 Configuration Service를 구성할 예정이다.
이와 같이 하나의 중앙화된 저장소에서 구성요소를 관리하며, 각 서비스를 다시 빌드하지 않고 바로 적용할 수 있다. 추가적으로 애플리케이션 배포 파이프라인을 통해 각 Profile 환경에 맞는 구성 정보를 사용하도록 설정할 수 있다.
로컬 Git 레포지토리 추가하기
이 강의에서는 git repository로 구성 정보를 관리할 예정인데, remote까지 올리지는 않고 로컬에만 두고 사용하고자 한다.
이처럼 로컬 레포지토리로 사용할 폴더를 만들어두고, git init을 통해 로컬 git repostiory로 등록하자.
token:
expiration_time: 86400000
secret: this_is_my_token_secret
gateway:
ip: 192.168.0.8
YAML
복사
그 안에 JWT 토큰에 대한 구성 정보를 담을 ecommerce.yml 파일을 만들어두자.
그 후 git add와 git commit까지만 하여 git에서 해당 파일을 추적하도록 만든다.
Spring Cloud Config 실행하기
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Java
복사
이처럼 Spring Cloud Config Server 라이브러리 의존성을 추가해주고,
@SpringBootApplication
@EnableConfigServer
public class ConfigServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServiceApplication.class, args);
}
}
Java
복사
애플리케이션 서비스에 @EnableConfigServer 애노테이션을 추가해주자.
server:
port: 8888
spring:
application:
name: config-service
cloud:
config:
server:
git:
uri: file://Users/jiwon/MSA/git-local-repo
YAML
복사
그 후 Config Service의 applicatoin.yml 파일에 위와 같이 애플리케이션 이름, 포트 번호, 아까 만들어둔 로컬 git repsotiroy 경로를 입력한다.
이와 같이 구성한 후 Config Service를 실행하여 8888 포트에 접속하면
이와 같이 아까 로컬 git respository에 저장해둔 JWT 토큰에 대한 구성 정보를 읽어서 보여준다. uri의 경로는 yml 파일의 이름에서 서비스 이름과 profile을 구분하여, ~/{서비스 이름}/{profile}로 구성된다. 위에서는 별도의 profile을 지정하지 않아 뒤에 default나 test, dev 등의 값을 입력해도 전부 ecommerce.yml 파일 정보만 가져온다.
UserService의 구성 정보 외부에서 관리하기
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
}
Java
복사
먼저 user-service에 위와 같이 두 개의 라이브러리 의존성을 추가해주어야 한다.
여기에 bootstrap.yml 파일을 추가해주어야 한다.
spring:
cloud:
config:
uri: http://127.0.0.1:8888
name: ecommerce
YAML
복사
기존의 application.yml 파일에서 일부 property들을 외부에서 관리하는 config service에서 관리하도록 빼게 되면, 구성 정보에 따라 애플리케이션 실행이 제대로 되지 않을 수 있다. 이를 해결하기 위해 bootstrrap.yml 파일을 application.yml 파일보다 먼저 읽어서, 저장된 정보를 통해 외부 config service 정보를 먼저 가져온다. uri를 통해 config serivce의 주소를 가져오고, name을 통해 config service에서 읽어올 yml 파일을 찾는다.
기존의 user-service의 application.yml 파일에 있던 토큰 정보를 주석처리하고 외부 저장소에서 잘 가져오는지 확인해보자.
private final Environment environment;
// UserController
@GetMapping("/health-check")
public String status(HttpServletRequest request) {
return "It's working in User-service On Port " + request.getServerPort()
+ "\ntoken secret = " + environment.getProperty("token.secret")
+ "\ntoken expiration time = " + environment.getProperty("token.expiration_time");
}
Java
복사
이처럼 잘 읽어오는지 테스트를 하기 위해 health-check API에 토큰 정보를 반환하도록 수정하고 동작을 시켜보면,
이와 같이 잘 동작하는 것을 확인할 수 있다.
외부 저장소의 Config 정보 수정사항 발생 시 갱신하기
먼저 외부 저장소의 config 값을 수정했을 때, 수정된 사항을 잘 읽어오는지 확인해보자.
token:
expiration_time: 86400000
secret: this_is_my_token_secret
YAML
복사
이와 같은 config 값을
token:
expiration_time: 8640
secret: this_is_test_token_secret
YAML
복사
이처럼 변경하고,
git commit까지 수행하자.
하지만 아까의 주소로 다시 들어가 확인해보면
이처럼 바뀐 config 값이 반영되어 있지 않다. 여기에서 user-service 애플리케이션을 재시작하게되면,
바뀐 값이 그제야 반영이 된다.
하지만 외부 저장소에서 값이 바뀔 때마다 모든 애플리케이션을 재시작하면 외부 저장소를 이용하는 이유가 많이 퇴색되므로, 애플리케이션 재시작 없이 변경된 외부 config 값을 반영하도록 만들어보자.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-actuator'
...
}
Java
복사
이처럼 actuator 라이브러리 의존성을 추가한 후,
management:
endpoints:
web:
exposure:
include: refresh, health, beans
YAML
복사
application.yml 파일에 액추에이터 설정을 추가하자. 데이터를 수집하여 보여주는 여러 액추에이터 기능 중에 refresh와 health, beans 정보들을 볼 수 있도록 하는 설정이며, 그 외에 모니터링을 위한 기능도 제공하지만 여기에서는 refresh를 사용할 것이다.
이와 같이 ~/actuator/health 혹은 ~/actuator/beans 주소로 들어가보면 액추에이터가 수집한 데이터를 볼 수 있다.
refresh의 경우 POST 요청을 보내면 내부적으로 데이터를 다시 읽도록 하여 외부 config 설정이 바뀌었을 때 애플리케이션 재시작 없이 변경된 값을 반영시킬 수 있다.
token:
expiration_time: 86400000
secret: this_is_my_token_secret
YAML
복사
외부 config 값을 다시 원래대로 돌리고
이를 로컬 저장소에 commit하고 나면
여전히 바뀌지 않은 채로 되어있다.
여기서 ~/actuator/refresh 주소로 post 요청을 보내게 되면,
수정된 사항들을 응답으로 보내고,
이처럼 애플리케이션 재시작 없이 수정된 사항을 잘 반영해온다. 이와 동일하게 Gateway에도 JWT 토큰에 관련된 구성 정보를 외부 config에서 가져오도록 반영할 수 있다.
여기서 주의할 점은 environment는 이와 같이 잘 반영이 되지만, 아래와 같이 @Value 애노테이션을 사용하는 방식은 애플리케이션 시작 시에 값을 읽어 클래스 필드값을 고정해버리기 때문에 아무리 refresh 요청을 날려도 반영이 되지 않는다.
@Component
@Data
public class UserServiceProperties {
@Value("${greeting.message}")
private String message;
@Value("${token.secret}")
private String tokenSecret;
@Value("${token.expiration_time}")
private Integer expirationTime;
}
Java
복사
HttpTrace
@Configuration
public class HttpTraceConfig {
@Bean
public HttpExchangeRepository httpTraceRepository() {
return new InMemoryHttpExchangeRepository();
}
}
Java
복사
Profiles 적용
여러 Profiles를 적용하는 방법은 Spring Cloud Config Server에서 추상화를 잘 해두었기 때문에 매우 간단하게 적용할 수 있다.
이와 같이 yml 파일을 service_name-profiles.yml 형태로 저장을 하고,
spring:
cloud:
config:
uri: http://127.0.0.1:8888
name: ecommerce
profiles:
active: dev
YAML
복사
각 마이크로 서비스마다 적용하고 싶은 profiles를 bootstrap.yml 파일에 적용해두면 끝이다.
config service에 들어가보면 잘 호출되는지 확인해볼 수 있다.
Git remote 및 Native File Repository 연결하기
이제까지 git 로컬 레포지토리에 연결해서 사용했었는데, 이를 remote에 올려놓고 관리를 해보자.
먼저 git 로컬 레포지토리를 원격 브랜치에 연결하고 파일들을 push하자.
그 후 config service의 application.yml 파일에
spring:
application:
name: config-service
cloud:
config:
server:
git:
uri: https://github.com/Z1Park/msa-practice-config.git
username: [your username]
password: [your password]
YAML
복사
이와 같이 git.url에 원격 git 주소를 넣어주면 된다. 만약 원격 repository가 pirvate로 설정되어 있다면, username과 password를 추가하면 된다.
git에 연결하지 않고 단순히 로컬 파일에만 두고 사용하고 싶다면 Native File Repository를 연결하여 사용하면 된다.
spring:
application:
name: config-service
profiles:
active: native
cloud:
config:
server:
native:
search-locations: file://${user.home}/MSA/native-repo
YAML
복사
native로 연결하는 방법은 profiles로 native를 설정해두고, git 로컬 레포지토리 연결할 때와 동일하게 yml 파일들이 저장되어있는 로컬 스토리지의 주소를 넣어주면 된다.
Spring Cloud Bus
위에서 외부 저장소의 Config가 변경되었을 때 spring actuator를 통해 refresh로 변경된 값을 반영하는 작업을 해보았다. 하지만 변경된 Conifg 파일을 사용하는 마이크로 서비스가 다수라면, 매번 모든 애플리케이션에 refresh 요청을 보내야 하는 불편함이 있다.
이를 해결하기 위해 Spring Cloud Bus를 통해 분산 시스템의 노드를 경량 메시지 브로커와 연결하여, 일괄적으로 수정사항을 반영하도록 만들어보자.
Spring Cloud Bus와 연결된 어디에든 busrefresh 요청을 보내면, 상태 및 구성에 대한 변경 사항을 연결된 노드에게 전달(Broadcast)하도록 만들 것이다.
서비스에 AMQP 추가하기
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
YAML
복사
config service에 위와 같이 actuator, bus-amqp, bootstrap에 대한 라이브러리 의존성 추가한다.
# maven
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
# gradle
implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
YAML
복사
Gateway와 user-service에도 bus-amqp 라이브러리 의존성을 추가한다.
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: refresh, health, info, beans, httptrace, busrefresh
YAML
복사
이와 같이 각 service와 config에 rabbitmq 설정 정보와 actuator의 busrefresh를 추가해준다.
여기서 중요한 것은 포트 번호는 웹 브라우저에서 접속할 때는 15672였지만, 서비스 포트번호는 5672로 설정해주어야 한다.
동작 확인하기
먼저 euraka 서버, rabbitmq 서버, config 서버와 user-service를 실행시키고,
token:
expiration_time: 864000000
secret: this_is_my_dev_token_secret
YAML
복사
이와 같이 config service 내에 저장되어 있는 ecommerce-dev.yml 값을 수정해보자.
수정 전에는 이와 같고,
token:
expiration_time: 864000000
secret: this_is_my_dev_token_secret_2
YAML
복사
이와 같이 토큰의 secret을 조금 변경한 후 git에 push하게되면
이처럼 config-service에는 git push로 인해 잘 반영이 된다.
이와 같이 요청을 보내 회원가입을 하고,
가입한 이메일과 비밀번호로 로그인을 시도하면,
바뀌지 않은 config 정보를 사용해 JWT 토큰을 발행한다.
하지만 여기서 busrefresh를 수행하게되면,
이처럼 user-serivce와 gateway 모두 update된 값이 반영된 구성 정보를 통해 JWT 토큰을 발행한다.
Config 암호화
Cloud Config 서버에 저장하는 구성 정보를 Plain text로 저장하게 되면, 해당 정보가 누출되었을 때 데이터베이스 암호나 IP 주소, 서버 접속 아이디/비밀번호 등 민감한 정보에 대해 전부 노출되게 된다.
이를 막기 위해 Config 서버에 저장할 때는 암호화(Encryption)를 수행하여 저장하고, 서비스에 구성 정보를 제공할 때 복호화(Decryption)하여 제공하는 것이 안전하다. 구성 정보 저장 시에는 암호화 되었음을 나타내기 위해 암호화된 값 앞에 {cipher}를 추가해주어야, config service에서 해당 값을 읽을 때 암호화 되었음을 자동으로 인식하고 복호화하여 다른 서비스들에게 전달하게 된다.
암호화와 복호화의 종류에는 대칭키를 사용한 암호화와 비대칭키를 사용한 암호화 방법이 있다.
•
대칭키 : 암호화와 복호와에 동일한 키를 사용하는 방식이다.
•
비대칭키 : 암호화와 복호와에 서로 다른 키를 사용하는 방식이다. 공개 키를 통해 암호화 하였다면 비공개 키를 통해 복호화하고, 비공개 키를 통해 암호화 하였다면 공개 키를 통해 복호화 한다.
대칭키를 사용한 암호화
먼저 config service에 암호화 키를 저장하기 위해 bootstrap 라이브러리 의존성과 bootstrap.yml 파일을 추가하고,
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
YAML
복사
encrypt:
key: abcdefhijklmnopqrstuvwxyz1234567890
YAML
복사
이와 같이 암호화 키를 추가해준다.
config service를 실행시킨 후
위처럼 uri + encryt로 body에 암호화하고 싶은 plain text를 넣어서 요청을 보내면, 우리가 설정한 암호화 키를 통해 body의 plain text를 암호화하여 응답으로 보내준다.
복호화는 uri + decrypt에 복호화 하고자하는 문자열을 body에 넣어 요청을 보내면 된다.
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: "{cipher}64a940bdf571a4fccde25917423aa9eca22f85b3c4b50098b687d6887399bf9b"
YAML
복사
위처럼 config 서버에서 관리하는 yml 파일에 암호화된 값을 앞에 {cipher}를 붙여서 저장하자.
그러면 이와 같이 config 서버에서는 {cipher}를 보고 암호화 된 값인 걸 판단하여 복호화하여 다른 서비스들에게 제공한다.
이와 같이 실제로 접속을 시도해보면, 비밀번호에 암호화 된 값을 그대로 넣거나 틀린 값을 넣으면 접속이 되지 않고 위에서 설정한 test1234로 입력을 해야 접속이 된다.
비대칭키를 사용한 암호화
먼저 JDK의 keytool을 사용해 비대칭 키를 생성하자.
keytool -genkeypair -alias apiEncryptionKey -keyalg RSA -dname "CN=Jiwon Park, OU=API Development, O=example.org, L=Seoul, C=KR" -keypass "1234test" -keystore apiEncryptionKey.jks -storepass "1234test"
Shell
복사
이와 같이 alias 이름, 서명 정보, 비밀번호, 저장할 이름을 추가하여 비대칭 키를 생성하고,
keytool -list -keystore apiEncryptionKey.jks -v
Shell
복사
키 정보를 확인해보면
이처럼 private 키가 생성된 것을 확인할 수 있다.
keytool -export -alias apiEncryptionKey -keystore apiEncryptionKey.jks -rfc -file trustServer.cer
Shell
복사
이후 생성한 private 키를 통해 인증서를 발행하고,
keytool -import -alias trustServer -file trustServer.cer -keystore publicKey.jks
Shell
복사
인증서를 공개 키로 전환한다.
그럼 이제 생성된 비대칭 키를 config service에 등록하고, 비대칭 키를 통해 암호화가 잘 수행되는지 확인해보자.
encrypt:
key-store:
location: file://${user.home}/MSA/keystore/apiEncryptionKey.jks
password: 1234test
alias: apiEncryptionKey
Shell
복사
이와 같이 사용할 비대칭 키 중 하나(공개/비공개)의 경로를 지정하여 넣어주고, 비대칭 키의 비밀번호와 alias를 넣어준다.
그 후 아까와 동일하게 암호화/복호화 요청을 보내보면, 이처럼 잘 동작한다. 이렇게 얻은 암호화된 값을
token:
expiration_time: 864000000
secret: '{cipher}AQBEH1olVJreIrlm2E7I/7OaKcqEcedw+jJjc0SihZ/DKlsNxXh2wIzxvr3akd8L1VAykreOrljcYyT1PFt9rwXGWcFiT/O9Aiz2v2jGdnHeamGL49d1gpaeE5M2uHXq/Liu58bZ9F5bT09Hka2aHxhXwGXKb7Si75fJ01r5DRvtNGBl1TSwryPpJcMeDD+dfgqD+QshD8YJT55ETx7Uj/97UgMDwhIoPZ1PTv/J7b5Fh3RH9xSgX/etyAvTKugHED9yRga6hzyi3rMsyIGpqK7fttifl7577nr0ybGPVLtdMZ/3nw1QC1W4FNqK3s2eWH6c1+9eB9jyOa/Z1z6/fYBtpv6AU6NO2D8RMFX8nRAGYIUpGpi4ytlXRdieMUVEcuDqUcBrEG7PvdHVfZpsfIbb'
YAML
복사
동일하게 {cipher}를 추가하여 사용하고자 하는 구성 정보에 넣어두면,
이처럼 값을 복호화하여 각 서비스에 전달하게 된다.
공통 config 정보 통합하기
JWT 토큰을 발행하는 user-service와 JWT을 통해 인가를 확인하는 Gateway의 구성 정보에는 각각 token.secret에 대한 정보가 담겨있다. 하지만 현재 user-serivce에서는 config 서버의 user-serivce.yml 파일을 읽어오고, Gateway는 ecommerce.yml 파일을 읽어가기 때문에 이를 통합하기는 어렵다.
Config 서버에는 상위 파일 개념이 존재하는데, 이를 활용하여 위 문제와 여러 profiles에서 공통으로 사용되는 정보들을 묶을 수 있다.
위처럼 ecommerce의 dev profile 정보를 요청을 해도, config 서버는 기본적으로 ecommerce-dev.yml 파일 뿐만 아니라 상위 파일인 ecommerce.yml 파일까지 조회해서 보내준다. 이를 활용하여 ecommerce.yml 파일에 여러 profiles에서 공통적으로 사용되는 구성 정보들을 담아두면 조금도 관리하기에 용이해진다.
거기에 더해 위에서 언급한 문제인 user-service.yml 파일과 ecommerce.yml 파일 간의 차이에 있어서도, application.yml 파일이 존재한다면 이처럼 application.yml 파일이 상위 파일 개념으로 각 서비스에 같이 전달된다. 때문에 user-serivce와 Gateway에서 같이 사용하는 JWT 토큰에 관련된 정보는 application.yml 파일에 저장하면 두 곳에서 관리할 필요 없이 간편하게 관리할 수 있다.