개요

Spring Cloud Config Server와 AWS Secrets Manager를 통합한 중앙 집중식 설정 관리 구조를 정리한다. 일반 설정은 Git 저장소에서 관리하고, 민감한 정보(비밀번호, 인증키 등)는 AWS Secrets Manager에서 관리하는 방식이다.

전체 아키텍처

┌─────────────────┐     GET /{app}/{profile}     ┌──────────────────┐
│ 클라이언트       │ ──────────────────────────▶ │ Config Server    │
│ (각 서비스)      │                              │ (8888)           │
└─────────────────┘                              └────────┬─────────┘
       ▲                                                  │
       │                                                  │ 1. Git에서 설정 로드
       │                                                  ▼
       │                                         ┌──────────────────┐
       │                                         │ cloud-config-repo │
       │                                         │ (Git 저장소)       │
       │                                         └──────────────────┘
       │                                                  │
       │                                                  │ 2. ${secrets@...} 치환
       │                                                  ▼
       │                                         ┌──────────────────┐
       │                                         │ AWS Secrets      │
       │                                         │ Manager          │
       │                                         └──────────────────┘
       │                                                  │
       └────────────────── 치환된 설정 응답 ──────────────┘

bootstrap.yml vs application.yml

실행 순서

bootstrap.yml → application.yml

bootstrap.yml이 먼저 로드되고, 그 다음 application.yml이 로드된다.

bootstrap.yml

역할: 애플리케이션 컨텍스트가 시작되기 전에 필요한 설정

주요 용도:

  • Spring Cloud Config Server 연결 정보
  • 암호화/복호화 키 설정
  • 애플리케이션 이름 (spring.application.name)
  • Config Server에서 설정을 가져오기 위한 최소 정보
spring:
  application:
    name: order-api
  cloud:
    config:
      uri: http://config-server:8888
      fail-fast: true

application.yml

역할: 애플리케이션의 일반적인 설정

주요 용도:

  • 데이터베이스 연결 정보
  • 서버 포트
  • 로깅 설정
  • 비즈니스 로직 관련 설정
server:
  port: 8081
logging:
  level:
    root: info
    com.example: debug

왜 분리되어 있는가?

Spring Cloud Config를 사용할 때:

  1. bootstrap.yml: “Config Server 어디 있어?”를 알려줌
  2. Config Server에서 설정을 가져옴
  3. application.yml: 로컬 설정과 병합됨

Config Server를 사용하지 않으면 bootstrap.yml은 필요 없다.

bootstrap.yml이 먼저 실행되는 원리

Spring Boot 자체의 기본 설정이 아니다. spring-cloud-context 라이브러리가 이 동작을 제공한다.

spring-cloud-starter-config
    └── spring-cloud-config-client
            └── spring-cloud-context  ← 여기서 bootstrap 로딩 처리

BootstrapApplicationListener 클래스가 bootstrap.yml을 먼저 로드하는 역할을 한다.

Spring Boot 2.4 이후 변경사항

Spring Boot 2.4부터는 bootstrap.yml 지원이 기본 비활성화되었다.

사용하려면 별도 의존성 추가 필요:

implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'

또는 spring.cloud.bootstrap.enabled=true 설정 필요.

설정 파일 비교

구분 bootstrap.yml application.yml
실행 시점 애플리케이션 컨텍스트 시작 전 애플리케이션 컨텍스트 시작 후
주요 용도 Config Server 연결 정보 일반 설정
제공 주체 Spring Cloud Context Spring Boot
필수 여부 Config Server 사용 시에만 필요 항상 필요
Boot 2.4+ 별도 의존성 필요 기본 지원

프로젝트 구조

두 개의 프로젝트로 구성된다.

1. cloud-config (Config Server)

설정을 배포하는 서버 애플리케이션이다.

cloud-config/
├── config-server/
│   ├── src/main/java/.../
│   │   ├── CloudConfigServerApplication.java
│   │   ├── advice/
│   │   │   └── AWSSecretsManagerAdvice.java    # 설정값 치환 로직
│   │   ├── config/
│   │   │   └── AWSSecretsManagerConfig.java    # AWS SDK 설정
│   │   └── service/
│   │       └── AWSSecretsManagerServiceImpl.java
│   └── src/main/resources/
│       ├── application.yml     # 서버 설정 (포트: 8888)
│       └── bootstrap.yml
└── test-client/                # 테스트용 클라이언트

2. cloud-config-repo (설정 저장소)

실제 설정 파일들이 저장된 Git 저장소다.

cloud-config-repo/
├── config/                     # 운영 환경 설정
│   ├── order/
│   │   ├── order-api.yml
│   │   └── application-order.yml
│   ├── payment/
│   ├── notification/
│   └── ...
├── config-staging/             # 스테이징 환경 설정
└── config-test/                # 테스트 환경 설정

핵심 연결 설정

Config Server → Git 저장소

config-server/src/main/resources/application.yml:

server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          clone-on-start: true
          timeout: 60000
          uri: https://github.com/my-org/cloud-config-repo.git
          username: ${SPRING_CLOUD_CONFIG_SERVER_REPO_USERNAME}
          password: ${SPRING_CLOUD_CONFIG_SERVER_REPO_PASSWORD}
          default-label: main
          search-paths: config/**
aws:
  secret-manager:
    secret-id: config-prod
  • uri: Git 저장소 주소
  • search-paths: 설정 파일을 검색할 경로 패턴
  • default-label: 기본 브랜치

설정 흐름 다이어그램

┌─────────────────────────────────────────────────────────────┐
│ 1. Bootstrap Phase (애플리케이션 컨텍스트 시작 전)            │
│    └── bootstrap.yml 로드                                   │
│        └── spring.profiles=cloudconfig 체크                 │
└──────────────────────────┬──────────────────────────────────┘
                           │
              ┌────────────┴────────────┐
              │                         │
        cloudconfig              기본 프로파일
        프로파일 활성화                  │
              │                         │
              ▼                         ▼
┌─────────────────────────┐   ┌─────────────────────────┐
│ 2. Config Server 연결    │   │ 2. 로컬 설정만 사용      │
│    - 외부 설정 가져오기   │   │    - application.yml    │
│    - 비밀값 주입         │   │    - application-{p}.yml│
└───────────┬─────────────┘   └───────────┬─────────────┘
            │                             │
            └──────────────┬──────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Application Phase                                        │
│    └── application.yml 로드                                 │
│    └── application-{profile}.yml 로드                       │
│    └── Config Server 설정과 병합 (우선순위: Config Server)   │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ 4. @ConfigurationProperties 바인딩                          │
│    └── @RefreshScope: 설정 변경 시 동적 리로드               │
└─────────────────────────────────────────────────────────────┘

실제 클라이언트 적용 예시 (order-api)

실제 MSA 서비스에서 Config Client를 어떻게 사용하는지 살펴본다.

클라이언트 bootstrap.yml

order-api/src/main/resources/bootstrap.yml:

spring:
  application:
    name: order-api
  profiles:
    include:
    - order
  cloud:
    config:
      enabled: false    # 기본값: 비활성화
---
# cloudconfig 프로필 활성화 시
spring.profiles: cloudconfig
spring:
  cloud:
    config:
      enabled: true
      fail-fast: true
      uri: http://${CONFIG_SERVER_HOST}:${CONFIG_SERVER_PORT}
      request-connect-timeout: ${CONFIG_SERVER_CONNECT_TIMEOUT:60000}
      request-read-timeout: ${CONFIG_SERVER_READ_TIMEOUT:60000}
  • spring.application.name: order-api: 이 이름으로 설정 파일 검색
  • spring.profiles.include: order: application-order.yml 설정도 함께 로드
  • cloudconfig 프로필: 운영 환경에서만 Config Server 연결 활성화
  • 환경변수로 Config Server 주소 주입 (K8s, Docker 등)

설정 저장소의 파일 구조

cloud-config-repo/config/order/:

config/order/
├── order-api.yml           # order-api 전용 설정
└── application-order.yml   # order 프로필 공통 설정 (모든 order-* 서비스 공유)

설정 파일 예시

cloud-config-repo/config/order/application-order.yml:

spring.profiles: dev,prod
order:
  datasource:
    primary:
      driver-class-name: org.postgresql.Driver
      jdbc-url: jdbc:postgresql://${secrets@order.datasource.primary.host}:${secrets@order.datasource.primary.port:5432}/${secrets@order.datasource.primary.database}
      username: ${secrets@order.datasource.primary.username}
      password: ${secrets@order.datasource.primary.password}
    replica:
      driver-class-name: org.postgresql.Driver
      jdbc-url: jdbc:postgresql://${secrets@order.datasource.replica.host}:${secrets@order.datasource.replica.port:5432}/${secrets@order.datasource.replica.database}
      username: ${secrets@order.datasource.replica.username}
      password: ${secrets@order.datasource.replica.password}
  storage:
    base-path: /app/files
    s3-bucket: my-service-dev
---
spring.profiles: prod
order:
  storage:
    s3-bucket: my-service-prod
  • DB 접속 정보(host, username, password)는 ${secrets@...}로 AWS Secrets Manager에서 조회
  • 포트는 기본값 지정: ${secrets@...port:5432}
  • 프로필별 설정 분리: dev,prod 공통 설정 + prod 전용 오버라이드

설정 로드 순서

1. order-api 로컬 application.yml (포트, 로깅 등 기본값)
     ↓
2. Config Server 요청: GET /order-api/dev,order
     ↓
3. 로드되는 파일들:
   - config/order/order-api.yml (application-name 매칭)
   - config/order/application-order.yml (profiles.include: order 매칭)
     ↓
4. AWS Secrets Manager 값 치환
     ↓
5. 최종 설정 주입

실행 예시

# 로컬 개발 (Config Server 미사용)
java -jar order-api.jar

# 운영 환경 (Config Server 사용)
java -jar order-api.jar \
  --spring.profiles.active=cloudconfig,dev \
  -DCONFIG_SERVER_HOST=config-server \
  -DCONFIG_SERVER_PORT=8888

파일 매핑 규칙

클라이언트 요청 매핑되는 파일
order-api + dev,order 프로필 config/order/order-api.yml, config/order/application-order.yml
payment-api + prod config/payment/payment-api.yml

설정 우선순위

여러 소스에서 같은 키가 정의된 경우 우선순위:

1. Config Server (가장 높음)
2. application-{profile}.yml
3. application.yml
4. bootstrap.yml (가장 낮음)

AWS Secrets Manager 통합

아키텍처

┌─────────────────────────────────────────────────────────────┐
│ Git Repository (일반 설정)                                   │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ order:                                                  │ │
│ │   datasource:                                           │ │
│ │     username: ${secrets@order.datasource.username}      │ │
│ │     password: ${secrets@order.datasource.password}      │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ Config Server                                               │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ AWSSecretsManagerAdvice (ResponseBodyAdvice)            │ │
│ │ - ${secrets@key} 패턴 탐지                               │ │
│ │ - AWS Secrets Manager에서 값 조회                        │ │
│ │ - 설정값 치환                                            │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ AWS Secrets Manager                                         │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Secret ID: config-prod                                  │ │
│ │ {                                                       │ │
│ │   "order.datasource.username": "prod_user",             │ │
│ │   "order.datasource.password": "super_secret"           │ │
│ │ }                                                       │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ Client Application (최종 설정)                               │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ order:                                                  │ │
│ │   datasource:                                           │ │
│ │     username: prod_user           ← 치환됨              │ │
│ │     password: super_secret        ← 치환됨              │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

패턴 문법

패턴 설명
${secrets@key} AWS Secrets Manager에서 key 조회 (없으면 원본 유지)
${secrets@key:default} key가 없으면 default 값 사용
${secrets@nested.key} 중첩 키 지원 (JSON의 nested.key)

치환 로직 (AWSSecretsManagerAdvice)

@ControllerAdvice를 활용하여 Config Server 응답 전에 값을 치환한다.

@ControllerAdvice
@RequiredArgsConstructor
public class AWSSecretsManagerAdvice implements ResponseBodyAdvice<Object> {

    private final AWSSecretsManagerService awsSecretsManagerService;
    private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\{([^}]+)}");
    private static final String SECRET_PREFIX = "secrets@";

    @Override
    public Object beforeBodyWrite(Object body, ...) {
        if (!(body instanceof Environment)) {
            return body;
        }

        JsonNode rootNode = this.awsSecretsManagerService.getSecret();

        for (PropertySource propertySource : ((Environment) body).getPropertySources()) {
            Map<String, Object> source = (Map<String, Object>) propertySource.getSource();
            for (Map.Entry<String, Object> entry : source.entrySet()) {
                entry.setValue(this.replaceSecret(rootNode, entry.getValue()));
            }
        }

        return body;
    }
}

타입 보존

치환 시 원래 타입을 유지한다.

public Object getSecretValueAsType() {
    if (this.secretNode.isBoolean()) {
        return this.secretNode.booleanValue();
    } else if (this.secretNode.isNumber()) {
        if (this.secretNode.isFloat() || this.secretNode.isDouble()) {
            return this.secretNode.doubleValue();
        } else {
            return this.secretNode.longValue();
        }
    } else {
        return this.secretNode.textValue();
    }
}

예시:

# Git 설정
app:
  enabled: ${secrets@app.enabled}        # Boolean
  max-retry: ${secrets@app.maxRetry}     # Integer/Long
  timeout: ${secrets@app.timeout}        # Double
// AWS Secrets Manager
{
  "app.enabled": true,
  "app.maxRetry": 3,
  "app.timeout": 30.5
}

보안 장점

  1. 민감 정보 분리: Git에는 플레이스홀더만 저장, 실제 값은 AWS에서 관리
  2. 접근 제어: IAM 정책으로 Secret 접근 권한 관리
  3. 감사 로그: AWS CloudTrail로 Secret 접근 기록 추적
  4. 자동 로테이션: Secrets Manager의 자동 비밀번호 로테이션 기능 활용 가능
  5. 암호화: AWS KMS로 저장 시 암호화

실제 동작 흐름

# 1. 클라이언트(order-api)가 Config Server에 요청
GET http://config-server:8888/order-api/dev,order

# 2. Config Server가 Git에서 설정 로드
#    search-paths: config/** 에서 order-api.yml, application-order.yml 검색

# 3. AWSSecretsManagerAdvice가 응답 전 치환
#    ${secrets@order.datasource.password} → AWS 비밀값으로 대체

# 4. 최종 응답
{
  "name": "order-api",
  "profiles": ["dev", "order"],
  "propertySources": [{
    "source": {
      "order.datasource.jdbc-url": "jdbc:postgresql://10.0.0.1:5432/orderdb",
      "order.datasource.username": "order_user",
      "order.datasource.password": "실제AWS비밀값"
    }
  }]
}

@ConfigurationProperties + @RefreshScope 활용

Config Server에서 가져온 설정을 구조화된 객체로 바인딩하고, 동적 리로드를 지원한다.

@Component
@ConfigurationProperties(prefix = "order")
@RefreshScope  // Config Server 설정 변경 시 동적 리로드
@Getter
@Setter
public class OrderProperties {

    private Map<String, DataSourceProperties> datasource;
    private StorageProperties storage;

    @Getter
    @Setter
    public static class DataSourceProperties {
        private String driverClassName;
        private String jdbcUrl;
        private String username;
        private String password;
    }

    @Getter
    @Setter
    public static class StorageProperties {
        private String basePath;
        private String s3Bucket;
    }
}

사용:

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderProperties orderProperties;

    public void process() {
        String s3Bucket = orderProperties.getStorage().getS3Bucket();
        // ...
    }
}

fail-fast와 타임아웃 설정

운영 환경에서는 Config Server 연결 실패 시 애플리케이션 시작을 중단하는 것이 안전하다.

spring:
  cloud:
    config:
      fail-fast: true  # 연결 실패 시 시작 중단
      request-connect-timeout: 60000  # 연결 타임아웃 (ms)
      request-read-timeout: 60000     # 읽기 타임아웃 (ms)
      retry:
        enabled: true
        initial-interval: 1000
        max-attempts: 6
        max-interval: 2000
        multiplier: 1.1

retry 설정을 사용하려면 의존성 추가:

implementation 'org.springframework.retry:spring-retry'

기술 스택

카테고리 기술 버전
Language Java 1.8
Framework Spring Boot 2.3.0.RELEASE
Cloud Spring Cloud Config Hoxton.SR4
AWS AWS SDK (STS, Secrets Manager) 2.29.2

정리

  • Config Server: Git 저장소에서 설정을 읽어 HTTP로 제공
  • Config Client: bootstrap.yml로 Config Server 연결, 설정 주입
  • AWS Secrets Manager 연동: 표준 Spring Cloud Config에 @ControllerAdvice로 커스텀 확장
  • 설정 분리: 일반 설정은 Git, 민감 정보는 AWS Secrets Manager
  • 프로파일 기반 제어: cloudconfig 프로파일로 환경별 Config Server 연동 제어

이 구조를 통해 설정의 중앙 집중 관리와 보안을 동시에 달성할 수 있다.