Spring Cloud Config - 개념과 적용

june·2022년 10월 10일
1
post-thumbnail

🌱Spring Cloud Config을 쓰는 이유

우리는 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 ServerConfig Client들 간의 통신 과정을 통해 이루어지게 된다. (대칭키, 비대칭키 둘 다 사용가능하다.) 또한 암호화도 Config Server에서의 키를 토대로 만들어 저장한다.

결국 이렇게 될 경우 앞서 말한 문제점인 설정파일의 변경 또한 Private Github의 정보를 받아오는 것이기 때문에, 빌드를 거칠 필요가 없고, 민감한 정보의 은닉 또한 Private Github에 저장한 채로 암호화 하여 받아오는 것이기 때문에 목적을 달성할 수 있다.

이 과정에서 나는 대칭키의 방식을 사용하기로 했고, 이 구조를 통해 실습을 거쳐보도록 하자.

🌱실습

🌼 Private Github, Config Server 생성

먼저 파일을 숨겨서 저장할 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파일의 변경 등에 사용할 것이다.

🌼 Config Server - Application.yml 설정

🌳Private Github 연결

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는 암호화와 복호화를 위한 키 설정을 말하며, 원하는 내용을 입력하면 된다.

managementactuator의 설정이며, 실행되고 있는 애플리케이션을 종료하지 않고 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파일도 암호화 하여 수정하자.

🌼 Config Client

🌳 복호화

그런데 이렇게 만들어도 우리는 암호화가 되었다는 것만 알 수 있지, 이 값을 실제로 해석하고 있는지는 알 수가 없다.

따라서 복호화를 알아보기 위한 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 설정

이제 테스트를 해봤으니 실제 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.javaMyConfig.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 후 배포해보자.

정상적으로 실행이 된다. 적용이 완료된 것이다.

profile
초보 개발자

0개의 댓글