사이드프로젝트 slecs

Grafana·Prometheus·Loki로 메트릭·로그·트레이스 통합

목차

관찰 가능성(Observability) 스택을 처음부터 제대로 쌓아보고 싶다는 생각은 꽤 오래됐는데, 이번에 사이드 프로젝트 삼아 Grafana + Prometheus + Loki + Tempo 조합으로 실제 운영 환경에 붙이는 작업을 마무리했다.

메트릭만 보던 시절엔 "그래프 하나 보면 다 나오지 않냐"는 생각이었는데, 막상 운영하다 보면 메트릭만으로는 절반도 알 수가 없다. 특정 시점에 에러가 튀는 건 보이는데 어떤 요청이 어느 서비스를 거쳐서 실패했는지는 로그를 직접 뒤져야 하고, 그 로그가 서버마다 흩어져 있으면 또 SSH 열어서 grep 하는 식이 된다. 세 가지 시그널 - 메트릭, 로그, 트레이스 - 을 한 화면에서 연결해서 볼 수 있어야 디버깅 속도가 달라진다는 걸 직접 겪고 나서야 제대로 세워야겠다 싶었다.

스택 구성

도구 역할
Prometheus 메트릭 수집 (Spring Actuator 연동)
Grafana 대시보드 시각화
Loki + Promtail 로그 집계
Tempo 분산 트레이스

Prometheus는 Spring Boot Actuator의 /actuator/prometheus 엔드포인트를 scrape하는 구조다. Grafana에서 Loki, Tempo를 datasource로 추가해두면 메트릭에서 로그로, 로그에서 트레이스로 바로 넘어갈 수 있다. 로그 한 줄에 traceId가 포함되어 있으면 Tempo 링크로 바로 연결되는데, 이게 생각보다 실용적이다. "이 시점에 에러 났는데 원인이 뭐야"를 한 흐름에서 쫓을 수 있게 된다.

전체 스택은 Docker Compose로 묶었고, CI 파이프라인에서 SSH로 원격 배포하는 구조로 자동화했다. 배포 때마다 손으로 서버 접속해서 docker compose up -d 치는 일을 없애는 게 목표였는데, CI에서 ssh-agent 키를 물려서 원격으로 compose를 내리고 올리는 방식으로 처리했다. 이 부분은 github actions 기준으로 appleboy/ssh-action 같은 걸 쓰면 빠르게 세울 수 있다.

실제로 막힌 것들

두 군데에서 꽤 오래 헤맸다.

내부망 주소 문제. 모니터링 서버에서 WAS 서버의 Actuator 엔드포인트를 scrape하려는데 내부 IP로는 통신이 안 됐다. 서버끼리 같은 내부 네트워크에 있다고 가정하고 설정을 짰는데, 실제 라우팅이 달랐던 것. 공인 IP로 바꾸니까 바로 해결됐다. "같은 망이겠지"라는 가정 아래 설정 짜면 이런 데서 꼭 한 번씩 걸린다. 인프라 다이어그램 없이 머릿속으로만 구조 잡는 것의 부작용이기도 하다.

Prometheus host 네트워크 모드. 컨테이너로 뜬 Prometheus가 호스트의 포트에 직접 접근해야 할 때, 기본 bridge 모드로는 localhost:포트가 Prometheus 컨테이너 자신의 localhost를 가리켜서 scrape이 안 된다. network_mode: host로 바꾸면 호스트 네트워크를 공유하니까 해결되는데, 이 설정이 다른 compose 항목과 충돌하는 경우가 있어서 한참 헤맸다.

# docker-compose.yml 중 prometheus 서비스
prometheus:
  image: prom/prometheus:latest
  network_mode: host
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
    - prometheus_data:/prometheus
  restart: unless-stopped

host 모드를 쓰면 ports: 항목을 별도로 지정할 필요가 없고, 오히려 지정하면 에러가 난다. 처음에 ports: - "9090:9090" 남겨두고 왜 안 뜨나 한참 봤는데 그게 원인이었다. host 모드일 때 ports: 항목은 무조건 제거.

Grafana datasource provisioning은 파일로 관리하는 방식을 택했다. 재배포 때마다 UI에서 datasource 다시 추가하는 건 자동화 흐름에 맞지 않으니까.

# grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://localhost:9090
    isDefault: true
  - name: Loki
    type: loki
    url: http://localhost:3100
  - name: Tempo
    type: tempo
    url: http://localhost:3200

이 파일을 compose 볼륨으로 마운트해두면 컨테이너가 올라올 때 datasource가 자동으로 잡힌다. 스택을 날리고 다시 올려도 설정이 그대로 유지되는 게 핵심이다. Grafana 대시보드 JSON도 같은 방식으로 provisioning 디렉터리에 넣어두면 재배포 후 대시보드도 자동으로 복원된다.

외부 접근 제한과 트레이드오프

Grafana 자체에 인증이 있긴 하지만, 포트 자체를 공인 IP로 열어두는 건 신경 쓰였다. Prometheus UI나 Loki 엔드포인트는 인증 없이 데이터를 뽑아갈 수 있는 구조라 더 그렇다. Nginx에서 IP 접근 제한을 걸어서 허용된 대역에서만 모니터링 UI에 접근할 수 있게 막아뒀다.

location /grafana/ {
    allow 1.2.3.0/24;   # 허용 대역
    deny all;
    proxy_pass http://localhost:3000/;
    proxy_set_header Host $host;
}

이렇게 하면 외부에서 Grafana 로그인 화면 자체가 안 뜬다. 모니터링 도구가 공격 표면이 되는 걸 막는 가장 간단한 방법이다. 허용 IP를 관리하는 게 번거롭긴 한데, VPN 없이 특정 IP만 허용하는 구조에선 이 방식이 제일 단순하다.

트레이드오프는 있다. 외부에서 급하게 대시보드 봐야 할 때 IP가 다르면 접근이 안 된다. 장기적으로는 VPN + 내부 DNS로 가거나, Grafana 앞에 별도 인증 프록시를 두는 게 낫긴 한데, 지금 단계에서는 IP 제한으로 충분하다고 판단했다.

작업 규모 자체는 크지 않았다. 근데 이걸 해두기 전과 후가 실제로 다르다. 뭔가 이상하다 싶을 때 대시보드 열면 어느 시점부터 메트릭이 꺾이는지, 그 시점에 어떤 로그가 찍혔는지 바로 연결해서 볼 수 있다는 게 안정감이 다르다. Prometheus scrape target 주소 실수가 제일 흔한 오류라는 건 이번에 직접 확인했고, Docker 네트워크 모드가 모니터링 스택 구성에서 생각보다 훨씬 중요한 변수라는 것도. 컨테이너 안에서 밖의 서비스를 바라봐야 하는 구조는 네트워크 경계를 먼저 명확히 그려놓고 설정하는 게 맞다.

댓글 0

첫 댓글 달아줘.