Kubernetes가 이미 서비스 목록을 알고 있는데, 왜 Eureka 같은 서비스 레지스트리를 또 띄워야 할까요?

Kubernetes는 자체적으로 Service라는 리소스를 통해 서비스 디스커버리를 제공합니다. 그런데 Spring Cloud 애플리케이션을 K8s 위에서 실행하면서도 Eureka를 별도로 운영하는 경우가 많습니다. Spring Cloud Kubernetes는 이 중복을 제거하고, K8s의 네이티브 기능을 Spring Cloud의 추상화에 직접 연결해줍니다.

Spring Cloud Kubernetes란

Spring Cloud Kubernetes는 Kubernetes의 네이티브 기능(Service, ConfigMap, Secret)을 Spring Cloud의 표준 인터페이스(DiscoveryClient, PropertySource)로 매핑하는 프로젝트입니다.

핵심 기능:

  • **서비스 디스커버리 **: K8s Service → Spring DiscoveryClient
  • ** 설정 관리 **: K8s ConfigMap/Secret → Spring PropertySource
  • ** 리더 선출 **: K8s 네이티브 리더 선출 메커니즘 지원
  • ** 헬스 체크 **: K8s의 liveness/readiness 프로브와 Spring Actuator 연동

의존성

XML
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-all</artifactId>
</dependency>

kubernetes-client-all은 디스커버리 + 설정 + 리더 선출을 모두 포함합니다. 필요한 것만 쓰고 싶다면 개별 스타터를 선택할 수 있습니다:

  • spring-cloud-starter-kubernetes-client-config — ConfigMap/Secret 연동만
  • spring-cloud-starter-kubernetes-client — 디스커버리만

DiscoveryClient: K8s Service를 자동 매핑

Spring Cloud의 DiscoveryClient 인터페이스를 구현하여, K8s Service를 Spring의 ServiceInstance로 변환합니다. 기존에 Eureka용으로 작성된 코드가 수정 없이 동작합니다.

JAVA
@Service
@RequiredArgsConstructor
public class ServiceRegistry {

    private final DiscoveryClient discoveryClient;

    public List<String> getAllServices() {
        // K8s Service 목록을 그대로 반환
        return discoveryClient.getServices();
    }

    public List<ServiceInstance> getInstances(String serviceId) {
        // 특정 서비스의 Pod 인스턴스 목록
        return discoveryClient.getInstances(serviceId);
    }
}

이 코드는 Eureka를 쓸 때와 ** 완전히 동일 **합니다. 의존성만 바꾸면 구현 코드를 수정할 필요가 없다는 게 Spring Cloud 추상화의 장점입니다.

설정 옵션

YAML
spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        all-namespaces: false  # true면 모든 네임스페이스 검색
        namespaces:            # 특정 네임스페이스만 검색
          - production
          - staging
        service-labels:        # 라벨 기반 필터링
          environment: production
          team: backend

공부하면서 가장 실수하기 쉬운 부분이 RBAC 권한 설정이었습니다. Spring Cloud Kubernetes가 K8s API 서버에 접근하려면 적절한 ServiceAccountRole/ClusterRole이 필요합니다:

YAML
# K8s RBAC 설정
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: service-discovery-role
rules:
  - apiGroups: [""]
    resources: ["services", "endpoints", "pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: service-discovery-binding
subjects:
  - kind: ServiceAccount
    name: my-app-sa
    namespace: default
roleRef:
  kind: ClusterRole
  name: service-discovery-role
  apiGroup: rbac.authorization.k8s.io

이 권한이 없으면 애플리케이션 기동 시 403 에러가 나는데, 처음 세팅할 때 가장 많이 마주치는 문제입니다.

ConfigMap/Secret 연동

K8s ConfigMap과 Secret을 Spring의 PropertySource로 자동 매핑하여, application.yml과 동일하게 @Value@ConfigurationProperties로 접근할 수 있습니다.

ConfigMap 기반 설정

YAML
# K8s ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service  # 애플리케이션 이름과 일치시키기
  namespace: default
data:
  application.yml: |
    order:
      max-items: 50
      timeout-seconds: 30
    feature:
      new-payment: true
JAVA
@Component
@ConfigurationProperties(prefix = "order")
@Getter @Setter
public class OrderProperties {
    private int maxItems;
    private int timeoutSeconds;
}

Spring Cloud Kubernetes는 기본적으로 spring.application.name과 일치하는 ConfigMap을 자동으로 찾습니다. 다른 이름의 ConfigMap을 사용하려면 명시적으로 지정합니다:

YAML
spring:
  cloud:
    kubernetes:
      config:
        enabled: true
        sources:
          - name: order-service-config
            namespace: default
          - name: shared-config  # 공통 설정 ConfigMap
            namespace: common

Secret 연동

YAML
# K8s Secret
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secrets
type: Opaque
data:
  db-password: cGFzc3dvcmQxMjM=  # base64 인코딩
YAML
spring:
  cloud:
    kubernetes:
      secrets:
        enabled: true
        sources:
          - name: order-service-secrets
JAVA
@Value("${db-password}")
private String dbPassword;  // 자동으로 base64 디코딩

동적 설정 갱신

ConfigMap이 변경되면 애플리케이션을 재배포하지 않고도 설정을 갱신할 수 있습니다:

YAML
spring:
  cloud:
    kubernetes:
      config:
        enabled: true
      reload:
        enabled: true
        mode: polling       # polling 또는 event
        period: 15000       # polling 주기 (ms)
        strategy: refresh   # refresh 또는 restart_context

@RefreshScope를 적용한 Bean은 ConfigMap 변경 시 자동으로 갱신됩니다:

JAVA
@RefreshScope
@Component
@ConfigurationProperties(prefix = "feature")
@Getter @Setter
public class FeatureFlags {
    private boolean newPayment;
}

modeevent로 설정하면 K8s API의 Watch 기능을 사용하여 변경 즉시 감지합니다. 다만 이 방식은 K8s API 서버와의 연결을 계속 유지하므로, Pod 수가 많으면 API 서버에 부하를 줄 수 있습니다.

네임스페이스와 라벨 기반 필터링

실무에서는 하나의 K8s 클러스터에 여러 환경(dev, staging, production)이 공존하는 경우가 많습니다. 이때 네임스페이스와 라벨로 서비스 디스커버리 범위를 제어합니다.

YAML
spring:
  cloud:
    kubernetes:
      discovery:
        # 특정 네임스페이스만
        namespaces:
          - production
        # 라벨 조건 추가
        service-labels:
          app.kubernetes.io/part-of: order-system
        # 특정 서비스 포함/제외
        filter:
          includes:
            - payment-service
            - inventory-service

라벨 기반 필터링을 쓰면, 같은 네임스페이스에 있더라도 관련 없는 서비스는 디스커버리 대상에서 제외됩니다. 대규모 클러스터에서 불필요한 서비스 목록을 줄이는 데 유용합니다.

Discovery Server

모든 Pod에 K8s API 접근 권한을 주는 건 보안상 바람직하지 않을 수 있습니다. Discovery Server는 이 문제를 해결합니다.

PLAINTEXT
[Discovery Server]  ← K8s API 접근 권한 (유일)

   HTTP 엔드포인트

[Pod A] [Pod B] [Pod C]  ← K8s API 권한 불필요

Discovery Server 설정:

XML
<!-- Discovery Server 의존성 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-discoverserver</artifactId>
</dependency>

클라이언트 측 설정:

YAML
# 클라이언트 Pod에서 Discovery Server를 사용
spring:
  cloud:
    kubernetes:
      discovery:
        discovery-server-url: http://discovery-server.default.svc:8761

이렇게 하면 RBAC 설정을 Discovery Server 한 곳에만 집중할 수 있어 보안 관리가 훨씬 간단해집니다.

Spring Cloud 2025.0.0 (Northfields) 업데이트

최신 릴리스인 Spring Cloud 2025.0.0에서 달라진 점:

  • Fabric8 Kubernetes Client 7.3.1 로 업그레이드 — K8s 1.32+ API 호환
  • Spring Boot 3.5.0 호환
  • GraalVM Native Image 공식 지원 — 네이티브 컴파일로 기동 시간 단축
  • 개선된 ConfigMap/Secret 감시 메커니즘

GraalVM Native Image 빌드:

BASH
# 네이티브 이미지 빌드
mvn -Pnative spring-boot:build-image

네이티브 이미지로 빌드하면 기동 시간이 수 초에서 수십 밀리초로 줄어들어, K8s 환경에서의 스케일링 속도가 크게 개선됩니다.

Istio와의 호환성

Istio 같은 서비스 메시를 이미 사용하고 있다면, Spring Cloud Kubernetes와의 역할 중복이 걱정될 수 있습니다.

역할 분담 정리:

  • ** 서비스 디스커버리 **: Istio가 처리 → Spring Cloud Kubernetes 디스커버리는 비활성화 가능
  • ** 로드밸런싱 **: Istio의 Envoy 프록시가 처리
  • ** 설정 관리 **: Spring Cloud Kubernetes의 ConfigMap 연동은 Istio와 무관하게 유용
  • ** 서킷 브레이커 **: Istio의 Outlier Detection 또는 Resilience4j 중 선택
YAML
# Istio 환경에서 디스커버리만 비활성화
spring:
  cloud:
    kubernetes:
      discovery:
        enabled: false  # Istio가 처리
      config:
        enabled: true   # ConfigMap 연동은 유지

공부하면서 느낀 건, Istio와 Spring Cloud Kubernetes는 경쟁 관계가 아니라 ** 보완 관계 **라는 것입니다. 네트워크 레벨은 Istio에, 애플리케이션 레벨 설정은 Spring Cloud Kubernetes에 맡기는 패턴이 가장 자연스럽습니다.

Eureka vs Spring Cloud Kubernetes 선택 기준

기준EurekaSpring Cloud Kubernetes
실행 환경K8s + VM + 베어메탈 모두 가능K8s 전용
추가 인프라Eureka 서버 운영 필요K8s 자체 기능 활용 (추가 없음)
설정 관리Spring Cloud Config 별도 필요ConfigMap/Secret 네이티브 연동
헬스 체크Eureka heartbeatK8s liveness/readiness 프로브
멀티 클러스터Eureka Peer ReplicationFederation 또는 별도 구성 필요

정리하면:

  • K8s 전용 환경 → Spring Cloud Kubernetes가 운영 부담이 적음
  • K8s + 비K8s 혼합 환경 → Eureka가 환경 독립적이라 유리
  • ** 이미 Eureka를 잘 쓰고 있다면** → 굳이 마이그레이션할 필요 없음
  • ** 새로 시작한다면** → K8s 환경이라면 Spring Cloud Kubernetes로 시작하는 것을 권장

정리

  • Spring Cloud Kubernetes는 K8s의 Service, ConfigMap, Secret을 Spring Cloud 추상화에 직접 매핑
  • DiscoveryClient 코드가 Eureka 때와 완전히 동일하므로, 의존성 교체만으로 전환 가능
  • ConfigMap 변경 시 @RefreshScope와 함께 사용하면 재배포 없이 설정 갱신 가능
  • Discovery Server를 사용하면 RBAC 권한 관리를 단순화할 수 있음
  • K8s 전용 환경이라면 Eureka 대신 Spring Cloud Kubernetes로 운영 복잡도를 줄이는 것이 합리적
댓글 로딩 중...