우리는 Spring
프로젝트를 만들 때, 설정 파일로 application.yml
을 작성한다. 그런데 만약 서비스가 운영되고 있는 와중에, RDS 문제라던가 각종 상황으로 application.yml
을 변경해야할 상황이 왔다고 생각해보자.
그렇다면 우리는 운영되고 있는 서비스를 중단한 다음, 파일을 수정한 다음, 다시 빌드 후 배포를 시작해야 할 것이다. 여기서 발생하는 문제는, 우리가 운영하고 있는 서비스가 중단된다는 점이고, 만약 무중단 서비스를 구축했다고 하더라도 다시 빌드 후 배포 과정을 거쳐야 한다는 점은 변함이 없다.
다른 문제도 생각해보자. 이전 프로젝트에서 스프링 백엔드 서비스를 만들었던 것을 생각해 보자. 여기서 우리는 깃허브에 application.yml
설정파일을 public으로 공유했다. 그런데 이렇게 된다면, 내 RDS에 연결하는 id와 password가 모두 노출되게 된다. 물론 이를 바꾸기 위해 gitignore
나 private 설정으로 바꿔서 막을 수 있다. 하지만 내 경우, 다른 application.yml
의 설정은 공유하고 싶지만, 민감한 정보는 숨기고 싶은데 이걸 구현할 수 있을까?
이러한 두 문제점은 Spring Cloud Config
을 통해 해결할 수 있다.
Spring Cloud Config
를 이용하게 된다면, Private Github에 민감한 정보가 담긴 yaml
파일을 암호화 한 채로 보관한 다음, 이를 Config Server
를 통해 Config Client
들이 받아오게 된다.
그 과정에서의 복호화는 Config Server
와 Config Client
들 간의 통신 과정을 통해 이루어지게 된다. (대칭키, 비대칭키 둘 다 사용가능하다.) 또한 암호화도 Config Server
에서의 키를 토대로 만들어 저장한다.
결국 이렇게 될 경우 앞서 말한 문제점인 설정파일의 변경 또한 Private Github의 정보를 받아오는 것이기 때문에, 빌드를 거칠 필요가 없고, 민감한 정보의 은닉 또한 Private Github에 저장한 채로 암호화 하여 받아오는 것이기 때문에 목적을 달성할 수 있다.
이 과정에서 나는 대칭키의 방식을 사용하기로 했고, 이 구조를 통해 실습을 거쳐보도록 하자.
먼저 파일을 숨겨서 저장할 Private Github를 만들어주자.
이후 깃허브에 설정에 필요한 yaml파일들을 넣어준다.
jh-test.yml
spring:
datasource: hello
username: test
password: notpassword
aws-jwt.yml
spring:
datasource:
url: 주소
username: 아이디
password: 비밀번호
나는 테스트용 파일인 jh-test.yml
파일과 토이프로젝트의 rds설정이 담긴 aws-jwt.yml
파일을 넣어줬다.
여기서 중요한건 이름이
/{application}-{profile}.yml
이런식으로 되야한다는 점이다. HTTP 서비스를 통해서 인식할 때curl localhost:8888/{application}/{profile}
이런 규격으로 인식하기 때문이다.
그럼 Config Server
를 만들자.
Java
는 17버전을 썼고, Spring Boot
는 2.7.4버전을 썼다.
설치한 의존성은 다음과 같다.
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server:3.1.4'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
여기서 Spring Boot Actuator
어플리케이션을 모니터링하고 관리하는 기능 중 하나인데, 즉 실행하고 있는 어플리케이션을 http통신 등으로 제어할 수 있는 것이다.
우리는 이것을 yaml파일의 변경 등에 사용할 것이다.
window 기준으로 설명하겠다.
cmd를 열어 다음과 같이 명령어를 입력한다.
> ssh-keygen -M PEM -t ecdsa -b 256 -C "Github 계정" -f 키파일명
이후 passphrase
부분을 입력하라고 나오는데 이 부분은 생략하고 넘어갔다.
그러면 C:₩Users₩사용자명₩.ssh
폴더에 공개키와 비밀키가 생성이 완료되었을 것이다.
그럼 pub
파일인 비밀키를 메모장으로 열어보자.
이런 형태가 나오면 정상으로 생성이 된 것이다.
이제 이것을 복사해서 Private Github에 넣어주자.
New SSH Key를 클릭한다.
복사한 키를 넣어준다.
이런 화면이 나왔다면 성공한 것이다.
이제 host-key와 알고리즘을 알아내 보자.
> ssh-keyscan -t ecdsa github.com
이후 private github과 연결하기 위한 ssh주소도 알아내자.
해당 깃헙에 들어가 ssh를 클릭하면 나온다.
이제 이 내용들을 전부 yaml내용에 적용시키면 된다.
application.yml
server:
port: 8081
spring:
cloud:
config:
server:
git:
uri: git@github.com:깃헙닉네임/private주소
default-label: main
ignore-local-ssh-settings: true
private-Key: |
-----BEGIN EC PRIVATE KEY-----
Private Key 내용
-----END EC PRIVATE KEY-----
host-key: AAAA...Host Key 내용
host-key-algorithm: ecdsa-sha2-nistp256
encrypt:
enabled: false
encrypt:
key: 평문은 마음대로 설정 가능.
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: false
위의 host-key-algorithm
까지의 내용은 위의 정보들을 붙여 넣은 것이고,
아래의 config
내의 encrypt
설정은 꼭 false로 해줘야한다. 왜냐하면 이를 true로 하게 된다면 통신 과정에서 모든 내용이 복호화 된 채로 나오기 때문이다.
이후 아래의 encrypt
는 암호화와 복호화를 위한 키 설정을 말하며, 원하는 내용을 입력하면 된다.
management
는 actuator
의 설정이며, 실행되고 있는 애플리케이션을 종료하지 않고 refresh
를 입력하여 yaml 변경파일을 가져오는 역할을 한다.
이제 실행을 해보자. Postman을 통해 통신을 해본다.
앞서 말했듯이 /{application}-{profile}.yml
의 형식의 파일이 curl localhost:8888/{application}/{profile}
로 변환된 방식으로 통신을 하므로 그에 맞게 해주자.
제대로 통신이 되고 있는 모습이다.
그런데 지금 우리가 통신하고 있는 내용이 다른 사람도 같은 URL로 통신한다고 가정해보자.
그렇다면 다른 사람도 우리가 주고받는 내용을 전부 볼 수 있다. 그러면 이러한 정보를 github에 숨겨놓은 의미가 없다.
따라서 해당 내용을 암호화 해보도록 하자.
postman을 키고 http://config서버 주소/encrypt
에 POST 요청으로 암호화 하고싶은 평문을 보내자.
이제 이 값을 private git repository의 yml에 넣어서 수정한다.
jh-test.yml
spring:
datasource: "{cipher}91a41504b9a90b13ab865f45d897a1ed730cf6cf35449933022526bd3f632d5d"
username: test
password: notpassword
암호화된 정보는 모두 이렇게 ""
로 묶은 다음 앞에 {cipher}
를 붙여줘야 한다.
이제 Actuator
를 통해 refresh를 해주고
다시 동일하게 get으로 받으면 암호화 된 정보를 볼 수 있다.
실제 rds 파일을 기반으로 하고있는 aws-jwt.yml
파일도 암호화 하여 수정하자.
그런데 이렇게 만들어도 우리는 암호화가 되었다는 것만 알 수 있지, 이 값을 실제로 해석하고 있는지는 알 수가 없다.
따라서 복호화를 알아보기 위한 Client
를 생성해보자.
자바와 스프링부트의 버전은 동일하며, 의존성은 다음과 같다.
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
이후 yaml설정을 해주자
application.yml
server:
port: 8082
spring:
config:
import: optional:configserver:config서버 주소
application:
name: jh
profiles:
active: test
encrypt:
key: config 서버에서 썼던 키
대칭키이므로 키는 config server
에서 썼던 키와 동일하게 설정해주자.
그리고, private git repository의 파일명이/{application}-{profile}.yml
구조로 되어있으므로 그에 맞게 값을 넣어준다.
이제 yaml파일에 있는 값을 저장하기 위한 클래스를 만들어보자.
MyConfig.java
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Getter
@Setter
@ToString
@Service
public class MyConfig {
@Value("${spring.datasource}")
private String data;
@Value("${spring.username}")
private String username;
@Value("${spring.password}")
private String pw;
}
이후 이 값들을 표현해 줄 Controller
도 제작한다.
ConfigController.java
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class ConfigController {
private final MyConfig myConfig;
@GetMapping("/data")
public ResponseEntity<String> getData() {
System.out.println(myConfig.getData());
return ResponseEntity.ok(myConfig.getData());
}
@GetMapping("/username")
public ResponseEntity<String> getUsername() {
System.out.println(myConfig.getUsername());
return ResponseEntity.ok(myConfig.getUsername());
}
@GetMapping("/pw")
public ResponseEntity<String> getPassword() {
System.out.println(myConfig.getPw());
return ResponseEntity.ok(myConfig.getPw());
}
}
이제 client를 postman을 통해 통신해보자.
성공적으로 통신이 되며, 암호화 된 hello도 복호화 되어서 잘 나온다.
이제 테스트를 해봤으니 실제 rds설정을 해보자.
build.gradle
에 의존성을 추가해주자.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
이 후 yml파일도 수정한다.
server:
port: 8082
spring:
config:
import: optional:configserver:config 서버주소
cloud:
config:
name: aws
profile: jwt
encrypt:
key: config 서버에서 썼던 키
이제 연결을 위해 ConfigController.java
와 MyConfig.java
는 지우고 실행해보자.
실행하면 Spring Data JDBC
와 같이 실제 DB와 연결되지 않을 시 오류를 뿜는 의존성들이 문제없이 실행되고 있음을 알 수 있다.
즉 연결이 제대로 되었다는 뜻이다.
그런데 지금까지의 Client에는 중대한 보안적 이슈가 있는데 바로, application.yml
에 대칭키가 노출된다는 뜻이다.
만약 이게 git repository에 public으로 올라갔을 경우 대칭키를 보고, 다들 해당 rds의 아이디와 비밀번호를 가로챌 수 있다는 뜻이다.
물론 gitignore과 private을 쓰면 막을 수 있지만, 그건 맨 앞에서 말했던 것과 같이 우회적인 방법일 뿐이다.
따라서 이것을 막기위해 환경변수를 통해 값을 숨길 것이다.
intelliJ IDEA
기준으로 설명한다. 다른 idea
를 사용할 경우 다를 수 있다.
우측상단에 있는 구성 편집을 클릭한다.
환경변수의 우측 아이콘을 클릭한다.
사용하고싶은 이름과, 숨겨야할 값을 넣는다.
yaml파일을 수정하자.
application.yml
server:
port: 8082
spring:
config:
import: optional:configserver:config server
application:
name: jh
profiles:
active: test
cloud:
config:
name: aws
profile: jwt
encrypt:
key: ${ENCRYPT_KEY} # 환경변수의 이름을 넣는다.
이제 실행하면 동일하게 실행되며, 정보 또한 숨겨진다.
일단 config server
가 local에서 실행되었으므로, 이것을 인스턴스에 배포해 실행하게 만들었다.
스프링 애플리케이션의 인스턴스 배포 및 실행방법은 이전글을 참조하길 바란다.
이제 이러한 과정을 일전의 프로젝트 백엔드에 적용시켜 보자.
일단 build.gradle
에 해당 의존성을 적용한다.
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-starter-config'
...
}
이후 yaml파일을 변경하자.
application.yml
spring:
config:
import: optional:configserver:config 서버 주소
cloud:
config:
name: aws
profile: jwt
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
show_sql: true
logging:
level:
com.tutorial: debug
jwt:
secret: ${JWT_SECRET_KEY}
encrypt:
key: ${ENCRYPT_KEY}
일전의 rds가 적혀있는 부분을 지우고, config
를 추가했으며 config
의 대칭키와 동시에 jwt의 시크릿키 또한 환경변수로 설정해놨다.
이제 이것을 로컬에서 실행해보자.
제대로 실행이 된다.
이제 백엔드 인스턴스에서 git pull
을 하고 변경점을 불러오자.
하지만 환경변수 적용은 모두 intelliJ IDEA
의 로컬과정에서 적용한 것이므로, 리눅스 환경에서는 따로 적용해줄 필요가 있다.
따라서 리눅스의 환경변수 설정을 열어보자.
$ vi /etc/profile
export 환경변수명=환경변수값
그러면 이런 형식으로 적은 다음 저장하면 된다.
이후 build 후 배포해보자.
정상적으로 실행이 된다. 적용이 완료된 것이다.