개요
기존에 Jenkins 자동화 배포를 할때 민감한 정보(db 정보 및 암호화 키 ...)가 기입되어 있는 설정파일(yml, properties ...) 파일은 gitignore를 적용하고 직접 Jenkins 서버에 접속해 넣어주고 빌드를 시켰다.
하지만 프로젝트가 많아졌을때 설정파일을 최신화 하게 되면 직접 프로젝트 루트에 설정파일을 직접 넣기 너무 귀찮고 빌드도 다시 시켜야 했다.
Spring Cloud Config를 사용하게 되면 한 곳에서 모든 설정파일을 관리하고, 수정이 있더라도 다시 배포하지 않아도 되는 장점이 있다.
단점은 한마디로 보안이 굉장히 중요하다.
물론 보안은 어느곳에도 중요하지만 만약 Spring Cloud Config 암호화 대칭값이 노출되거나, 암호화 및 복호화 기능에 적용한 Security의 인증값이 노출된다면 모든 서비스의 설정값들을 확인할 수 있게 된다.
현재 구조는 아래 그림과 같다.
대략 설명하자면,
Spring Cloud Config Server와 Jenkins Server에 동일한 환경변수를 설정하고 Cliend Application 이미지 빌드 후 컨테이너 저장소에 Push한다. 이때 환경변수를 주입해 해당 컨테이너가 실행될때, 해당 서버에 환경변수를 똑같이 등록해준다.
Application 실행 시 등록된 환경변수로 Spring Cloud Config Server에서 인증 및 복호화를 진행해 설정값을 받아온다.
구축 예제
이전에 Spring Cloud Config를 사용할때 정리해둔 글이 있는데, 그때와 Spring boot에 버전 차이가 있어 설정하는 방법이 약간 다르다.
구현 스팩
- Amazon Corretto JDK - 11
- Spring Boot Version 2.5.4
- Spring Cloud Version : 2020.0.5
- Gradle, Jar
- Bitbucket
1. Bitbucket 인증 설정
먼저 Bitbucket Private Repository를 생성한다.
일단 로컬에서 Spring Cloud Config Server를 구축할 것이고, Bitbucket에 ssh로 접속할 것이기 때문에 인증 시 필요한 ssh key를 생성한다.
$ ssh-keygen -m PEM -t rsa -b 4096 -f ~/.ssh/config_server_rsa
기본적인 id_rsa는 다른곳에서 사용하기 때문에 config_server_rsa라는 이름에 새로운 ssh key를 생성했다.
정상적으로 생성됬으면 해당 public key를 Bitbucket에 등록해야 한다.
$ cat ~/.ssh/config_server_rsa.pub
그런데 ssh key로 접속할때 기본적으로 id_rsa로 인증을 시도하기 때문에 생성한 config_server_rsa로 인증을 할 수 없다.
그래서 ~/.ssh 경로에 config 파일을 생성해 bitbucket.org-config_server라는 호스트에 ssh 접속을 할때 위에서 생성한 config_server_rsa ssh key로 인증하도록 설정하였다.
Host bitbucket.org-config_server
HostName bitbucket.org
UseKeychain yes
IdentityFile ~/.ssh/config_server_rsa
IdentitiesOnly yes
로컬 테스트가 아닌 실재 Config Serve 배포 시 해당 서버에도 동일한 설정을 해주어야 한다.
2. Spring Cloud Config Server 구축
build.gradle에Spring Cloud Config Server 의존성을 주입한다.
plugins {
id 'org.springframework.boot' version '2.5.4'
...
}
...
ext {
set('springCloudVersion', "2020.0.5")
}
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-config-server'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
...
빌드를 하고 main application에 @EnableConfigServer 어노테이션을 추가한다.
@EnableConfigServer
@SpringBootApplication
public class ConfigServerExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerExampleApplication.class, args);
}
}
application.yml 파일은 아래와 같이 작성한다.
server:
port: 8080
spring:
cloud:
config:
server:
git:
uri: git@bitbucket.org-config_server:devbeekei/config-files.git # Bitbucket ssh 접속 경로
default-label: main # 기본 브랜치
ignore-local-ssh-settings: false # local ssh 세팅으로 접속하도록 설정
git.url에는 위에서 생성한 Bitbucket Repository ssh 접속 경로를 입력해주는데,
위에서 설정한 bitbucket.org-config_server alias로 접속을 시도해야 새로 생성한 config_server_rsa ssh key를 이용해 인증이 가능하다.
이제 위에서 생성한 Bitbucket Private Repository에 설정파일을 등록해보자.
datasource:
url: jdbc:mysql://localhost:3306/example
username: root
password: root
auth:
token-secret: "Fea1dqjTgB9fd1dVL1RN"
파일을 등록했다면 서버를 시작해보자.
만약 서버 실행 시 reject HostKey: bitbucket.org (atlassian.com)라는 오류가 발생한다면 아래 추가작업을 해준 뒤 서버를 시작해야 한다.
만약 Bitbucket에 처음 접속하는 것이라면 아래와 같은 문구가 출력되는데 yes를 입력해야 접속이 가능하다.
The authenticity of host 'bitbucket.org (104.192.141.1)' can't be established.
RSA key fingerprint is SHA256:zzQO12BEiasfAADWuE8AikJYKasfafaxvSc0ojez9YXaGp1A.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?
하지만 Spring Cloud Server에서 자동적으로 yes를 입력해줄 순 없을 것이다.
그렇기 때문에 Bitbucket 접속 테스트를 통해 yes를 입력하면 ~/.ssh 경로에 known_host라는 파일이 생기는 것을 확인할 수 있다.
(원래 생성되어 있는 경우는 내용이 추가되었을 것이다.)
$ ssh -T git@bitbucket.org
yes를 입력하고 You can use git to connect to Bitbucket. Shell access is disabled라는 문구가 출력된다면 접속에 성공한 것이다.
정확히는 모르겠지만 known_host 파일에 ssh key 접속 정보를 기억하는 듯 하다.
요 부분은 추후에 알아보고 알게된다면 업데이트 하도록 하겠다.
만약 로컬이 아닌 EC2에 구축한다고 했을때도 같은 방법으로 하면 된다.
서버가 정상적으로 실행되었다면 Repository에 등록한 정보를 정상적으로 읽어 오는지 확인해보자.
Spring Cloud Config Server는 아래와 같은 endpoint를 가지고 있다.
GET /{application}/{profile}[/{label}]
GET /{application}-{profile}.yml
GET /{label}/{application}-{profile}.yml
GET /{application}-{profile}.properties
GET /{label}/{application}-{profile}.properties
http://localhost:8080/example/dev로 접속해보면 main 브랜치에 저장된 example-dev.yml의 정보가 아래와 같은 Json 형식으로 출력된다.
여기까지 왔다면 기본적인 Spring Cloud Config Server는 구축된 것이다.
3. 정보 암호화
당연한 말이지만 위처럼 정보가 누구나에게 노출되면 큰일이다. 암호화 처리를 해줘야 한다.
Spring Cloud Config에서는 대칭키와 비대칭키를 이용한 암호화 및 복호화를 지원하고 있는데 대칭키가 유출되면 위험이 크므로 비대칭키를 이용해서 암호화 하는것을 권장한다.
하지만 개인적인 생각으론 비대칭키를 사용할 경우 관리 포인트가 많아지고,
Jenkins에서 application 빌드 후 실제 운영 서버에 Jar파일을 실행하면 비대칭키의 위치를 찾지 못하였다.
왠지 Jenkins 서버안에 있는 비대칭키의 경로를 그대로 가져와 찾으려고 하기 때문인것 같은데, 이 부분은 더욱 자세히 알아보고 업데이트 하도록 하겠다.
관리가 쉬운 대칭키를 사용하기로 했고, 대칭키를 환경변수로 등록해 서버에서만 알 수 있도록 구현하였다.
먼저 Basic Auth로 사용할 username, password 그리고 암복호화 대칭키를 환경변수에 등록한다.
$ vim ~/.bash_profile
// .bash_profile 파일에 추가
export CONFIG_SERVER_GIT_URL=git@bitbucket.org-config_server:devbeekei/config-files.git
export CONFIG_SERVER_ENCRYPT_KEY=abcdefg
export CONFIG_SERVER_USERNAME=config_server_admin
export CONFIG_SERVER_PASSWORD=config_server_password
$ source ~/.bash_profile
application.yml에 아래와 같이 설정한다.
server:
port: 8080
spring:
cloud:
config:
username: ${CONFIG_SERVER_USERNAME} # Basic Auth Username
password: ${CONFIG_SERVER_PASSWORD} # Basic Auth Password
server:
encrypt:
enabled: false
git:
uri: ${CONFIG_SERVER_GIT_URL}
default-label: main
ignore-local-ssh-settings: false
encrypt:
key: ${CONFIG_SERVER_ENCRYPT_KEY} # 암복호화 대칭키
만약 encrypt.enabled를 설정하지 않거나 true로 설정하게 된다면 http://localhost:8080/example/dev로 접속했을때 복호화된 값이 그대로 노출되게 된다.
모두 설정했다면 했다면 build.gradle에 spring boot start web과 security dependency를 추가하고 Security 설정을 해준다.
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.cloud.config.username}")
private String username;
@Value("${spring.cloud.config.password}")
private String password;
@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser(username).password(passwordEncoder().encode(password)).roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
}
@Component
public class CustomAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 - " + authEx.getMessage());
}
@Override
public void afterPropertiesSet() {
setRealmName("CONFIG SERVER");
super.afterPropertiesSet();
}
}
위 설정까지 마쳤다면 서버를 다시 시작한다.
Spring Cloud Config에서는 아래 API를 통해 암호화 및 복호화를 할 수 있다.
- 암호화 : POST http://config-server.com/encrypt
- 복호화 : POST http://config-server.com/decrypt
만약 Basic Auth 인증에 실패했다면 아래처럼 암복화가 불가능하게 된다.
이렇게 얻은 암호화된 값으로 다시 Bitbucket Repository에 등록한 example-dev.yml을 수정해주자
datasource:
url: "{cipher}40073df1ce28290aede4d0557df36f501e4a496bcff0ccd826e089a94be1a4dea4a7af9d39ce8e6e4bd74702defb220c71760e7becfc7cb981f27a0569f37ebb"
username: "{cipher}8e1038b2bb5073ef587f564d15dee32a3e19b8ba4605f635da29bb166a68a9f5"
password: "{cipher}afd217557e72242c4e36444753295c656deaa65fc6298be7c9af8036245b54e0"
auth:
token-secret: "{cipher}dbb893d44f437f86b83d3451760070a10c83a5e08a49ce070d166db088b0df32f343525b4cfab20a9098e9aa3b073255"
암호화 된 값을 설정할때는 문자열 앞에 {cipher}를 추가해 암호화된 문자라는것을 알려줘야 한다.
다시 http://localhost:8080/example/dev로 접속해보면 아래처럼 암호화된 값이 출력된다.
절대적으로 username, password, encrypt key는 유출되서는 안된다!!
4. Client Application 설정
Client Application에는 Spring Cloud Config Client와 Spring Boot Actuator를 주입한다.
implementation group: 'org.springframework.cloud', name: 'spring-cloud-config-client', version: '3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Spring Boot Actuator는 어플리케이션을 모니터링하고 관리하는 기능이다.
여기서는 설정값의 변경이 있을때 이를 새로고침해주기 위해 사용하는데, 설정값이 바뀌더라고 빌드 및 배포를 다시 하지 않아도 된다.
Client Server에서도 마찬가지로 username, password, 대칭키를 환경변수로 등록해 사용할 것이므로 application.yml을 아래처럼 설정해준다.
spring:
application:
name: Example Application
profiles:
active: dev
config:
import: "optional:configserver:http://localhost:8080/" # Config Server URL
cloud:
config:
name: example
profile: dev # example-dev.yml을 읽어온다.
username: ${CONFIG_SERVER_USERNAME} # Basic Auth Username
password: ${CONFIG_SERVER_PASSWORD} # Basic Auth Password
encrypt:
key: ${CONFIG_SERVER_ENCRYPT_KEY}
management:
endpoints:
web:
exposure:
include: refresh # 설정값 새로고침 Endpoint
5. Jenkins 설정
Config Server에서 환경변수로 설정한 username, password, 대칭키를 똑같이 Jenkins 환경변수에도 등록해줘야 한다.
환경변수가 등록되었으면 Dockerfile을 빌드해줄때 변수를 전달한다.
...
stage('Clean Build Test') {
steps {
sh "SPRING_PROFILES_ACTIVE=${BRANCH_NAME} ./gradlew clean build test"
}
}
stage('Docker Image Build') {
steps {
script {
image = docker.build("${IMAGE_STORAGE}/${IMAGE_NAME}", "--build-arg CONFIG_SERVER_ENCRYPT_KEY=${CONFIG_SERVER_ENCRYPT_KEY} --build-arg CONFIG_SERVER_USERNAME=${CONFIG_SERVER_USERNAME} --build-arg CONFIG_SERVER_PASSWORD=${CONFIG_SERVER_PASSWORD} .")
}
}
}
...
Dockerfile에서는 전달받은 값들을 배포할 서버에 환경변수로 등록해준 후 Application을 시작한다.
FROM amazoncorretto:11-alpine-jdk
MAINTAINER DevBeekei <devbeekei.shin@gmail.com>
EXPOSE 8080
ARG CONFIG_SERVER_ENCRYPT_KEY
ARG CONFIG_SERVER_USERNAME
ARG CONFIG_SERVER_PASSWORD
ENV CONFIG_SERVER_ENCRYPT_KEY=$CONFIG_SERVER_ENCRYPT_KEY
ENV CONFIG_SERVER_USERNAME=$CONFIG_SERVER_USERNAME
ENV CONFIG_SERVER_PASSWORD=$CONFIG_SERVER_PASSWORD
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} deploy-app.jar
CMD java -jar ./deploy-app.jar
이제 Jenkins에서 빌드를 진행해 서버가 정상적으로 배포되는지 확인해 보자.
서버가 정상적으로 작동한다면 Spring Cloud Config Server에서 설정값을 잘 불러온 것이다.
이렇게 Jenkins와 Docker를 활용해 Spring Cloud Config를 도입해봤다.
한번 구축해놓고 나면 gitignore를 설정하고 직접 YAML 파일을 볼륨에 올리지 않아도되서 프로젝트가 많아지거나 변경이 있을때 매우 편리하다.
중요한 점은 중요 키 값이 만약 유출된다면 모든 프로젝트의 정보를 복호화 할 수 있기 때문에 절대적으로 유출되서는 안된다.
설명을 너무 길고 자세히 하지 못하였는데, 만약 더욱 설명이 필요한 부분이나 틀린 내용 및 오류가 있을 경우 댓글로 달아주시면 감사하겠습니다.
'Spring' 카테고리의 다른 글
플러그인을 사용해 라이선스 보고서 생성하기 (0) | 2022.09.06 |
---|---|
Spring Boot + AOP + Sentry 로깅 및 오류 모니터링 하기 (0) | 2022.07.19 |
Spring Docs + Swagger 설정하기 (Spring Rest Docs 비교) (2) | 2022.05.02 |
스프링 배치(Spring Batch)를 이용한 데이터 마이그레이션 (2) | 2022.02.24 |
스프링 배치(Spring Batch) 활용하기 (0) | 2022.02.22 |