카테고리 없음

스프링 엑추에이터와 커스터마이징 (1)

tally 2024. 3. 17. 22:31

서론

애플리케이션을 프로덕션 환경에서 운영하기 위해서는 기능적인 것들 뿐만 아니라 비 기능적인 요소들도 구성되어 있어야 합니다. 이러한 비 기능적인 요소들은 서비스를 안정적이게 운영하고, 애플리케이션을 보호하고, 장애 상황에 문제를 탐지하고 분석하는데 용이한데요. 
오늘은 이 중에서도 애플리케이션의 운영과 관리에 용이하게 하는 스프링 부트에서 제공하는 엑추에이터를 소개하려고 합니다.
 
 

Spring Actuator

엑추에이터는 애플리케이션이나 데이터베이스의 상태, 메트릭, 로그, 스레드 상태 등에 대한 애플리케이션을 관리하는데 도움이 되는 여러 기능들을 HTTP 엔드포인트나 JMX 형태로 제공합니다.
 
엑츄에이터가 제공해주는 기능을 사용하려면 두 가지 설정이 선행되어야 합니다.

  • 엔드포인트 활성화
  • 엔드포인트 노출

엑추에이터는 기본적으로 shutdown(애플리케이션 종료)을 제외한 나머지 엔드포인트에 대해서 활성화(enabled) 상태로 되어있습니다.

management:
  endpoint:
    health:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "health"

 
이처럼 엑추에이터가 제공해주는 기능의 엔드포인트를 활성화 및 노출을 하게되면 기본 경로와 속성 경로(/actuator/{id})로 접근하면 기능에 따른 정보를 제공받을 수 있습니다.

 
 
엑추에이터의 기본 경로나 포트를 변경할 수도 있습니다.
 
기본 경로 변경
application.properties에 아래의 설정을 하게 되면 기본 경로를 변경할 수 있습니다.

management.endpoints.web.base-path=/manage

 
포트 변경
기본적으로 엑추에이터는 애플리케이션 포트와 동일한 포트를 사용하게 됩니다. 그렇기 때문에 퍼블릭 서브넷에 위치한 서버의 경로만 알고있다면 누구나 외부 인터넷 망을 통해 애플리케이션의 정보에 접근할 수 있게 됩니다. 따라서, 내부망에서만 접근 가능한 포트로 설정하여 외부망에서 접근이 불가능하도록 설정하는 것을 권장합니다.
기본 경로 변경과 마찬가지로 포트를 변경하는 설정을 application.properties에 추가하면 엑추에이터의 기본 포트 번호를 변경할 수 있게 됩니다.

management.server.port=8082

 
 

Actuator의 주요 엔드포인트

엑츄에이터가 제공하는 일부 기능을 알아보도록 하겠습니다.

엑추에이터가 제공하는 전체 기능은 공식 문서를 참고해주세요.

Spring Boot 3.2.3 Actuator Doc.
https://docs.spring.io/spring-boot/docs/3.2.3/actuator-api/htmlsingle/

 
health
애플리케이션의 상태 정보를 제공하는 기능으로 주로 Load Balancer나 probe가 가용성을 유지하고, 서비스 중단이나 요청 실패를 줄이기 위해 애플리케이션의 상태를 확인하는 용도로 사용되는 기능입니다.
 
헬스 정보를 요청에 대한 응답이 가능한 상태만을 나타낼수도 있지만, 애플리케이션과 연결된 데이터베이스가 정상적인지 또는 서버의 사용량에는 문제가 없는지 등과 같은 구체적인 정보들도 포함하여 나타낼 수도 있습니다. 

management.endpoint.health.show-details: always

 
위의 설정을 적용하면 여러 데이터베이스가 연결된 애플리케이션 서버의 각 데이터베이스에 대한 헬스 체크도 가능하게 됩니다.

 
 
loggers
애플리케이션의 로그와 각각의 로그 설정(설정된 로그 레벨, 적용된 로그 레벨)에 대한 정보를 제공합니다.
 
응답에 두 가지의 설정 정보가 존재하는데요. 각각이 의미하는 바는 다음과 같습니다.

  • configuredLevel: 설정된 로그의 레벨
  • effectiveLevel: 실제로 적용된 로그의 레벨

 
 
이러한 정보 외에도 애플리케이션이 실행중인 상태에서 로그의 레벨을 변경하는 것도 가능합니다.
 
일반적으로는 로그의 레벨을 변경하려면 애플리케이션의 설정 정보에 로그 레벨 설정을 변경하고 배포하여 서버를 재시작 해주어야 합니다. 이처럼 서비스 운영중에 급히 DEBUG나 TRACE로 변경을 해야 하는 상황이 생겼을 때 실시간으로 로그 레벨을 변경할 수 있습니다.
 
위 이미지와 같이 현재 실행중인 애플리케이션의 ROOT 경로에 대한 로그 레벨이 INFO로 설정되어 있다고 가정해보겠습니다.  
(자바 진영에서 스프링 부트를 사용한다면 일반적으로 Slf4j의 구현체인 Logback을 사용합니다.)
https://logback.qos.ch/manual/architecture.html#LoggerContext

Chapter 2: Architecture

All true classification is genealogical. —CHARLES DARWIN, The Origin of Species It is difficult, if not impossible, for anyone to learn a subject purely by reading about it, without applying the information to specific problems and thereby forcing himsel

logback.qos.ch

 
변경하고자 하는 로그 경로 엔드포인트에 POST 요청으로 변경하고자 하는 로그 레벨을 바디에 담아 전달하면 해당 로그 레벨로 서버 재시작없이 로그 레벨이 변경되게 됩니다.

$ curl -X POST http://localhost:8080/actuator/loggers/ROOT \
        -H "Content-Type: application/json" \
        -d '{"configuredLevel": "DEBUG"}'

 

 
 

엑추에이터 커스터마이징

필요에 따라서는 기본적으로 제공해주는 기능에 추가적인 세부 정보들을 함께 제공할 수 있습니다.
 
한 가지 예시로, 애플리케이션의 헬스 체크를 할 때 필요로 하는 외부 서비스의 헬스 체크를 하고 싶은 상황이라고 가정해보겠습니다.
XxxIndicator 인터페이스로 된 클래스를 상속받아 상세 정보를 추가하면 원하는 커스텀한 응답 값들을 함께 전달받을 수 있습니다.
 
아래의 예시는 위 조건에 맞게 의존하는 외부 서비스의 상태를 확인하여 상태 코드에 따라 헬스 체크에 추가 정보를 담는 코드입니다.

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {

    private final Random randomizer = new Random();
    private final List<Integer> statusCodes = List.of(200, 204, 401, 404, 502, 503);
    private final static Map<Integer, Supplier<Health.Builder>> statusCodeHandlers;

    static {
        statusCodeHandlers = Map.of(
                200, () -> Health.up()
                        .withDetail("External_Service", "UP")
                        .withDetail("url", "https://external-service.com"),
                204, () -> Health.up()
                        .withDetail("External_Service", "UP")
                        .withDetail("url", "https://external-service.com"),
                502, () -> Health.down()
                        .withDetail("External_Service", "Down")
                        .withDetail("alternative_url", "https://static-external-service.com"),
                503, () -> Health.down()
                        .withDetail("External_Service", "Down")
                        .withDetail("alternative_url", "https://static-external-service.com"),
                401, () -> Health.unknown()
                        .withException(new RuntimeException("Received status: 401")),
                404, () -> Health.unknown()
                        .withException(new RuntimeException("Received status: 404"))
        );
    }

    @Override
    public Health health() {
        int randomStatusCode = statusCodes.get(randomizer.nextInt(statusCodes.size()));

        return statusCodeHandlers.getOrDefault(randomStatusCode, () -> Health.unknown().withException(new RuntimeException("Unexpected status: " + randomStatusCode))).get().build();
    }
}

 
 
외부 서비스의 서버 상태가 정상적이지 않아서 502, 503의 응답을 받았다면 추가 정보들이 설정한 대로 나타나는 것을 확인할 수 있습니다.

 
 

정리

본 글에서는 소개하지 않았지만 엑추에이터는 환경 설정 정보, 메트릭 정보, 쓰레드/힙 정보 등 애플리케이션에 대한 많은 정보들을 제공합니다. 그런 만큼, 외부망에서는 엑추에이터에 접근하지 못하도록 막고, 필요한 정보들만 사용하도록 구성하는 것이 바람직합니다.
 
혹여나 보안적으로 더 알아가고자 한다면 아래 블로그를 추천드립니다.
https://techblog.woowahan.com/9232/

Security Actuator 안전하게 사용하기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요, 우아한형제들 SOC팀에서 Application Security를 담당하고 있는 권현준입니다. 오늘 준비한 주제는 개발자에게 편리함을 제공하나, 잘못 사용하면 매우 위험한 Actuator를 안전하

techblog.woowahan.com

 
 

참고자료