728x90

아래 구성은 3개 노드 Wazuh Indexer(OpenSearch) 클러스터를 대상으로,
- Prometheus Exporter로 노드 성능 지표 수집 → Prometheus/Grafana 시각화,
- ElastAlert2로 OpenSearch 쿼리 기반 탐지 → Slack 알림,
- 오류 해결(매핑/호환모드/TLS),
- Dev Tools(콘솔)에서 바로 실행할 스니펫
까지 한 번에 정리한 운영 표준 레시피입니다.
인프라 구성 개요(아키텍처)
- OpenSearch(Wazuh Indexer) ×3
- 각 노드에 opensearch-prometheus-exporter 플러그인 설치
/_prometheus/metrics에서 노드 지표 노출
- Prometheus
- 각 노드
/_prometheus/metrics를 스크레이프 - (선택) Alertmanager 연동
- 각 노드
- Grafana
- Prometheus를 데이터소스로 등록
- 대시보드/Rule 알림 구성
- ElastAlert2
- OpenSearch에 쿼리 → 조건 매칭 시 Slack 알림
- 내부 writeback 인덱스(
elastalert_status등) 사용
- 보안
- TLS + 전용 모니터/읽기 계정 + 내부망 스크레이프 권장
Indexer 이미지 커스터마이징(Exporter 포함)
1) Dockerfile (예시)
# Dockerfile.wazuh-indexer-exporter
FROM wazuh/wazuh-indexer:4.14.1
ENV OPENSEARCH_HOME=/usr/share/opensearch
RUN ${OPENSEARCH_HOME}/bin/opensearch-plugin install --batch \
https://github.com/opensearch-project/opensearch-prometheus-exporter/releases/download/3.3.2.0/prometheus-exporter-3.3.2.0.zip
# ↑ 실제 Indexer 내장 OpenSearch 버전에 호환되는 Exporter 릴리스를 선택하세요.
※ 버전 호환이 가장 중요합니다. 설치 실패 시 Exporter 버전을 상/하 조정하세요.
2) opensearch.yml(Prometheus 노출 옵션)
prometheus.metric_name.prefix: "opensearch_"
prometheus.nodes.filter: "_all" # 기본은 _local
# (권장) ES7 호환 응답 끄기 — ElastAlert2 매핑 오류 예방
compatibility:
override_main_response_version: false
docker-compose.yml에서 해당 파일을 각 노드에 마운트하세요.
docker-compose
version: "3.8"
services:
wazuh1.indexer:
build: { context: ., dockerfile: Dockerfile.wazuh-indexer-exporter }
image: local/wazuh-indexer-exporter:4.14.1
hostname: wazuh1.indexer
environment:
- cluster.name=wazuh
- node.name=wazuh1.indexer
- plugins.security.disabled=true # 테스트용. 운영은 TLS/보안 활성화 권장
ports:
- "9200:9200"
volumes:
- ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
wazuh2.indexer:
image: local/wazuh-indexer-exporter:4.14.1
hostname: wazuh2.indexer
environment:
- cluster.name=wazuh
- node.name=wazuh2.indexer
- plugins.security.disabled=true
ports: [ "9201:9200" ]
volumes:
- ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
wazuh3.indexer:
image: local/wazuh-indexer-exporter:4.14.1
hostname: wazuh3.indexer
environment:
- cluster.name=wazuh
- node.name=wazuh3.indexer
- plugins.security.disabled=true
ports: [ "9202:9200" ]
volumes:
- ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
prometheus:
image: prom/prometheus:v2.55.0
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=15d
ports: [ "9090:9090" ]
depends_on: [ wazuh1.indexer, wazuh2.indexer, wazuh3.indexer ]
grafana:
image: grafana/grafana:11.2.0
container_name: grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123!
ports: [ "3000:3000" ]
volumes:
- grafana-data:/var/lib/grafana
depends_on: [ prometheus ]
volumes:
prometheus-data:
grafana-data:
Prometheus 스크레이프 설정
./prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: opensearch-indexer
metrics_path: /_prometheus/metrics
scheme: http # 운영은 https 권장
static_configs:
- targets:
- wazuh1.indexer:9200
- wazuh2.indexer:9200
- wazuh3.indexer:9200
# 보안 활성 시:
# basic_auth:
# username: monitor
# password: ${OPENSEARCH_MONITOR_PASSWORD}
# tls_config:
# ca_file: /etc/prometheus/certs/ca.crt
확인
docker compose up -d
curl http://localhost:9200/_prometheus/metrics | head
# Prometheus UI: http://localhost:9090 → Status > Targets → UP 확인
Grafana 대시보드(빠른 시작)
- 데이터소스 등록: URL
http://prometheus:9090 - 샘플 쿼리
- CPU:
avg by (instance) (opensearch_os_cpu_percent) - Heap%:
avg by (instance) (opensearch_jvm_mem_heap_used_percent) - Rejected(write):
rate(opensearch_thread_pool_write_rejected_total[5m])
- CPU:
- 알림 예시(Grafana Rule)
- Heap% > 75% 5분 지속 → Slack/Webhook
ElastAlert2 — 설치/전역 설정/Slack/룰
1) Docker Compose
version: "3.8"
services:
elastalert:
image: jertel/elastalert2:latest
container_name: elastalert2
volumes:
- ./elastalert/config.yaml:/opt/elastalert/config.yaml:ro
- ./elastalert/rules:/opt/elastalert/rules:ro
- ./certs/ca.crt:/opt/elastalert/certs/ca.crt:ro
environment:
- TZ=Asia/Seoul
depends_on:
- wazuh1.indexer
restart: unless-stopped
2) config.yaml(전역)
es_url: "https://wazuh1.indexer:9200"
use_opensearch: true
use_ssl: true
verify_certs: true
ca_certs: /opt/elastalert/certs/ca.crt
es_username: "monitor"
es_password: "********"
writeback_index: "elastalert_status"
run_every: { minutes: 1 }
buffer_time: { minutes: 15 }
logging: { level: INFO }
중요: OpenSearch의 opensearch.yml에서 compatibility.override_main_response_version: false 권장.
(ES7 호환 응답이 켜져 있으면 include_type_name 에러 유발 가능)
3) Slack Webhook
- Slack 앱 → Incoming Webhooks 활성화 → URL 발급
- 룰에
slack_webhook_url기입(보안상 Secret/환경변수 권장)
4) 룰 예시(3종)
① Wazuh 고심각 경보 버스트(5분 10건)./elastalert/rules/wazuh_severity_high.yaml
name: Wazuh High-Severity Burst
type: frequency
index: wazuh-alerts-4.*
num_events: 10
timeframe: { minutes: 5 }
filter:
- query:
query_string:
query: 'rule.level:>=10'
query_key: agent.name
realert: { minutes: 10 }
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_emoji_override: ":rotating_light:"
slack_msg_color: "danger"
alert_text_type: alert_text_only
alert_text: |
🚨 *고심각 경보 버스트*
• 에이전트: {0}
• 5분 내 건수: {1}
• 예: {2}
alert_text_args: [agent.name, num_matches, rule.description]
② Nginx 5xx 스파이크(10분 3배 증가)./elastalert/rules/nginx_5xx_spike.yaml
name: Nginx 5xx Spike
type: spike
index: logs-nginx-*
timeframe: { minutes: 10 }
spike_height: 3
spike_type: up
filter: [ { term: { response_code: "500" } } ]
query_key: host.name
realert: { minutes: 15 }
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_msg_color: "warning"
alert_text_type: alert_text_only
alert_text: |
📈 *5xx 급증 감지*
• 호스트: {0}
• 10분 기준 과거 대비 3배 ↑
alert_text_args: [host.name]
③ 에이전트 침묵(flatline, 10분 무이벤트)./elastalert/rules/agent_flatline.yaml
name: Agent Flatline (No Events)
type: flatline
index: wazuh-alerts-4.*
threshold: 1
timeframe: { minutes: 10 }
filter: [ { term: { agent.name: "web-01" } } ]
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_msg_color: "danger"
alert_text_type: alert_text_only
alert_text: |
💤 *에이전트 이벤트 끊김*
• 대상: web-01
• 지난 10분 동안 이벤트 없음
5) writeback 인덱스 생성(Dev Tools 권장)
include_type_name 오류를 피하려면 Dev Tools에서 수동 생성이 가장 확실합니다.
(템플릿 1개 + 본체 인덱스 or 5개 전체 인덱스 수동 생성)
방법 A) 템플릿 + 본체(권장)
# (기존 잘못된 인덱스가 있으면 정리)
DELETE elastalert_status_status
DELETE elastalert_status_error
DELETE elastalert_status_past
DELETE elastalert_status_silence
# 필요 시: DELETE elastalert_status
# 템플릿
PUT _index_template/elastalert_status_template
{
"index_patterns": ["elastalert_status*"],
"template": {
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"alert_time": { "type": "date" },
"alert_sent": { "type": "boolean" },
"aggregate_id": { "type": "keyword" },
"rule_name": { "type": "keyword" },
"match_body": { "type": "object", "enabled": false },
"alert": { "type": "object", "enabled": false }
}
}
}
}
# 본체 인덱스 생성
PUT elastalert_status
방법 B) 5개 인덱스 모두 수동 생성
(필수 핵심 필드 타입만 정확히 지정)
PUT elastalert_status
{
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"alert_time": { "type": "date" },
"alert_sent": { "type": "boolean" },
"aggregate_id": { "type": "keyword" },
"rule_name": { "type": "keyword" },
"match_body": { "type": "object", "enabled": false },
"alert": { "type": "object", "enabled": false }
}
}
}
PUT elastalert_status_status
{
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"timestamp": { "type": "date" },
"rule_name": { "type": "keyword" },
"status": { "type": "keyword" },
"error": { "type": "text" }
}
}
}
PUT elastalert_status_error
{
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"@timestamp": { "type": "date" },
"rule_name": { "type": "keyword" },
"alert_time": { "type": "date" },
"message": { "type": "text" }
}
}
}
PUT elastalert_status_past
{
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"alert_time": { "type": "date" },
"rule_name": { "type": "keyword" },
"aggregate_id": { "type": "keyword" },
"match_body": { "type": "object", "enabled": false }
}
}
}
PUT elastalert_status_silence
{
"settings": { "number_of_shards": 1, "number_of_replicas": 1 },
"mappings": {
"dynamic": true,
"properties": {
"rule_name": { "type": "keyword" },
"until": { "type": "date" }
}
}
}
6) 인덱스/매핑 점검(Dev Tools)
GET elastalert_status/_mapping
# 핵심: alert_time 정렬 가능해야 함
GET elastalert_status/_search?size=1&sort=alert_time:asc
{
"query": { "match_all": {} }
}
7) 룰 테스트/실행
# 테스트
docker run --rm \
-v $(pwd)/elastalert/config.yaml:/opt/elastalert/config.yaml:ro \
-v $(pwd)/elastalert/rules:/opt/elastalert/rules:ro \
jertel/elastalert2:latest \
elastalert-test-rule --config /opt/elastalert/config.yaml /opt/elastalert/rules/wazuh_severity_high.yaml
# 본 실행
docker compose -f docker-compose.elastalert.yml up -d
docker logs -f elastalert2
운영 가이드
- TLS 필수 + CA 검증
- ElastAlert2
verify_certs: true,ca_certs마운트 - Prometheus는 내부망에서만 스크레이프(필요시 mTLS/BasicAuth)
- ElastAlert2
- 전용 계정(최소 권한)
- OpenSearch: 모니터/읽기 전용 Role
- Slack Webhook: Secret/환경변수 관리
- ES7 호환 응답 끄기(중요)
compatibility.override_main_response_version: falseinclude_type_name매핑 오류 방지
- 소음 억제
- ElastAlert 룰:
realert,query_key사용 - Grafana/Prometheus 알림: 지연/지속시간 조건 설정
- ElastAlert 룰:
- 보관/성능
elastalert_status*에 ISM/ILM 적용(롤오버+삭제)- Prometheus 보관기간/스크레이프 주기 조정(예: 15s/15d)
트러블슈팅(핵심 에러별)
- InsecureRequestWarning
→ TLS 검증 미설정.verify_certs: true,ca_certs설정/마운트. include_type_name400
→ OpenSearch 호환 응답 ON 또는 ElastAlert2 구버전.override_main_response_version: falsejertel/elastalert2:latest갱신- Dev Tools로 수동 인덱스 생성
No mapping found for [alert_time]
→elastalert_status매핑에alert_time: date없음.
→ Dev Tools로 템플릿/인덱스를 위 스니펫대로 재생성.- Prometheus Target DOWN
→ 네트워크/포트/DNS 점검. 컨테이너 내에서curl http://wazuh1.indexer:9200/_prometheus/metrics. - Slack 미수신
→ 잘못된 Webhook, 채널 권한, 방화벽 확인. ElastAlert 로그에서alert_sent여부 확인.
설치·검증 체크리스트
- 각 Indexer에 Exporter 플러그인 정상 설치
-
/_prometheus/metrics로 메트릭 수집 확인 - Prometheus Target UP
- Grafana에서 지표 시각화/알림 구성
- OpenSearch
compatibility.override_main_response_version: false - Dev Tools로 writeback 인덱스(템플릿/매핑) 정상화
- ElastAlert2 룰 테스트 통과 → Slack 수신 확인
- TLS/계정/ISM 정책 등 보안·보관 기준 충족
Dev Tools 빠른 진단 스니펫 모음
# 메트릭 확인(Exporter)
GET _prometheus/metrics
# 인덱스/매핑 확인
GET elastalert_status/_mapping
# alert_time 정렬 테스트
GET elastalert_status/_search?size=1&sort=alert_time:asc
{
"query": { "match_all": {} }
}
# 템플릿 조회
GET _index_template/elastalert_status_template
# (옵션) ISM 정책 확인
GET _plugins/_ism/policies/elastalert-status-retention728x90
그리드형(광고전용)
댓글