사내에 쿠버네티스 Probes를 도입하면서 Spring Boot 환경에서의 동작 과정을 확인하고 어떻게 적용하면 좋을지 정리봅니다.
Kubernetes Probes
- 쿠버네티스는 probe를 확인하여 컨테이너의 상태를 확인하고, 컨테이너를 관리한다.
- kubelet은 Readiness probe를 통해 트래픽을 받아들일 수 있는지 확인한다.
- 애플리케이션은 네트워크 연결 설정, 파일 로드 및 캐시 워밍 같은 초기 작업에 시간이 많이 걸린다. Spring Boot도 애플리케이션이 시작하자마자 요청을 받아들일 수 있는 상태가 되는 것이 아니기에 순단이 발생할 수 있다.
- Readiness probe를 확인하면 트래픽 수락이 준비 되었을 때, 트래픽을 받도록 할 수 있다.
- kubelet은 Liveness probe를 통해 컨테이너가 살아있는지 확인한다.
- Liveness probe를 반복적으로 실패하면, kubelet은 컨테이너를 다시 시작한다.
kubelet 각 노드에서 실행되는 Kubernetes의 주요 "노드 에이전트"이다. 이는 Pod를 관리하고 컨테이너가 정상적으로 실행되도록 하는 역할을 한다. PodSpec을 통해 제공되는 컨테이너 설명을 가져와 실행하며, apiserver를 통해 제공된 PodSpec을 기반으로 동작한다. kubelet은 Kubernetes에서 생성된 컨테이너만을 관리하며, 다양한 메커니즘을 통해 PodSpec을 처리한다.
Liveness and Readiness in Spring Actuator
- spring-boot-actuator
- actuator에는 헬스 체크를 위한 클래스가 구현되어 있다.
- LivenessStateHealthIndicator와 ReadinessStateHealthIndicator 클래스를 통해 애플리케이션의 Liveness 및 Readiness 상태를 보여준다.
- 애플리케이션을 k8s에 배포하면 Spring Boot가 자동으로 이러한 상태 표시기를 등록한다.
- LifeCycle에 대한 이벤트를 수신하고 상태를 업데이트 한다.
- 그 결과 /actuator/health/liveness 및 /actuator/health/readiness 엔드포인트를 각각 활성 및 준비 상태 probe로 사용할 수 있다.
- 쿠버네티스에만 적용되는 것이 아니라 다른 플랫폼에서도 사용할 수 있다.
management.endpoint.health.probes.enabled=true
속성을 사용한다.
Spring 구동 중 Readiness and Liveness 상태 변경 플로우
SpringApplication.run()
ApplicationListener
와ApplicationContextInitializer
등록
- ApplicationEnvironment 생성
- properties 파일, 환경 변수, 커맨드 라인 인자등을 처리
- ApplicationContext 생성
- (예:
AnnotationConfigServletWebServerApplicationContext
)
- ApplicationContext 준비
ApplicationContextInitializer
들 실행ContextLoaderListener
등록 (서블릿 환경의 경우)
- Bean 정의 로딩
- 컴포넌트 스캔,
@Configuration
클래스 처리
- ApplicationContext 새로고침
- Bean 인스턴스화, Bean 의존성 주입
- Liveness 상태를 CORRECT로 변경
DispatcherServlet
등록 및 초기화
- Readiness 상태를 ACCEPTING_TRAFFIC으로 변경

- 첫번째는 @PostConstruct 어노테이션을 사용했을 때 로그이다.
- @PostConstruct은 빈 생성자가 호출되고, 의존성 주입이 완료(빈의 초기화)된 직후 호출된다.
- LivenessState : BROKEN, ReadinessState : REFUSING_TRAFFIC
- WebApplicationContext 초기화만 완료된 상태로는 상태를 변경하지 않는다.
- 두번째는 ApplicationRunner 인터페이스를 구현했을 때 로그이다.
- ApplicationRunner의 run() 콜백 메서드는 Spring application context가 인스턴스화 되고 애플리케이션이 시작했을 때 호출된다.
- LivenessState : CORRECT, ReadinessState : REFUSING_TRAFFIC
- LivenessState는 애플리케이션이 시작되었을 때 정상 동작으로 인식하고 상태를 변경한다.
- 이 시점에 ReadinessState는 상태 변경이 되지 않았다. 이처럼 Spring Boot는 시작하자마자 사용자의 요청을 받을 수 있는 상태가 되는 것이 아니며 이 때문에 ReadinessState 체크가 필요하다.
- 세번째는 DispatcherServlet이 초기화된 이후 시점의 로그이다.
- LivenessState : CORRECT, ReadinessState : ACCEPTING_TRAFFIC
- ReadinessState는 DispatcherServlet이 초기화 되어 트래픽을 받을 수 있을 때 상태가 변경됨을 알 수 있다.
Availability 조회 및 업데이트
Availability 조회
@Autowired private ApplicationAvailability applicationAvailability; public void checkAvailability() { LivenessState livenessState = applicationAvailability.getLivenessState(); ReadinessState readinessState = applicationAvailability.getReadinessState(); }
- ApplicationAvailability 인터페이스를 통해 readiness와 liveness 상태를 조회할 수 있다.
- ApplicationAvailability에는 ApplicationAvailabilityBean라는 구현체가 주입된다.
Availability 업데이트
AvailabilityChangeEvent.publish(context, LivenessState.BROKEN);
- AvailabilityChangeEvent 이벤트를 게시하여 응용 프로그램 상태를 업데이트할 수도 있다.
Spring Boot 설정
implementation 'org.springframework.boot:spring-boot-starter-actuator'
- 우선 actuator 종속성을 추가해야 한다.
- 다음은 AvailabilityProbes를 활성화시켜야 하는데 이를 위한 두 가지 조건이 있다.
- 쿠버네티스 배포 환경인가?
- ‘management.health.probes.enabled’ 속성이 false가 아닌가?
- 위 조건 두 가지 중 하나만 만족하면 활성화 되는데 코드로는 다음과 같다.
private static final String ENABLED_PROPERTY = "management.endpoint.health.probes.enabled"; private static final String DEPRECATED_ENABLED_PROPERTY = "management.health.probes.enabled"; @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); ConditionMessage.Builder message = ConditionMessage.forCondition("Probes availability"); ConditionOutcome outcome = onProperty(environment, message, ENABLED_PROPERTY); if (outcome != null) { return outcome; } outcome = onProperty(environment, message, DEPRECATED_ENABLED_PROPERTY); if (outcome != null) { return outcome; } if (CloudPlatform.getActive(environment) == CloudPlatform.KUBERNETES) { return ConditionOutcome.match(message.because("running on Kubernetes")); } return ConditionOutcome.noMatch(message.because("not running on a supported cloud platform")); }
- management.health.probes.enabled은 혼돈을 줄 수 있다하여 Spring Boot 2.3.2부터 deprecated 되었지만, 코드 상으로는 아직 사용은 가능하다.
- CloudPlatform.KUBERNETES 여부를 확인하여 쿠버네티스 배포 환경인지 확인한다.
- CloudPlatform는 클라우드 플랫폼 감지를 위한 enum 클래스이다.
- 사내 프로젝트는 aws eks에 배포되어 있기 때문에 쿠버네티스로 감지할 수 있을지 의문이 생겼다.
- 더불어 ec2 등에서는 어떻게 감지될지 확인해보도록 하자.
@Autowired private Environment environment; public void checkCloudPlatform() { CloudPlatform platform = CloudPlatform.getActive(environment); System.out.println("Detected Cloud Platform: " + platform); }
Detected Cloud Platform: null // ec2 Detected Cloud Platform: KUBERNETES // aws eks
- 각 서버에 올려서 확인해본 결과, ec2에서는 null, eks는 KUBERNETES로 감지되었다.
private boolean isAutoDetected(ConfigurableEnvironment environment) { PropertySource<?> environmentPropertySource = environment.getPropertySources() .get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); if (environmentPropertySource != null) { if (environmentPropertySource.containsProperty(KUBERNETES_SERVICE_HOST) && environmentPropertySource.containsProperty(KUBERNETES_SERVICE_PORT)) { return true; } if (environmentPropertySource instanceof EnumerablePropertySource) { return isAutoDetected((EnumerablePropertySource<?>) environmentPropertySource); } } return false; } public static CloudPlatform getActive(Environment environment) { if (environment != null) { for (CloudPlatform cloudPlatform : values()) { if (cloudPlatform.isActive(environment)) { return cloudPlatform; } } } return null; }
- CloudPlatform 클래스에서 쿠버네티스 감지는 환경변수에 “KUBERNETES_SERVICE_HOST” 와 “KUBERNETES_SERVICE_PORT”가 있는 지로 체크하고 있다. 때문에 쿠버네티스 환경이 eks여도 상관이 없었던 것이다.
- 또한, CloudPlatform에 AZURE는 있지만 AWS는 존재하지 않는다. 환경 변수에서 AWS 서비스라고 특정할 수 있는 요소가 없는 것이 아닌가 생각된다.
- 결국 쿠버네티스 Probes를 적용하기 위해 필요한 설정은 actuator 종속성을 추가하는 것 뿐이다.
Actuator 보안 설정 관련
actuator는 health 체크 뿐만 아니라 환경 변수 등 다양한 정보들을 제공해준다. 때문에 편리하지만 보안에 주의할 필요가 있다. 기본적으로 주의해야할 endpoint는 다음과 같다.
- env endpoint
- heapdump endpoint
- Shutdown endpoint
서비스 운영에 사용되는 포트와 다른 포트 사용, 접속 시 보안 설정 등 권장되는 다양한 설정들이 있지만 probe 확인을 위한 설정을 추리면 다음과 같다.
- management.endpoints.enabled-by-default = false
- 기본적으로는 shutdown endpoint를 제외하고 전부 활성화되어 있다. 전체 비활성화하고 필요한 것만 활성화 한다.
- management.endpoint.[endpoint name].enable 같은 형태로 활성화 할 수 있다.
- management.endpoints.web.exposure.include
- endpoint는 활성화가 되어도 노출 설정이 되어야 외부에서 접근이 가능하다. HTTP 방식의 경우
health
endpoint만 노출되어있다.
- management.endpoints.jmx.exposure.exclude
- JMX는 기본으로 노출되어있는 endpoint가 많기 때문에, 사용하지 않는다면 사용 불가하도록 설정한다.
- management.endpoints.web.base-path
- 기본 설정 경로부터 확인하여 공격하기 때문에 유추하기 어려운 문자열의 경로로 설정함으로써 보안성을 향상시킬 수 있다.
위와 같은 사항을 고려하여 최종적으로 다음과 같이 설정하였다.
management: endpoints: enabled-by-default: false jmx: exposure: exclude: '*' web: base-path: '/{base-path}' exposure: include: health endpoint: health: enabled: true
설정 중 management.endpoints.enabled-by-default 로 전체 endpoint를 비활성화시켰기 때문에
health
endpoint를 활성화시켜야 /health/liveness
와 /health/readiness
endpoint도 사용할 수 있다.Convention over Configuration(CoC)
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration. [ Spring Boot OverView ]
사실 probes를 연동하기 위해 대단한 작업이 필요한 것은 아니다. actuator는 이벤트 구독 방식으로 상태값을 변경하여 그 값을 반환하지만, 단순히
/health
경로로 요청/응답 할 수 있는 API 하나만 만들어도 원하는 목적을 달성하는 데에는 충분하다. 이를 고려하면 "왜 굳이 이 정도의 기능을 만들어 사용해야 하는가?"라는 의문이 든다.이는 Convention over Configuration(설정보다 관례, CoC)라는 디자인 패러다임을 따른 것이다. CoC는 소프트웨어 개발에서 명시적인 설정을 최소화하고, 대신 일반적으로 사용하는 관례나 기본값을 활용하는 것이다.
CoC가 적용되면, 개발자들은 불필요하게 설정을 하는 데에 시간을 보낼 필요가 없다. 또한, 관례적으로 설정할 사항들까지 설정을 일일이 확인해야 하는 수고스러움도 없앨 수 있다. Spring Boot는 이러한 철햑을 적극적으로 수용하고 있다. (예: WAS로 Tomcat이 내장되는 등)
probes 설정으로 돌아가 보자. 앞서 말한 것처럼 단순히 API만 구현할 수도 있고, actuator처럼 Readiness와 Liveness 상태를 각각 체크하여 상태값을 반환할 수도 있다. 이렇게 개발자가 일일이 설정해야 한다면 구현 방법에서부터 선택지가 늘어난다. 그러나 대부분의 경우 최선의 방법은 이미 정해져 있다.
따라서 최선의 방법(Readiness와 Liveness 상태를 각각 이벤트로 체크하여 처리하는 방식)을 관례로 미리 정해 구현해 놓고, 개발자가 고민 없이 사용할 수 있도록 하는 것이다.
Share article