개요
운영 중인 Spring Boot 서버가 저사양이라 Prometheus + Grafana를 동일 서버에 올리기 어려웠습니다. Prometheus와 Grafana는 합쳐서 400~800MB의 메모리를 사용하기 때문에 별도 모니터링 서버로 분리하는 것이 필수였습니다.
전체 아키텍처
1
2
3
4
[앱 서버 (저사양)] [모니터링 서버 (별도)]
Spring Boot App ←────── Prometheus (메트릭 수집)
/actuator/prometheus Grafana (시각화)
(메트릭 노출)
- 앱 서버: Spring Boot Actuator로
/actuator/prometheus엔드포인트 노출 - 모니터링 서버: Prometheus가 주기적으로 앱 서버에서 메트릭을 수집하고, Grafana가 이를 시각화
Step 1. Spring Boot 앱 설정
1-1. build.gradle 의존성 추가
1
2
3
// Monitoring (Prometheus + Actuator)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
spring-boot-starter-actuator:/actuator/prometheus엔드포인트를 제공micrometer-registry-prometheus: JVM, HTTP, DB 커넥션 등의 메트릭을 Prometheus 포맷으로 변환
1-2. application.yml 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
base-path: /actuator
endpoint:
health:
show-details: always
prometheus:
enabled: true
metrics:
tags:
application: addiction-prd # 환경별로 구분 (dev/prd)
1-3. Spring Security 설정
Spring Security가 적용된 프로젝트라면 두 곳을 수정해야 합니다.
SecurityConfig.java - 인가(Authorization) 예외 처리:
1
2
3
4
5
6
7
8
private RequestMatcher[] getPublicMatchers() {
return new RequestMatcher[] {
new AntPathRequestMatcher("/api/v1/jwt/**"),
new AntPathRequestMatcher("/api/v1/auth/**"),
// ...
new AntPathRequestMatcher("/actuator/**") // 추가
};
}
JwtAuthenticationFilter.java - 인증(Authentication) 필터 예외 처리:
1
2
3
4
5
6
private static final String[] excludePath = {
"/api/v1/auth",
"/docs",
"/health-check",
"/actuator" // 추가
};
SecurityConfig의permitAll()만 추가해서는 안 됩니다. JWT 필터는 Spring Security의 인가 체크보다 먼저 실행되므로,shouldNotFilter에도/actuator경로를 반드시 추가해야 합니다.
Step 2. 모니터링 서버 설정
디렉토리 구조
1
2
3
4
/home/ubuntu/monitoring/
├── docker-compose.yml
└── prometheus/
└── prometheus.yml
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: always
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
- '--web.external-url=https://도메인/prometheus'
- '--web.route-prefix=/'
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123!
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://도메인/grafana
- GF_SERVER_SERVE_FROM_SUB_PATH=true
depends_on:
- prometheus
volumes:
prometheus_data:
grafana_data:
nginx의 서브패스(/prometheus, /grafana)를 통해 접근할 경우 반드시 아래 옵션이 필요합니다.
- Prometheus:
--web.external-url,--web.route-prefix=/ - Grafana:
GF_SERVER_ROOT_URL,GF_SERVER_SERVE_FROM_SUB_PATH=true
prometheus.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'addiction-dev'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['dev서버IP:8080']
labels:
env: 'dev'
- job_name: 'addiction-prd'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets: ['prd서버IP:8080']
labels:
env: 'prd'
실행
1
2
3
4
5
6
7
8
9
10
11
12
13
# Docker 설치
sudo apt-get update
sudo apt-get install -y docker.io docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker ubuntu
newgrp docker
# 실행
cd /home/ubuntu/monitoring
docker-compose up -d
# 상태 확인
docker-compose ps
Step 3. 앱 서버 방화벽 설정
Prometheus가 앱 서버의 8080 포트로 메트릭을 수집할 수 있도록, 모니터링 서버 IP에서만 접근을 허용합니다.
1
2
# 앱 서버에서 실행 (dev, prd 각각)
sudo ufw allow from 모니터링서버IP to any port 8080
AWS EC2라면 Security Group 인바운드 규칙에도 TCP 8080 / 모니터링서버IP/32를 추가해야 합니다.
Step 4. nginx 설정 (서브패스 프록시)
1
2
3
4
5
6
7
8
9
10
11
12
13
location /grafana {
proxy_pass http://localhost:3000/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /prometheus {
proxy_pass http://localhost:9090/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Step 5. Grafana 대시보드 설정
https://도메인/grafana접속 후 로그인- Connections > Data sources > Add new data source → Prometheus 선택
- URL 입력 후 Save & Test
- Dashboards > Import → ID
19004입력 → Load → Import
Grafana에서 Prometheus 데이터소스 URL은 Grafana 컨테이너 내부에서 사용하므로, nginx를 거치지 않고 컨테이너 간 직접 통신하는
http://prometheus:9090을 사용하는 것이 권장됩니다.
트러블슈팅
1. prometheus.yml이 디렉토리로 마운트되는 오류
오류 메시지:
1
2
Are you trying to mount a directory onto a file (or vice-versa)?
Check if the specified host path exists and is the expected type
원인: prometheus/prometheus.yml 파일이 존재하지 않는 상태에서 docker-compose up을 실행하면 Docker가 해당 경로를 파일이 아닌 디렉토리로 생성합니다.
해결:
1
2
3
4
5
6
7
8
9
10
11
# 잘못 생성된 디렉토리 제거
rm -rf /home/ubuntu/monitoring/prometheus/prometheus.yml
# 파일 직접 생성
cat > /home/ubuntu/monitoring/prometheus/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
...
EOF
docker-compose down && docker-compose up -d
2. prometheus.yml YAML 파싱 오류
오류 메시지:
1
2
yaml: unmarshal errors:
line 10: field labels not found in type config.ScrapeConfig
원인: labels의 위치가 잘못됐습니다. labels는 static_configs 내부의 각 타겟 항목 아래에 위치해야 합니다.
AS-IS (잘못된 구조):
1
2
3
4
5
6
scrape_configs:
- job_name: 'addiction-dev'
static_configs:
- targets: ['서버IP:8080']
labels: # ← 잘못된 위치 (ScrapeConfig 레벨)
env: 'dev'
TO-BE (올바른 구조):
1
2
3
4
5
6
scrape_configs:
- job_name: 'addiction-dev'
static_configs:
- targets: ['서버IP:8080']
labels: # ← 올바른 위치 (targets 항목 아래)
env: 'dev'
3. Grafana 데이터소스 - 502 Bad Gateway
오류 메시지:
1
502 Bad Gateway - There was an error returned querying the Prometheus API.
원인: nginx는 /prometheus → http://localhost:9090으로 프록시 설정이 되어 있었지만, Prometheus 컨테이너가 YAML 파싱 오류로 계속 재시작(Restarting) 상태였기 때문입니다.
진단:
1
2
3
docker ps -a # Restarting 상태 확인
docker logs prometheus # 오류 원인 확인
curl http://localhost:9090/-/healthy
해결: prometheus.yml의 YAML 구조 오류를 수정 후 재시작
4. Grafana 데이터소스 - 404 Not Found
오류 메시지:
1
404 Not Found - There was an error returned querying the Prometheus API.
원인: Grafana 컨테이너 내부에서 http://prometheus:9090으로 연결 시 Docker 내부 DNS(127.0.0.11)가 제대로 동작하지 않았습니다.
해결: Docker 컨테이너 간 직접 통신 대신, 호스트의 Docker 브리지 IP를 사용합니다.
1
2
# Docker 네트워크 게이트웨이 IP 확인
docker network inspect monitoring_default | grep Gateway
데이터소스 URL을 http://172.18.0.1:9090 형태로 설정하거나, Prometheus가 정상 기동된 후 다시 http://prometheus:9090을 시도합니다.
5. actuator 엔드포인트 401 Unauthorized
오류 메시지:
1
{"statusCode":401,"httpStatus":"UNAUTHORIZED","message":"JWT토큰이 수신되지 않았거나 형식이 맞지않습니다."}
원인: Spring Security의 SecurityConfig에 /actuator/**를 permitAll()로 추가했지만, JWT 인증 필터(JwtAuthenticationFilter)가 Spring Security의 인가 체크보다 먼저 실행되어 401을 반환했습니다.
JwtAuthenticationFilter는 OncePerRequestFilter를 상속하며, shouldNotFilter 메서드로 특정 경로를 필터에서 제외할 수 있습니다.
AS-IS:
1
2
3
4
5
6
private static final String[] excludePath = {
"/api/v1/auth",
"/docs",
"/health-check"
// /actuator 누락!
};
TO-BE:
1
2
3
4
5
6
private static final String[] excludePath = {
"/api/v1/auth",
"/docs",
"/health-check",
"/actuator" // 추가
};
Spring Security에서
permitAll()은 인가(Authorization) 레이어를 우회하지만, 그 이전에 실행되는 커스텀 필터는 별도로 처리해야 합니다.
6. prd 서버 도메인 타겟 스크랩 실패
원인: Blue/Green 배포 환경에서 포트가 변경되므로 IP:port 대신 도메인을 타겟으로 설정했는데, targets에 포트를 명시하지 않으면 Prometheus가 기본 포트인 80으로 접근을 시도합니다. HTTPS 도메인이므로 443으로 연결해야 합니다.
AS-IS (잘못된 설정):
1
2
3
4
- job_name: 'addiction-prd'
scheme: https
static_configs:
- targets: ['api.quitmate.co.kr'] # 포트 미지정 → 80 포트로 시도
TO-BE (올바른 설정):
1
2
3
4
- job_name: 'addiction-prd'
scheme: https
static_configs:
- targets: ['api.quitmate.co.kr:443'] # 443 포트 명시
Blue/Green 배포처럼 앱 서버의 포트가 동적으로 변경되는 환경에서는 nginx가 라우팅을 담당하므로, IP:port 대신 도메인:443으로 설정하는 것이 올바른 방법입니다.
최종 확인
1
2
3
4
5
6
7
# 앱 서버에서 메트릭 정상 노출 확인
curl http://localhost:8080/actuator/prometheus
# → jvm_memory_used_bytes, http_server_requests_seconds 등 메트릭 출력
# Prometheus 타겟 상태 확인
https://도메인/prometheus/targets
# → addiction-dev, addiction-prd 모두 UP 상태 확인
Grafana 대시보드(ID: 19004)에서 JVM 메모리, CPU, HTTP 요청 수, HikariCP 커넥션 현황 등을 실시간으로 확인할 수 있습니다.