Sign In
기술이야기

Spring Properties를 외부 저장소와 연동하기

전지훈
Status
Empty

들어가며

SaaS(Software As A Service)가 보편화되며 아마 많은 개발자가 이를 구현하기 위해 Twelve-Factor를 읽어보았을 것으로 생각된다. 이 글은 Twelve-Factor에서 설정에 대해 어떻게 Spring Boot를 사용하는 개발자들이 외부의 설정 저장소에 접근하여 설정을 읽을 수 있을지 구현에 관해 이야기하고자 한다.
이미 대부분의 스타트업들은 클라우드 환경에서 시작하고 있으며, 국내 IT 대기업들은 자체 클라우드를 구축하고, IT 업계 외의 사업을 주로 하는 기업들도 클라우드로 옮겨가고 있다. 주된 방향성은 클라우드 환경에 있지만 아직 온프레미스를 주로 사용하는 조직도 있기에, 여기서는 온프레미스와 클라우드 환경에 따라 어떻게 설정을 달리할 수 있는지 다뤄보려고 한다.

Spring Externalized Configuration

본문에서 다루고자 하는 모든 개발 방법의 기반은 Spring Documentation에서 이미 기술하고 있는 내용에 대한 응용이다. 또한 외부에서 설정을 읽는 부분에 대한 많은 지원을 이미 Spring Cloud Config에서 제공하고 있으니, 사용하려는 환경이 이미 Spring Cloud Config에서 지원하고 있다면 사용하는 것이 좋다. 이 글은 Spring Cloud Config보다는 좀 더 바퀴를 만드는 방법에 대해 다룬다.

본문이 도움될 만 한 상황

아래와 같은 상황에서 이 글이 도움이 될 것이라 생각된다.
Spring Cloud Config를 조금 더 가볍게 모듈화하거나, 자신의 조직에 맞게 구성하고 싶은 경우.
전체 혹은 일부 설정에 대해 Spring Cloud Config에서 지원하지 않는 외부 설정 저장소를 사용하려는 경우.
혹은 아래와 같이 application.properties를 커스터마이징하려는 경우.
spring: # 평문으로 소스 코드에 기재 가능한 경우 jackson: date-format: yyyy-MM-dd'T'HH:mm:ss # 민감 정보로 외부 저장소로 분리를 원하는 경우 datasource: url: "{external/AAA}SPRING_DATASOURCE_URL" username: "{external/AAA}SPRING_DATASOURCE_USERNAME" password: "{external/AAA}SPRING_DATASOURCE_PASSWORD"

외부 환경에서 설정을 개발자가 직접 호출하도록 개발하기

Spring Cloud Config에서 지원하지 않는 환경에 설정을 저장하고, 불러와야 하는 경우가 있다. 사내에서 자체 구축한 시스템을 사용하는 경우이다. 이런 경우 EnvironmentPostProcessor를 진입점으로 간주하고 원하는 결과를 이룰 수 있다.

EnvironmentPostProcessor

EnvironmentPostProcessor는 ApplicationContext가 초기화되기전에 애플리케이션의 환경 설정을 변경할 수 있으며 아래 인터페이스를 가지고 있다.
public interface EnvironmentPostProcessor { void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application); }
ConfigurationEnvironment: getPropertySources()를 통해 MutablePropertySources를 사용할 수 있으며, 여기서 property를 추가하거나, 우선순위를 조절하여 덮어쓸 수 있다.
MutablePropertySources propertySources = environment.getPropertySources(); Map<String, Object> myMap = new HashMap<>(); myMap.put("xyz", "myValue"); propertySources.addFirst(new MapPropertySource("MY_MAP", myMap));
따라서 내부 구현을 통해 외부 설정 저장소에서 설정값을 불러와 우선순위를 높여 PropertySources에 저장하면 외부 설정 저장소의 설정값을 애플리케이션에서 참조할 수 있다.

CSV 예제

단순한 예제로 CSV에서 설정을 읽어와 사용하는 경우를 구현해 보자. custom.greeting이라는 설정에 Hello world!를 출력하고자 한다.
application.yaml
custom.greeting: Hello world!
HelloWorldController.java
@RequestMapping("/") @RestController public class HelloWorldController { private final String greeting; public HelloWorldController(@Value("${custom.greeting}") String greeting) { this.greeting = greeting; } @GetMapping public String helloWorld() { return greeting; } }
CsvEnvironmentPostProcessor.java
public class CsvEnvironmentPostProcessor implements EnvironmentPostProcessor { public static final String CSV_ENVIRONMENT_FILE_PATH = "environment.csv.path"; public static final String PROPERTY_SOURCE_NAME = "csv"; public static final String CSV_DELIMITER = ","; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { Map<String, Object> properties = readProperties(environment.getProperty(CSV_ENVIRONMENT_FILE_PATH)); environment.getPropertySources().addFirst(new MapPropertySource(PROPERTY_SOURCE_NAME, properties)); } private Map<String, Object> readProperties(String filePath) { try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { return reader.lines() .map(line -> line.split(CSV_DELIMITER)) .collect(Collectors.toUnmodifiableMap(elements -> elements[0], elements -> elements[1])); } catch (IOException e) { throw new RuntimeException(e); } } }
EnvifronmentPostProcessor의 구현체로 CsvEnvironmentPostProcessor로 명명한다.
ignite.environment.csv.path로 CSV 파일의 위치를 프로퍼티 파일로 입력받는다.
해당 위치의 CSV 파일을 읽어 Map<String, Object>로 변환하고, PropertySource의 가장 앞에 삽입한다.
CSV 파일을 생성한다.
custom.greeting,Hello world(From CSV)
application.yaml 파일에 아래와 같이 CSV 파일의 경로를 추가한다.
environment.csv.path: ${실제 경로 입력}/resources/static/configuration.csv custom.greeting: Hello world!
META-INF/spring.factories 파일을 추가한다.
org.springframework.boot.env.EnvironmentPostProcessor=com.ignite.configuration.CsvEnvironmentPostProcessor
위까지의 설정을 완료한다면 아래와 같은 결과 화면을 볼 수 있다.

EnvironmentPostProcessor를 응용하여 스타터를 조직에 전달

위와 같은 방식을 이해한다면, 조직에서 잘 알려지지 않은 외부 설정 저장소를 사용하려 할 때를 공통 모듈로 스프링 부트 스타터로 만들어 제공할 수 있다. 최종 결과물로서 예를 들자면 아래와 같은 설정값으로 동작하게 할 수 있을 것이다.
environment-providers: csv: path: /home/user/file1.csv http: url: https://some-storage.com/configuration/develop aws: secrets-manager: name: a-project-product
이를 동작시키기 위해서는 각각 "environment-providers.csv.path", "environment-providers.http.url", "environment-providers.aws.secrets-manager.name"과 같은 값을 기반으로 외부 설정 저장소와 통신하는 "EnvironmentPostProcessor"를 구현하고, "{조직명}-spring-boot-starter-environment-csv", "{조직명}-spring-boot-starter-environment-http", "{조직명}-spring-boot-starter-environment-aws-secrets-manager"와 같은 스프링 부트 스타터를 작성하면 된다.

클라우드 환경에서 외부 설정을 주입받기(인스턴스 혹은 컨테이너)

외부 저장소에서 설정을 받기 위해 스프링의 공식 문서를 읽어본 사람들은 알겠지만, 스프링에서는 OS의 환경 변수를 참조해서 설정값으로 사용할 수 있으며, 대부분의 클라우드 서비스나 쿠버네티스의 경우에도 민감정보나 설정을 분리 보관하며, 필요시를 이를 환경 변수로 참조할 수 있도록 할 수 있다.

OS 환경 변수에 대한 스프링의 설정 값 처리 방식

스프링에서는 OS 환경 변수의 키 대해 다음과 같이 적용한다.
1.
언더스코어(_)를 .으로 변환.
2.
대시(-)를 제거.
3.
대문자로 변환.
위 동작에 대해 아래 예제와 같이 수행한다.
SPRING_DATASOURCE_URL → spring.datasource.url
CUSTOM_NAMES_0_NAME → custom.names[0].name

예제로 확인

앞에서의 예제에서 테스트를 해보자
1.
spring.factories 부분을 주석 처리한다.
2.
.bash_profile 등에서 아래와 같이 환경 변수를 입력한다.
export CUSTOM_GREETING="Hello world!(from os env)"

AWS EC2에서 Secrets Manager 참조하기

EC2 인스턴스에 권한을 부여하여 Secrets Manager에 저장된 값을 json으로 받을 수 있다.

1. EC2에 권한 부여하기

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue" ], "Resource": "arn:aws:secretsmanager:${region}:${account-id}:secret:${secret-name}" } ] }

2. EC2에서 환경변수로 입력하기

리눅스 환경이라면 AWS CLI를 통해 .bash_profile과 같이 환경 변수로 입력받을 수 있다.
CUSTOM_GREETING=$(aws secretsmanager get-secret-value --secret-id ${secret-name} --query SecretString --output text | jq -r ".${field name}") export CUSTOM_GREETING
💬
위 예제에서는 jq를 사용하여 단 건에 대해 환경 변수를 입력하도록 했는데, 이런 경우 외부 설정이 많아질수록 스크립트가 길어질 수 있으니 귀찮아질 수 있다. 때문에 오히려 EnvironmentPostProcessor를 정의해서 JSON의 키/값을 그대로 사용하는 것이 나을 수도 있다.

쿠버네티스 환경에서 secret 이용하기

k8s에서는 secret을 바로 컨테이너의 환경 변수로 설정할 수 있기에 별다른 설정 없이 바로 사용할 수 있다.
apiVersion: v1 kind: Pod metadata: name: secret-test-pod spec: containers: - name: test-container image: registry.k8s.io/busybox command: [ "/bin/sh", "-c", "env" ] envFrom: - secretRef: name: mysecret restartPolicy: Never

마무리

스프링을 통해 설정을 외부화하여 직접 참조하는 방법과 클라우드 혹은 컨테이너 지원을 받아 환경 변수로 참조하는 방법에 대해 다루었다. 개발은 진행하는 조직의 수만큼이나 설정 저장소, 설정을 조회하는 방법이 다를 것으로 생각된다. 각 조직별로 위에서 이야기한 방법이 도움이 되었으면 한다.
Subscribe to '이그나이트'
Subscribe to my site to be the first to receive notifications and emails about the latest updates, including new posts.
Join Slashpage and subscribe to '이그나이트'!
Subscribe
👍
3