Spring Cloud Config Server와 Vault 통합을 이용한 분산 설정 관리

업데이트:

개념 (Concept)

Spring Cloud Config Server란?

Spring Cloud Config Server는 분산 시스템에서 설정 정보를 중앙에서 관리하고 제공하는 서버입니다. Git, SVN, 로컬 파일 시스템 등을 백엔드 저장소로 사용하여 여러 마이크로서비스의 설정을 버전 관리하고 동적으로 업데이트할 수 있습니다.

주요 특징:

  • 중앙 집중식 설정 관리
  • Git 기반 버전 관리
  • 환경별(dev, prod) 설정 분리
  • 실시간 설정 업데이트
  • RESTful API를 통한 설정 조회

HashiCorp Vault란?

HashiCorp Vault는 민감한 정보를 안전하게 저장, 관리, 접근하기 위한 도구입니다. 데이터베이스 패스워드, API 키, 토큰, 인증서 등을 암호화하여 저장하고 감사 로그를 기록합니다.

주요 특징:

  • 암호화된 저장소
  • 다양한 인증 방식 지원
  • 자동 인증서 관리
  • 비밀 로테이션
  • 감사 로그 기록

메커니즘 (Mechanism)

Spring Cloud Config Server 동작 원리

┌─────────────────────────────────────────────────────────┐
│                    Client Application                   │
│  ┌─────────────────────────────────────────────────┐    │
│  │   bootstrap.yml                                 │    │
│  │   - Config Server URI 설정                       │    │
│  │   - Vault 연결 정보                              │    │
│  └─────────────────────────────────────────────────┘    │
└──────────────────────┬──────────────────────────────────┘
                       │ (1) 시작 시 Config Server 연결
                       ▼
        ┌──────────────────────────────┐
        │   Config Server              │
        │  (Port: 8888)                │
        │  ┌────────────────────────┐  │
        │  │ Git Repository         │  │
        │  │ ├─ client-app.yml      │  │
        │  │ ├─ client-app-dev.yml  │  │
        │  │ └─ client-app-prod.yml │  │
        │  └────────────────────────┘  │
        └──────────────────────────────┘
                       │ (2) 설정 파일 제공
                       ▼
        ┌──────────────────────────────┐
        │      Vault Server            │
        │     (Port: 8200)             │
        │  ┌────────────────────────┐  │
        │  │ Secret Storage         │  │
        │  │ ├─ database/username   │  │
        │  │ ├─ database/password   │  │
        │  │ ├─ jwt-secret          │  │
        │  │ └─ api-key             │  │
        │  └────────────────────────┘  │
        └──────────────────────────────┘
                       │ (3) 민감한 정보 제공
                       ▼
        Client Application (실행 시작)

설정 로드 순서

  1. Bootstrap Phase (부트스트랩 단계)
    • bootstrap.yml 로드 (Config Server/Vault 연결 정보)
    • Config Server에서 설정 파일 다운로드
    • Vault에서 민감한 정보 조회
  2. Application Phase (애플리케이션 단계)
    • application.yml 로드 (기본 설정)
    • 환경 변수 적용
    • 스프링 부트 초기화

내부 구조 (Architecture)

Config Server 내부 동작

// 1. Config Server 시작
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

// 2. Config Server 설정
// application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: file:${java.io.tmpdir}/spring-cloud-config-repo
          clone-on-start: true
          search-paths: '{application}'

// 3. 클라이언트의 설정 요청
// GET /client-app/prod
// 응답:
// {
//   "name": "client-app",
//   "profiles": ["prod"],
//   "label": "master",
//   "version": "abc1234",
//   "state": null,
//   "propertySources": [...]
// }

Vault 통합 아키텍처

┌────────────────────────────────────────────────┐
│         Spring Cloud Vault                     │
│  ┌──────────────────────────────────────────┐  │
│  │  VaultTemplate                           │  │
│  │  ├─ readSecrets()                        │  │
│  │  ├─ write()                              │  │
│  │  └─ opsForKeyValue()                     │  │
│  └──────────────────────────────────────────┘  │
└────────────────────┬───────────────────────────┘
                     │
        ┌────────────▼─────────────┐
        │   Authentication         │
        │  ┌────────────────────┐  │
        │  │ Token              │  │
        │  │ AppRole            │  │
        │  │ Kubernetes         │  │
        │  │ LDAP/Okta          │  │
        │  └────────────────────┘  │
        └──────────────────────────┘
                     │
        ┌────────────▼─────────────┐
        │   Vault Server           │
        │  ┌────────────────────┐  │
        │  │ Secret Engines     │  │
        │  │ ├─ KV v2           │  │
        │  │ ├─ Database        │  │
        │  │ ├─ PKI             │  │
        │  │ └─ Transit         │  │
        │  └────────────────────┘  │
        └──────────────────────────┘

속성 주입 메커니즘

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    // Config Server + Vault → Spring Environment → AppProperties
    private String name;           // application.yml에서
    private Database database;     // Config Server에서
    private Security security;     // Vault에서
}

@RestController
public class ConfigController {
    // 방법 1: @Value 애노테이션
    @Value("${app.name}")
    private String appName;
    
    // 방법 2: @ConfigurationProperties 클래스 주입
    @Autowired
    private AppProperties appProperties;
    
    // 방법 3: Environment 객체 사용
    @Autowired
    private Environment environment;
}

Spring Cloud Config Server vs Vault 차이점

항목 Config Server Vault
목적 애플리케이션 설정 관리 민감한 정보 관리
저장소 Git, SVN, 파일 시스템 암호화된 메모리 저장소
데이터 타입 설정값 전체 민감한 정보 (암호, 토큰)
버전 관리 Git 기반 버전 관리 버전 지원하지 않음
감시 변경 이력 조회 가능 감사 로그 기록
보안 저장소 수준의 보안 암호화 + 접근 제어
동적 업데이트 지원 (Config Client) 제한적
확장성 좋음 높음
사용 사례 애플리케이션 설정 API 키, DB 패스, 인증서

장점과 단점

Spring Cloud Config Server

장점:

  • 중앙 집중식 설정 관리로 일관성 유지
  • Git 기반 버전 관리 및 감시
  • 환경별 설정 분리 용이
  • 변경 이력 추적 가능
  • RESTful API 제공
  • 동적 설정 업데이트 지원

단점:

  • 민감한 정보는 평문으로 저장되므로 추가 보안 필요
  • Config Server 가용성 영향도가 큼
  • 네트워크 지연 가능성
  • Git 저장소 관리 필요

HashiCorp Vault

장점:

  • 암호화된 저장소로 높은 보안
  • 다양한 인증 방식 지원
  • 자동 인증서 관리
  • 감사 로그 기록
  • 비밀 로테이션 지원
  • 고가용성 클러스터 지원

단점:

  • 추가 서비스 운영 필요
  • 러닝 커브가 높음
  • Config Server와 별도로 구성 필요
  • 성능 오버헤드 가능

활용 샘플

1. 프로젝트 구조

spring-cloud-config-vault/                    # 부모 프로젝트 (Multi-Module)
├── pom.xml                                   # 부모 POM (의존성 관리)
├── config-server/                            # Config Server 모듈
│   ├── pom.xml
│   └── src/main/java
│       └── com/example/configserver/
│           └── ConfigServerApplication.java
├── client-app/                               # Client Application 모듈
│   ├── pom.xml
│   └── src/main/java
│       └── com/example/clientapp/
│           ├── ClientAppApplication.java
│           ├── config/AppProperties.java
│           └── controller/ConfigController.java
└── vault-config/                             # Vault 설정 및 스크립트
    └── init-vault.ps1

2. Vault 초기화 (PowerShell)

# Vault 서버 시작 (개발 모드)
vault server -dev

# 데이터베이스 자격증명 저장
vault kv put secret/data/client-app/database `
    username="dbuser" `
    password="db-secure-password-2024" `
    url="jdbc:postgresql://localhost:5432/client_db" `
    maxPoolSize=20

# 보안 설정 저장
vault kv put secret/data/client-app/security `
    jwtSecret="vault-managed-jwt-secret-key" `
    jwtExpiration=86400000 `
    apiKey="vault-managed-api-key"

3. Config Server 설정

# config-server/src/main/resources/application.yml
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: file:${java.io.tmpdir}/spring-cloud-config-repo
          clone-on-start: true
          search-paths: '{application}'

server:
  port: 8888

4. Client Application 설정

# client-app/src/main/resources/bootstrap.yml
spring:
  application:
    name: client-app
  cloud:
    config:
      uri: http://localhost:8888
      fail-fast: true
    vault:
      enabled: true
      host: localhost
      port: 8200
      scheme: http
      authentication: TOKEN
      token: s.xxxxxxxxxxxxxxxx
      kv:
        enabled: true
        backend: secret
        version: 2
# client-app/src/main/resources/application.yml
spring:
  application:
    name: client-app

server:
  port: 8080

app:
  name: Client Application
  version: 1.0.0
  environment: development
  database:
    url: jdbc:postgresql://localhost:5432/myapp_db
    username: appuser
    password: password123
    max-pool-size: 10
  security:
    jwt-secret: ${vault:secret/data/client-app/security/jwtSecret}
    jwt-expiration: 86400000
    api-key: ${vault:secret/data/client-app/security/apiKey}

5. 환경별 설정 파일

# vault-config/client-app.yml (공통 설정)
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

app:
  name: Client Application
  version: 1.0.0
  environment: ${ENVIRONMENT:development}
# vault-config/client-app-dev.yml (개발 환경)
spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

app:
  environment: development
  database:
    url: jdbc:postgresql://localhost:5432/myapp_dev
    max-pool-size: 5

logging:
  level:
    root: DEBUG
    com.example: DEBUG
# vault-config/client-app-prod.yml (프로덕션 환경)
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

app:
  environment: production
  database:
    url: jdbc:postgresql://prod-db.example.com:5432/myapp_prod
    max-pool-size: 20

logging:
  level:
    root: WARN

주요 코드 설명

1. Config Server 활성화

// config-server/src/main/java/com/example/configserver/ConfigServerApplication.java
@SpringBootApplication
@EnableConfigServer      // Config Server 기능 활성화
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

@EnableConfigServer 애노테이션은 다음을 자동 설정합니다:

  • EnvironmentController: 설정 조회 엔드포인트
  • ResourceRepository: Git/파일 시스템 접근
  • EnvironmentRepository: 설정 데이터 관리

2. @ConfigurationProperties 클래스

// client-app/src/main/java/com/example/clientapp/config/AppProperties.java
@Component
@ConfigurationProperties(prefix = "app")
@Data  // Lombok: getter, setter, toString, equals, hashCode 자동 생성
public class AppProperties {
    
    private String name;
    private String version;
    private String environment;
    
    private Database database = new Database();
    private Security security = new Security();

    @Data
    public static class Database {
        private String url;
        private String username;
        private String password;
        private int maxPoolSize;
    }

    @Data
    public static class Security {
        private String jwtSecret;
        private long jwtExpiration;
        private String apiKey;
    }
}

로드 흐름:

Vault: secret/data/client-app/security → Spring Environment
                                              ↓
                            @ConfigurationProperties 처리
                                              ↓
                            AppProperties.security 필드 값 설정

3. Controller에서 설정 사용

// client-app/src/main/java/com/example/clientapp/controller/ConfigController.java
@RestController
@RequestMapping("/api/config")
@RequiredArgsConstructor  // Lombok: final 필드 생성자 자동 생성
public class ConfigController {

    private final AppProperties appProperties;

    // 방법 1: @Value 애노테이션
    @Value("${app.name:Not Set}")
    private String appName;

    @Value("${app.version:Not Set}")
    private String appVersion;

    // 방법 2: @ConfigurationProperties 주입
    @GetMapping("/properties")
    public ResponseEntity<Map<String, Object>> getProperties() {
        Map<String, Object> response = new HashMap<>();
        
        Map<String, Object> app = new HashMap<>();
        app.put("name", appProperties.getName());
        app.put("version", appProperties.getVersion());
        app.put("environment", appProperties.getEnvironment());
        
        Map<String, Object> database = new HashMap<>();
        database.put("url", appProperties.getDatabase().getUrl());
        database.put("username", appProperties.getDatabase().getUsername());
        database.put("maxPoolSize", appProperties.getDatabase().getMaxPoolSize());
        
        Map<String, Object> security = new HashMap<>();
        security.put("jwtExpiration", appProperties.getSecurity().getJwtExpiration());
        // 민감한 정보는 존재 여부만 확인
        security.put("apiKeyExists", 
            appProperties.getSecurity().getApiKey() != null && 
            !appProperties.getSecurity().getApiKey().isEmpty());
        
        response.put("app", app);
        response.put("database", database);
        response.put("security", security);
        
        return ResponseEntity.ok(response);
    }

    @GetMapping("/status")
    public ResponseEntity<Map<String, String>> getStatus() {
        Map<String, String> response = new HashMap<>();
        response.put("status", "UP");
        response.put("appName", appName);
        response.put("appVersion", appVersion);
        
        return ResponseEntity.ok(response);
    }
}

4. Vault에서 설정 로드 (bootstrap.yml)

spring:
  application:
    name: client-app
  
  cloud:
    # Config Server 설정
    config:
      uri: http://localhost:8888
      fail-fast: true
      retry:
        initial-interval: 1000
        max-interval: 2000
        max-attempts: 6
    
    # Vault 설정
    vault:
      enabled: true
      host: localhost
      port: 8200
      scheme: http
      
      # 인증 방식
      authentication: TOKEN
      token: s.xxxxxxxxxxxxxxxx
      
      # KV v2 Secret Engine
      kv:
        enabled: true
        backend: secret
        version: 2
        profile-separator: '/'

경로 매핑:

Vault 경로: secret/data/client-app/security/jwtSecret
매핑된 속성: app.security.jwt-secret (kebab-case로 자동 변환)

5. 환경 변수 사용

// 환경 변수 설정
System.setProperty("ENVIRONMENT", "production");
System.setProperty("DATABASE_URL", "jdbc:postgresql://prod-db:5432/db");

// application.yml에서 사용
app:
  environment: ${ENVIRONMENT:development}
  database:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/db}

실행 방법

1. Vault 시작 (개발 모드)

vault server -dev
# 출력: Unseal Key: ...
#       Root Token: s.xxxxxxxxxxxxx

2. Vault 초기화

PowerShell 스크립트 실행:

cd vault-config
.\init-vault.ps1

3. Config Server 실행

cd config-server
mvn spring-boot:run
# 또는
mvn clean install
java -jar target/spring-cloud-config-server-1.0.0.jar

4. Client Application 실행

cd client-app
mvn spring-boot:run
# 또는
java -jar target/spring-cloud-config-client-app-1.0.0.jar

5. API 호출

# 설정 확인
curl http://localhost:8080/api/config/properties

# 상태 확인
curl http://localhost:8080/api/config/status

# Actuator Endpoint
curl http://localhost:8080/actuator/configprops
curl http://localhost:8080/actuator/env

Best Practices (모범 사례)

1. Config Server 구성

# 개발 환경에서는 로컬 파일 시스템 사용
spring:
  cloud:
    config:
      server:
        git:
          uri: file:/tmp/config-repo

# 프로덕션 환경에서는 원격 Git 저장소 사용
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/org/config-repo
          username: ${GIT_USERNAME}
          password: ${GIT_PASSWORD}

2. Vault 인증 방식 선택

# 개발 환경: Token 기반
vault:
  authentication: TOKEN
  token: s.xxxxxxxxxxxxx

# 프로덕션 환경: AppRole 기반
vault:
  authentication: APPROLE
  app-role:
    role-id: ${VAULT_ROLE_ID}
    secret-id: ${VAULT_SECRET_ID}

# Kubernetes 환경: Kubernetes 인증
vault:
  authentication: KUBERNETES
  kubernetes:
    role: client-app

3. 민감한 정보 관리

# Vault에만 저장할 정보
app:
  security:
    jwt-secret: ${vault:secret/data/client-app/security/jwtSecret}
    api-key: ${vault:secret/data/client-app/security/apiKey}
    db-password: ${vault:secret/data/client-app/database/password}

# Config Server에 저장 가능한 정보
app:
  name: My Application
  version: 1.0.0
  logging:
    level: INFO

4. 설정 검증

@Component
@ConfigurationProperties(prefix = "app")
@Validated  // 유효성 검증 활성화
public class AppProperties {
    
    @NotNull(message = "Application name must not be null")
    @NotBlank(message = "Application name must not be blank")
    private String name;
    
    @NotNull
    private String version;
    
    @Valid  // 중첩된 객체 검증
    private Database database = new Database();
    
    @Data
    public static class Database {
        @NotNull
        @Pattern(regexp = "jdbc:.*", message = "Invalid JDBC URL")
        private String url;
        
        @NotNull
        @Min(1)
        @Max(100)
        private int maxPoolSize;
    }
}

문제 해결

Config Server에 연결할 수 없음

# 문제: Connection refused
# 해결:
1. Config Server 실행 확인: curl http://localhost:8888/actuator/health
2. bootstrap.yml의 spring.cloud.config.uri 확인
3. fail-fast 설정 확인: false로 변경하면 서버 시작 안 함

Vault에서 비밀을 로드할 수 없음

# 문제: 403 Forbidden
# 해결:
1. Token 유효성 확인: vault token lookup
2. Token 권한 확인: vault token lookup -self
3. 경로 확인: vault kv list secret/data/client-app

# 권한 설정 예
vault policy write client-app - <<EOF
path "secret/data/client-app/*" {
  capabilities = ["read", "list"]
}
EOF

vault token create -policy=client-app

설정이 업데이트되지 않음

# 문제: 설정 변경 후 적용 안 됨
# 해결:

# 1. Actuator refresh 엔드포인트 활용
curl -X POST http://localhost:8080/actuator/refresh

# 2. @RefreshScope 애노테이션 사용
@Component
@RefreshScope  // 설정 변경 시 자동 갱신
public class DynamicConfig {
    @Value("${app.name}")
    private String appName;
}

# 3. 애플리케이션 재시작

참고자료


소스코드

샘플 코드는 여기에서 확인 가능합니다.

댓글남기기