팀 프로젝트 festabook에서 학습한 내용을 정리한 글입니다.
💭 들어가며
Grafana 스택 구축을 완료했다. 초기에는 비교적 단순한 작업이라 판단했지만, 실제로 구성해 보니 고려해야 할 요소가 많아 예상보다 긴 과정을 거치게 되었다. 그 과정에서 여러 시행착오를 겪었고, 전체 아키텍처와 설정을 정리하는 데 상당한 시간이 필요했다.
정리해야 할 내용의 분량이 많아 하나의 글로 구성하기에는 분량이 과하다고 판단하여, 총 다섯 편의 글로 나누어 정리한다.
해당 글에서는 Grafana 스택의 구성과 역할을 설명하고, 이를 Docker 기반으로 구성하는 방법과 주요 설정 방법을 정리한다.
✅ 구축 계기 및 의사결정
기존에 사용하던 우아한테크코스 인프라 환경에서 쫓겨났기도 했고, 그동안 서비스를 운영하며 쌓여온 여러 요구사항들을 정리할 필요가 있었다. 이를 계기로 모니터링 스택을 다시 점검하고, 보다 적합한 구조를 고민하게 되었다.
기존에는 CloudWatch를 사용하고 있었다. CloudWatch는 각 모니터링 스택이 분리되어 있어 메트릭, 로그, 트레이스를 한 번에 시각화하기 어려웠다. 장애가 발생하거나 사용자 흐름을 추적해야 할 때마다 여러 콘솔을 오가며 원인을 추적해야 했고, 이 과정이 상당히 번거로웠다. 특히 CloudWatch Logs의 경우 조회 비용이 부담스러워, 로그를 적극적으로 활용하기 어려운 점도 문제였다. 물론 CloudWatch는 GUI가 잘 구성되어 있어 초기 구축이 간편하다는 장점이 있다. 하지만 실제 운영 단계에 들어가면서 점점 더 다양한 요구사항이 생겼고, 이를 유연하게 대응하기에는 한계가 느껴졌다. 가장 치명적인 문제는 Blue-Green 배포 과정에서 인스턴스가 새로 생성될 때마다 모니터링 대상 인스턴스를 수동으로 변경해야 했다는 점이었다. 배포 과정에서 사람이 직접 설정을 변경해야 하다 보니 실수의 여지가 있었고, 실제로 실수로 모니터링 데이터가 수집되지 않는 기간이 발생하기도 했다. 또한 각 모니터링 스택마다 Pull과 Push 방식이 제각각이어서 전체 구조를 일관적으로 관리하기도 어려웠다. 이런 상황에서 Promtail이 Deprecated되고, Alloy라는 통합 에이전트가 등장해 메트릭, 로그, 트레이스를 하나의 파이프라인으로 관리할 수 있게 되었다는 글을 봤던 것이 큰 계기가 되었다.
ElasticSearch 스택도 검토 대상이었지만, 현재 서비스는 검색 중심의 서비스가 아니고, 데이터를 기반으로 정교한 사용자 시나리오 분석이나 A/B 테스트를 수행하는 단계도 아니었다. 이에 비해 디스크 리소스 사용량이 크다고 판단하여 이번 구성에서는 제외했다.
DataDog 역시 매우 다양한 기능을 제공하지만, 유료 서비스라는 점에서 학생 신분으로 운영 중인 현 시점에서는 오버 엔지니어링이라고 판단했다.
이러한 고민 끝에 Grafana 스택을 선택하게 되었다. 초기 설정 비용이 다소 크다는 점은 감안해야 했지만, 메트릭, 로그, 트레이스를 하나의 관점에서 통합적으로 관리할 수 있다는 점과 향후 확장 가능성을 고려해 최종적으로 Grafana 스택을 기반으로 모니터링 시스템을 구성하게 되었다.
✅ 전체 아키텍처

본 글에서는 사진의 오른쪽에 표시된 Grafana Server 설정 방법을 설명한다.
위에서 설명했듯이, 본 시스템에서는 Alloy라는 Agent를 중심으로 각 모니터링 스택으로 데이터를 Push하는 구조를 사용하고 있다. Alloy는 메트릭, 로그, 트레이스를 하나의 에이전트에서 수집할 수 있어, 관측 파이프라인을 한 번에 관리하기 편하다는 장점이 있어 도입하게 되었다. 기존 Promtail과 달리 Metric과 Trace까지 함께 수집할 수 있다는 점도 Alloy를 선택한 주요 이유 중 하나이다. 다만 Alloy는 구조적으로 단일 장애점(SPOF)이 될 수 있다는 한계가 있다. Grafana에서는 다중 에이전트 구성을 전제로 Alloy를 설계했지만, 현재 서버 규모와 운영 상황을 고려했을 때 다중 에이전트 구성이 필수적이지 않다고 판단하여 우선은 단일 Alloy 인스턴스로 구성하였다. Alloy의 수집 방식은 데이터 유형에 따라 다르게 구성되어 있는데, 이에 대한 구체적인 설정 방법은 Alloy 설정편에서 자세히 다루도록 하겠다.
한편, 서버 비용을 절약하기 위해 Oracle Cloud Free Tier의 단일 서버를 사용하고 있으며, 해당 서버에는 Grafana 대시보드만 구성되어 있다. 이로 인해 보안적으로 신경 써야 할 부분도 존재했다. 처음에는 Alloy에서 Grafana로 데이터를 전송할 때 Inbound를 특정 Instance의 IP로만 제한하는 방식을 고려했지만, Blue-Green 배포 과정에서 인스턴스 IP가 변경되면서 매번 사람이 수동으로 설정을 변경해야 한다. AWS NAT Gateway에 탄력적 IP를 할당하는 방법도 검토했으나, 비용 부담이 커서 현재는 적용하지 않았다. 대신 키 기반 인증 방식을 대안으로 검토 중이며, 이 부분은 아직 완전히 해결되지 않아 추후 지속적으로 글을 업데이트할 예정이다. 현재는 임시방편으로 전송 포트를 임의로 변경하여 노출을 최소화하고 있는데, 이 때문에 포트 관련 정보는 블로그 상에서 최대한 공개하지 않도록 할 예정이다.
✅ 개념
▶ Grafana
메트릭, 로그, 트레이스를 통합 조회하는 시각화 UI이다.
- 데이터 저장 기능은 없으며 Query 전용
- Prometheus, Loki, Tempo 등을 Data Source로 등록하여 조회
- Dashboard, Alert, Explore 기능 제공
- 기본 포트: 3000
Grafana는 데이터를 수집하거나 저장하지 않는다. 저장소(Prometheus/Loki/Tempo)에 Query만 수행한다.
▶ Prometheus
메트릭 수집 및 시계열 데이터베이스(TSDB)이다.
- HTTP endpoint를 Polling(Scrape) 방식으로 수집
- 주요 수집 대상
- Spring Boot Actuator
- Node Exporter
- Kubernetes metrics
- PromQL을 사용한 시계열 Query 지원
- 기본 포트: 9090
Prometheus는 로컬 디스크 기반으로 동작해 디스크 용량에 영향을 받으며, 노드 장애 시 데이터 유실 위험이 있다. 또한 수평 확장이 불가능해 장기 보관 용도로는 적합하지 않다.
▶ Loki
로그 저장 및 조회 전용 스토리지이다.
- 로그 원문 전체를 인덱싱하지 않고 Label만 인덱싱
- 로그는 압축되어 파일 시스템 또는 Object Storage(S3 등)에 저장
- Elasticsearch 대비 경량 구조 (Full-text 검색 목적에는 부적합)
- 로그 수집 주체
- Promtail (Deprecated)
- Alloy
- 기본 포트: 3100
▶ Tempo
분산 트레이싱 저장소이다.
- OpenTelemetry, Jaeger, Zipkin 등에서 생성된 Trace 저장
- 인덱싱을 최소화하고 TraceID 기반 조회
- 로컬 디스크 또는 Object Storage(S3 등)에 저장
- OTLP 수신 포트
- 4317: gRPC
- 4318: HTTP
▶ Alloy
Grafana Labs 공식 통합 Observability 에이전트이다.
- Promtail 후속 에이전트
- Promtail과 달리 로그, 메트릭, 트레이스를 단일 에이전트에서 처리
- 다중 인스턴스 운영 가능
- 노드 단위 배치가 일반적
- 관리용 UI 제공 (12345 포트)
▶ Mimir
Prometheus 호환 장기 메트릭 스토리지이다.
- Remote Write 방식으로 메트릭 수신
- Prometheus의 로컬 저장 한계를 보완
- 수평 확장 및 고가용성 지원
- 장기 메트릭 보관
- Object Storage(S3 등) 기반 저장에 적합
▶ Node Exporter
호스트(노드) 레벨 메트릭 수집기이다.
- 애플리케이션(Spring) 메트릭만으로는 부족한 시스템 자원 지표를 수집
- 수집 대상
- CPU
- Memory
- Disk
- Network
- 애플리케이션과 무관한 시스템 지표 제공
- 기본 포트: 9100
✅ 설정 방법 (Docker, Configuration)
이번 글에서는 모든 데이터를 로컬 디스크에 저장하는 구성을 전제로 한다. 추후 백업 편에서 Mimir + Object Storage 기반 장기 보관 구조로 전환할 예정이므로, 현재 설정은 로컬 단독 구성의 기준점으로 보면 된다.
/grafana-docker
├─ docker-compose.yml
├─ grafana/
│ └─ datasources/
│ ├─ loki.yml
│ ├─ prometheus.yml
│ └─ tempo.yml
├─ prometheus/
│ └─ prometheus.yml
├─ loki/
│ └─ loki.yml
├─ tempo/
│ └─ tempo.yml
└─ nginx/
└─ conf.d
└─ grafana.conf
위 구조는 각 컴포넌트별 설정 파일의 위치를 한눈에 파악하기 위한 예시용 디렉터리 구성이다. 실제 프로젝트의 세부 폴더 구조나 파일 구성과는 일부 차이가 있을 수 있다.
- Node Exporter는 각 서버 노드에 설치되어야 하므로, 수집 에이전트인 Alloy 파트에서 함께 다룬다.
- Mimir는 백업 편에서 다룬다.
- Nginx는 이번 글의 범위(메트릭/로그/트레이스 구성)와 직접적인 관련이 없다고 판단하여 생략한다.
🔽 전체 docker-compose.yml
services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./grafana/data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/datasources:/etc/grafana/provisioning/datasources
environment:
- GF_SECURITY_ADMIN_USER={그라파나대시보드아이디}
- GF_SECURITY_ADMIN_PASSWORD={그라파나대시보드비밀번호}
networks:
- grafana_network
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/var/prometheus'
- '--storage.tsdb.retention.time=10y'
- '--web.enable-remote-write-receiver'
volumes:
- ./prometheus/config:/etc/prometheus
- ./prometheus/data:/var/prometheus
networks:
- grafana_network
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
ports:
- "3100:3100"
command:
- -config.file=/etc/loki/loki.yml
volumes:
- ./loki/config:/etc/loki
- ./loki/data:/var/loki
networks:
- grafana_network
tempo:
image: grafana/tempo:latest
container_name: tempo
restart: unless-stopped
ports:
- "3200:3200" # HTTP API 그라파나 조회 포트
- "4317:4317" # OTLP gRPC 트레이스 수집 수신 포트
- "4318:4318" # OTLP HTTP 트레이스 수집 수신 포트
- "9095:9095" # Tempo gRPC, 외부 RPC 클라이언트가 Tempo 네이티브 API를 사용하는 경우.
command:
- -config.file=/etc/tempo/tempo.yml
volumes:
- ./tempo/config:/etc/tempo
- ./tempo/data:/var/tempo
networks:
- grafana_network
nginx:
image: nginx
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8443:8443"
volumes:
- ./nginx/cert:/etc/letsencrypt # certbot발급 ssl 인증서 경로
- ./nginx/conf.d:/etc/nginx/conf.d # nginx 설정
- ./nginx/log:/var/log/nginx # nginx 로그
depends_on:
- grafana
- prometheus
- tempo
- loki
networks:
- grafana_network
networks:
grafana_network:
driver: bridge
▶ Grafana
🔽 Grafana Docker (docker-compose.yml)
services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./grafana/data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/datasources:/etc/grafana/provisioning/datasources
environment:
- GF_SECURITY_ADMIN_USER={그라파나대시보드아이디}
- GF_SECURITY_ADMIN_PASSWORD={그라파나대시보드비밀번호}
networks:
- grafana_network
🔽 Datasource 등록
Grafana에서 Prometheus, Loki, Tempo를 사용하려면 각 컴포넌트를 데이터소스로 등록해야 한다. 데이터소스 등록은 Grafana UI(GUI)에서도 가능하지만, provisioning을 활용하면 yml 파일로 사전 정의할 수 있다. 프로비저닝 방식을 사용하면 컨테이너 재시작 시에도 설정이 자동으로 적용되는 이점이 있어 사용했다.
Grafana Datasource에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
- Grafana 공식 문서
- /grafana/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus_ds
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
version: 1
editable: true
- /grafana/datasources/loki.yml
apiVersion: 1
datasources:
- name: Loki
type: loki
uid: loki_ds
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: false
version: 1
editable: true
- /grafana/datasources/tempo.yml
apiVersion: 1
datasources:
- name: Tempo
type: tempo
uid: tempo_ds
access: proxy
orgId: 1
url: http://tempo:3200
basicAuth: false
isDefault: false
version: 1
editable: true
▶ Prometheus
🔽 Prometheus Docker (docker-compose.yml)
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/var/prometheus'
- '--storage.tsdb.retention.time=10y'
- '--web.enable-remote-write-receiver'
volumes:
- ./prometheus/config:/etc/prometheus
- ./prometheus/data:/var/prometheus
networks:
- grafana_network
- -config.file
- Prometheus가 기본 설정 대신 지정한 prometheus.yml을 사용하도록 명시한다.
- --storage.tsdb.retention.time=10y
- Mimir 기반 장기 보관을 도입하기 전까지 임시 로컬 보관을 위한 설정으로 보존 기간을 10년으로 지정했다. 다만 실제 운영 환경에서는 디스크 사용량과 장애 복구 전략을 고려해 더 짧은 보존 기간을 설정하는 것이 일반적이다.
- --web.enable-remote-write-receiver
- Prometheus가 Remote Write 수신 엔드포인트(/api/v1/write)를 열도록 하는 옵션이다.
🔽 Prometheus Configuration (prometheus.yml)
Prometheus 전역 설정에 대한 공식 문서는 다음을 참고한다.
- Prometheus 공식 문서
global:
scrape_interval: 15s
evaluation_interval: 15s
- scrape_interval: 15s
- 타깃 엔드포인트로부터 메트릭을 15초 주기로 수집한다.
- evaluation_interval: 15s
- Recording Rule 및 Alert Rule 평가 주기를 scrape 주기와 동일하게 설정했다.
▶ Loki
🔽 Loki Docker (docker-compose.yml)
services:
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
ports:
- "3100:3100"
command:
- -config.file=/etc/loki/loki.yml
volumes:
- ./loki/config:/etc/loki
- ./loki/data:/var/loki
networks:
- grafana_network
- -config.file
- Loki가 기본 설정 대신 지정한 loki.yml을 사용하도록 명시한다.
🔽 Loki Configuration (loki.yml)
Loki 전역 설정에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
auth_enabled: false
server:
http_listen_address: 0.0.0.0
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
path_prefix: /var/loki
storage:
filesystem:
chunks_directory: /var/loki/chunks
rules_directory: /var/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2025-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
pattern_ingester:
enabled: true
metric_aggregation:
loki_address: localhost:3100
ruler:
enable_alertmanager_discovery: true
enable_api: true
frontend:
encoding: protobuf
compactor:
working_directory: /var/loki/retention
delete_request_store: filesystem
retention_enabled: false
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
metric_aggregation_enabled: true
allow_structured_metadata: true
volume_enabled: true
- auth_enabled: false
- Loki 자체 인증을 비활성화했다. 외부 접근 제어는 리버스 프록시(Nginx) 레벨에서 별도로 처리할 예정이다.
- replication_factor: 1 + ring.kvstore: inmemory
- 단일 Loki 인스턴스를 전제로 한 구성이다.
- reject_old_samples_max_age: 168h
- 7일보다 오래된 로그는 수집 단계에서 거부했다.
- compactor.retention_enabled: false
- Object Storage 기반 백업을 도입하기 전 단계이므로, 로그 보관, 삭제 정책은 아직 적용하지 않았다.
▶ Tempo
🔽 Tempo Docker (docker-compose.yml)
services:
tempo:
image: grafana/tempo:latest
container_name: tempo
restart: unless-stopped
ports:
- "3200:3200" # HTTP API 그라파나 송출 포트
- "4317:4317" # OTLP gRPC 트레이스 수집 수신 포트
- "4318:4318" # OTLP HTTP 트레이스 수집 수신 포트
command:
- -config.file=/etc/tempo/tempo.yml
volumes:
- ./tempo/config:/etc/tempo
- ./tempo/data:/var/tempo
networks:
- grafana_network
- -config.file
- Tempo가 기본 설정 대신 지정한 tempo.yml을 사용하도록 명시한다.
🔽 Tempo Configuration (tempo.yml)
Tempo 전역 설정에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
stream_over_http_enabled : true
server:
http_listen_address: 0.0.0.0
http_listen_port: 3200
grpc_listen_port: 9095
log_level: info
query_frontend:
mcp_server:
enabled: true
search:
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09
metadata_slo:
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09
trace_by_id:
duration_slo: 100ms
metrics:
max_duration: 168h
query_backend_after: 5m
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09
distributor:
usage:
cost_attribution:
enabled: true
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
metrics_generator:
registry:
external_labels:
source: tempo
cluster: docker-compose
storage:
path: /var/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
send_exemplars: true
traces_storage:
path: /var/tempo/generator/traces
processor:
local_blocks:
filter_server_spans: false
flush_to_storage: true
storage:
trace:
backend: local
wal:
path: /var/tempo/wal
local:
path: /var/tempo/blocks
compactor:
compaction:
block_retention: 876000h
overrides:
defaults:
cost_attribution:
dimensions:
service.name: ""
span.http.target: "service_route"
metrics_generator:
processors: [service-graphs, span-metrics, local-blocks]
generate_native_histograms: both
- OTLP 수신 포트 (4317, 4318)
- 애플리케이션 또는 Alloy에서 전송되는 트레이스를 OTLP gRPC, HTTP 방식으로 수신한다.
- compactor.block_retention: 876000h
- Object Storage 기반 백업을 도입하기 전 단계이므로, 로컬 스토리지에 장기간(약 100년) Trace 블록을 유지하도록 설정했다.
📍 참고 자료
'DevOps > Log&Monitoring' 카테고리의 다른 글
| [Log&Monitoring] Grafana Observability 구축 (4) - Mimir, Object Storage 백업 (0) | 2025.12.29 |
|---|---|
| [Log&Monitoring] Grafana Observability 구축 (3) - Grafana 대시보드 (0) | 2025.12.29 |
| [Log&Monitoring] Grafana Observability 구축 (2) - Alloy 기반 수집 파이프라인 (0) | 2025.12.28 |
| [Log&Monitoring] 관측 가능성(Observability) (6) | 2025.08.08 |