Alert

์ด ๊ธ€์€ Claude Code์˜ ๋„์›€์„ ๋ฐ›์•„ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

TL;DR

  • ELK Stack์€ Elasticsearch + Logstash + Kibana๋กœ ๊ตฌ์„ฑ๋œ ๋กœ๊ทธ ์ˆ˜์ง‘ยท๊ฒ€์ƒ‰ยท์‹œ๊ฐํ™” ์˜คํ”ˆ์†Œ์Šค ์Šคํƒ
  • Beats ์ถ”๊ฐ€ ํ›„ Elastic Stack์œผ๋กœ ๋ช…์นญ ํ™•์žฅ, Filebeat๊ฐ€ ๊ฒฝ๋Ÿ‰ ๋กœ๊ทธ ์ˆ˜์ง‘ ๋‹ด๋‹น
  • Elasticsearch๋Š” ์—ญ์ธ๋ฑ์Šค ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๊ฒ€์ƒ‰ ์—”์ง„, Logstash๋Š” ํŒŒ์ดํ”„๋ผ์ธ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜, Kibana๋Š” ์›น ๋Œ€์‹œ๋ณด๋“œ
  • ๋กœ๊ทธ ์ „๋ฌธ ๊ฒ€์ƒ‰๊ณผ ์˜๊ตฌ ๋ณด๊ด€์ด ์ค‘์š”ํ•œ ์ค‘๋Œ€๊ทœ๋ชจ ์šด์˜ ํ™˜๊ฒฝ์— ์ ํ•ฉ (์†Œ๊ทœ๋ชจ์—๋Š” ๋‹ค์†Œ ๊ณผํ•œ ํŽธ)

1. ELK Stack์ด๋ž€

์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๋ฉด ์„œ๋ฒ„, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜, ๋„คํŠธ์›Œํฌ ์žฅ๋น„ ๋“ฑ์—์„œ ๋กœ๊ทธ๊ฐ€ ์Ÿ์•„์ง„๋‹ค. ์„œ๋ฒ„๊ฐ€ ํ•œ๋‘ ๋Œ€์ผ ๋•Œ๋Š” tail -f๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ๋…ธ๋“œ๊ฐ€ ์ˆ˜์‹ญ ๋Œ€๋กœ ๋Š˜์–ด๋‚˜๋ฉด ๋กœ๊ทธ๋ฅผ ํ•œ๊ณณ์— ๋ชจ์•„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ์‹œ์Šคํ…œ์ด ํ•„์š”ํ•˜๋‹ค.

ELK Stack์€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค ์กฐํ•ฉ์ด๋‹ค.

๊ตฌ์„ฑ ์š”์†Œ์—ญํ• 
Elasticsearch๋กœ๊ทธ ์ €์žฅยท๊ฒ€์ƒ‰ยท๋ถ„์„ ์—”์ง„
Logstash๋‹ค์–‘ํ•œ ์†Œ์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ยท๋ณ€ํ™˜ยท์ „์†กํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ
KibanaElasticsearch ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋Š” ์›น ๋Œ€์‹œ๋ณด๋“œ

ํ˜„์žฌ๋Š” Beats(๊ฒฝ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ)๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด์„œ ๊ณต์‹ ๋ช…์นญ์ด Elastic Stack์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค.


2. ๋ฐ์ดํ„ฐ ํ๋ฆ„

์•ฑ/์„œ๋ฒ„ โ†’ Beats(์ˆ˜์ง‘) โ†’ Logstash(๋ณ€ํ™˜ยทํ•„ํ„ฐ๋ง) โ†’ Elasticsearch(์ธ๋ฑ์‹ฑยท์ €์žฅ) โ†’ Kibana(์‹œ๊ฐํ™”)

์†Œ๊ทœ๋ชจ ํ™˜๊ฒฝ์—์„œ๋Š” Logstash ์—†์ด Beats โ†’ Elasticsearch๋กœ ์ง์ ‘ ๋ณด๋‚ด๋Š” ๊ตฌ์„ฑ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.


3. ๊ฐ ๊ตฌ์„ฑ ์š”์†Œ ์ƒ์„ธ

3-1. Elasticsearch

Apache Lucene ๊ธฐ๋ฐ˜์˜ ๋ถ„์‚ฐ ๊ฒ€์ƒ‰ยท๋ถ„์„ ์—”์ง„์ด๋‹ค.

  • ์—ญ์ธ๋ฑ์Šค(Inverted Index) ๊ตฌ์กฐ๋กœ ์ „๋ฌธ ๊ฒ€์ƒ‰(Full-text Search)์ด ๋น ๋ฆ„
  • ๋ฐ์ดํ„ฐ๋ฅผ JSON ๋ฌธ์„œ ๋‹จ์œ„๋กœ ์ €์žฅ
  • ํด๋Ÿฌ์Šคํ„ฐ ๊ตฌ์„ฑ์œผ๋กœ ์ˆ˜ํ‰ ํ™•์žฅ ๊ฐ€๋Šฅ (๋…ธ๋“œ ์ถ”๊ฐ€๋งŒ์œผ๋กœ ์šฉ๋Ÿ‰ยท์„ฑ๋Šฅ ํ™•์žฅ)
  • REST API๋กœ ๋ชจ๋“  ์ž‘์—… ์ˆ˜ํ–‰
# ์ธ๋ฑ์Šค์— ๋ฌธ์„œ ์ถ”๊ฐ€
curl -X POST "localhost:9200/logs/_doc" -H 'Content-Type: application/json' -d '{
  "timestamp": "2026-06-09T12:00:00",
  "level": "ERROR",
  "message": "Connection refused to database",
  "service": "api-server"
}'
 
# ๊ฒ€์ƒ‰
curl -X GET "localhost:9200/logs/_search?q=level:ERROR"

ํ•ต์‹ฌ ๊ฐœ๋…

  • Index: RDB์˜ ํ…Œ์ด๋ธ”์— ๋Œ€์‘. ๊ฐ™์€ ๊ตฌ์กฐ์˜ ๋ฌธ์„œ ๋ชจ์Œ
  • Document: RDB์˜ ํ–‰(row)์— ๋Œ€์‘. ํ•˜๋‚˜์˜ JSON ๊ฐ์ฒด
  • Shard: ์ธ๋ฑ์Šค๋ฅผ ๋ถ„ํ• ํ•œ ๋‹จ์œ„. ์—ฌ๋Ÿฌ ๋…ธ๋“œ์— ๋ถ„์‚ฐ ์ €์žฅ
  • Replica: ์ƒค๋“œ์˜ ๋ณต์ œ๋ณธ. ๊ฐ€์šฉ์„ฑ๊ณผ ์ฝ๊ธฐ ์„ฑ๋Šฅ ํ–ฅ์ƒ

3-2. Logstash

๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘(Input) โ†’ ๋ณ€ํ™˜(Filter) โ†’ ์ „์†ก(Output) ํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ ๋„๊ตฌ๋‹ค. JVM ์œ„์—์„œ ๋™์ž‘ํ•œ๋‹ค.

# logstash.conf ์˜ˆ์‹œ
input {
  beats {
    port => 5044
  }
}
 
filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
  }
  date {
    match => [ "timestamp", "ISO8601" ]
  }
}
 
output {
  elasticsearch {
    hosts => ["http://localhost:9200"]
    index => "app-logs-%{+YYYY.MM.dd}"
  }
}
  • Input: Beats, ํŒŒ์ผ, Kafka, Redis, syslog ๋“ฑ ๋‹ค์–‘ํ•œ ์†Œ์Šค ์ง€์›
  • Filter: grok(์ •๊ทœ์‹ ํŒŒ์‹ฑ), mutate(ํ•„๋“œ ๋ณ€ํ™˜), geoip(IP โ†’ ์œ„์น˜ ๋ณ€ํ™˜) ๋“ฑ
  • Output: Elasticsearch, S3, Kafka ๋“ฑ์œผ๋กœ ์ „์†ก

Logstash vs Beats

Logstash๋Š” ๋ณต์žกํ•œ ๋ณ€ํ™˜ยทํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. ๋‹จ์ˆœ ์ˆ˜์ง‘๋งŒ ํ•„์š”ํ•˜๋ฉด Beats๊ฐ€ ๋” ๊ฐ€๋ณ๊ณ  ํšจ์œจ์ ์ด๋‹ค. ๋‘˜์„ ์กฐํ•ฉํ•ด์„œ Beats๋กœ ์ˆ˜์ง‘ โ†’ Logstash๋กœ ๋ณ€ํ™˜ โ†’ Elasticsearch๋กœ ์ „์†กํ•˜๋Š” ํŒจํ„ด์ด ์ผ๋ฐ˜์ ์ด๋‹ค.

3-3. Kibana

Elasticsearch์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋Š” ์›น ์ธํ„ฐํŽ˜์ด์Šค๋‹ค.

  • Discover: ๋กœ๊ทธ ์›๋ณธ ๊ฒ€์ƒ‰ยทํƒ์ƒ‰
  • Dashboard: ์ฐจํŠธยทํ…Œ์ด๋ธ”ยท์ง€๋„ ๋“ฑ์„ ์กฐํ•ฉํ•œ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ
  • Lens: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์‹œ๊ฐํ™” ์ƒ์„ฑ
  • Alerting: ์กฐ๊ฑด ๊ธฐ๋ฐ˜ ์•Œ๋ฆผ ์„ค์ • (Slack, ์ด๋ฉ”์ผ ๋“ฑ)
  • KQL(Kibana Query Language): ์ง๊ด€์ ์ธ ์ฟผ๋ฆฌ ๋ฌธ๋ฒ•
# KQL ์˜ˆ์‹œ
level: "ERROR" and service: "api-server" and message: "timeout"

3-4. Beats

Go๋กœ ์ž‘์„ฑ๋œ ๊ฒฝ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ ์‹œ๋ฆฌ์ฆˆ๋‹ค. ์šฉ๋„๋ณ„๋กœ ์ข…๋ฅ˜๊ฐ€ ๋‚˜๋‰œ๋‹ค.

Beat์ˆ˜์ง‘ ๋Œ€์ƒ
Filebeat๋กœ๊ทธ ํŒŒ์ผ
Metricbeat์‹œ์Šคํ…œยท์„œ๋น„์Šค ๋ฉ”ํŠธ๋ฆญ (CPU, ๋ฉ”๋ชจ๋ฆฌ ๋“ฑ)
Packetbeat๋„คํŠธ์›Œํฌ ํŒจํ‚ท
Heartbeat์„œ๋น„์Šค ๊ฐ€์šฉ์„ฑ (uptime ๋ชจ๋‹ˆํ„ฐ๋ง)
Auditbeat์‹œ์Šคํ…œ ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ

๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ๊ฒƒ์€ Filebeat์ด๋‹ค. Logstash๋ณด๋‹ค ๋ฆฌ์†Œ์Šค ์†Œ๋น„๊ฐ€ ํ›จ์”ฌ ์ ์–ด ๊ฐ ์„œ๋ฒ„์— ์—์ด์ „ํŠธ๋กœ ์„ค์น˜ํ•˜๊ธฐ ์ ํ•ฉํ•˜๋‹ค.


4. Docker Compose๋กœ ์‹œ์ž‘ํ•˜๊ธฐ

๋กœ์ปฌ์—์„œ ELK Stack์„ ๋น ๋ฅด๊ฒŒ ๋„์šฐ๋Š” ์ตœ์†Œ ๊ตฌ์„ฑ์ด๋‹ค.

# docker-compose.yml
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
 
  logstash:
    image: docker.elastic.co/logstash/logstash:8.17.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    depends_on:
      - elasticsearch
 
  kibana:
    image: docker.elastic.co/kibana/kibana:8.17.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
 
volumes:
  es-data:
docker compose up -d
 
# Elasticsearch ๋™์ž‘ ํ™•์ธ
curl localhost:9200
 
# Kibana ์ ‘์†
# http://localhost:5601

๋ฆฌ์†Œ์Šค ์ฐธ๊ณ 

Elasticsearch๋Š” JVM ๊ธฐ๋ฐ˜์ด๋ผ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค. ์ตœ์†Œ 2GB ์ด์ƒ์˜ ํž™ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ถŒ์žฅํ•˜๋ฉฐ, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋…ธ๋“œ๋‹น 8~16GB๊ฐ€ ์ผ๋ฐ˜์ ์ด๋‹ค.


5. ์‹ค๋ฌด์—์„œ ์ž์ฃผ ์“ฐ๋Š” ๊ตฌ์„ฑ ํŒจํ„ด

ํŒจํ„ด 1: ๊ธฐ๋ณธ (์†Œ๊ทœ๋ชจ)

Filebeat โ†’ Elasticsearch โ†’ Kibana

๋กœ๊ทธ ๋ณ€ํ™˜์ด ํ•„์š” ์—†์„ ๋•Œ. Filebeat์ด ์ง์ ‘ Elasticsearch๋กœ ์ „์†กํ•œ๋‹ค.

ํŒจํ„ด 2: ํ‘œ์ค€ (์ค‘๊ทœ๋ชจ)

Filebeat โ†’ Logstash โ†’ Elasticsearch โ†’ Kibana

๋กœ๊ทธ ํŒŒ์‹ฑยทํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ•  ๋•Œ. ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๊ตฌ์„ฑ์ด๋‹ค.

ํŒจํ„ด 3: ๋ฒ„ํผ ํฌํ•จ (๋Œ€๊ทœ๋ชจ)

Filebeat โ†’ Kafka โ†’ Logstash โ†’ Elasticsearch โ†’ Kibana

๋กœ๊ทธ ์œ ์‹ค ๋ฐฉ์ง€์™€ ๋ฐฑํ”„๋ ˆ์…” ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ๋•Œ. Kafka๊ฐ€ ๋ฒ„ํผ ์—ญํ• ์„ ํ•œ๋‹ค.


6. ELK vs ๋Œ€์•ˆ ๋น„๊ต

ํ•ญ๋ชฉELK StackGrafana LokiDatadog
ํƒ€์ž…์˜คํ”ˆ์†Œ์Šค (self-hosted)์˜คํ”ˆ์†Œ์Šค (self-hosted)SaaS
์ „๋ฌธ ๊ฒ€์ƒ‰์—ญ์ธ๋ฑ์Šค ๊ธฐ๋ฐ˜, ๋งค์šฐ ๋น ๋ฆ„๋ผ๋ฒจ ๊ธฐ๋ฐ˜, ์ œํ•œ์ ์ „๋ฌธ ๊ฒ€์ƒ‰ ์ง€์›
๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋†’์Œ (JVM ๊ธฐ๋ฐ˜)๋‚ฎ์Œ (๋กœ๊ทธ ๋ณธ๋ฌธ ์ธ๋ฑ์‹ฑ ์•ˆ ํ•จ)๊ด€๋ฆฌ ๋ถˆํ•„์š”
์šด์˜ ๋‚œ์ด๋„๋†’์Œ (ํด๋Ÿฌ์Šคํ„ฐ ๊ด€๋ฆฌ ํ•„์š”)์ค‘๊ฐ„๋‚ฎ์Œ
๋น„์šฉ์ธํ”„๋ผ ๋น„์šฉ๋งŒ์ธํ”„๋ผ ๋น„์šฉ๋งŒ์‚ฌ์šฉ๋Ÿ‰ ๊ธฐ๋ฐ˜ ๊ณผ๊ธˆ
์ ํ•ฉํ•œ ํ™˜๊ฒฝ๋กœ๊ทธ ์ „๋ฌธ ๊ฒ€์ƒ‰์ด ์ค‘์š”ํ•œ ์ค‘๋Œ€๊ทœ๋ชจ๋ผ๋ฒจ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง์ด๋ฉด ์ถฉ๋ถ„ํ•œ ํ™˜๊ฒฝ์šด์˜ ๋ถ€๋‹ด์„ ์ค„์ด๊ณ  ์‹ถ์„ ๋•Œ

์„ ํƒ ๊ธฐ์ค€

  • ๋กœ๊ทธ ๋ณธ๋ฌธ์„ ์ž์œ ๋กญ๊ฒŒ ๊ฒ€์ƒ‰ํ•ด์•ผ ํ•œ๋‹ค๋ฉด โ†’ ELK
  • Prometheus/Grafana๋ฅผ ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ด๊ณ  ๋กœ๊ทธ๋ฅผ ๊ฐ€๋ณ๊ฒŒ ๋ถ™์ด๊ณ  ์‹ถ๋‹ค๋ฉด โ†’ Loki
  • Docker ์ปจํ…Œ์ด๋„ˆ ๋กœ๊ทธ๋ฅผ ์ €์žฅยท๊ฒ€์ƒ‰ ์—†์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธ๋งŒ ํ•˜๋ฉด โ†’ Dozzle

๋‹จ, Dozzle์€ ๋กœ๊ทธ๋ฅผ ์ €์žฅยท์ธ๋ฑ์‹ฑํ•˜์ง€ ์•Š๋Š” Docker ์ „์šฉ ์‹ค์‹œ๊ฐ„ ๋ทฐ์–ด๋ผ, ELKยทLoki์™€ ๊ฐ™์€ ๊ธ‰์˜ ๋กœ๊ทธ ํ”Œ๋žซํผ ๋Œ€์•ˆ์€ ์•„๋‹ˆ๋‹ค.


7. ์šด์˜ ์‹œ ์ฃผ์˜์‚ฌํ•ญ

  • ์ธ๋ฑ์Šค ์ˆ˜๋ช… ๊ด€๋ฆฌ(ILM): ์˜ค๋ž˜๋œ ์ธ๋ฑ์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ•์†Œยท์‚ญ์ œํ•˜๋Š” ์ •์ฑ…์„ ์„ค์ •ํ•ด์•ผ ๋””์Šคํฌ ํญ์ฆ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์ƒค๋“œ ์ˆ˜ ์„ค๊ณ„: ์ƒค๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ํด๋Ÿฌ์Šคํ„ฐ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ฆ๊ฐ€ํ•œ๋‹ค. ์ƒค๋“œ๋‹น 10~50GB๊ฐ€ ์ ์ • ๋ฒ”์œ„
  • ๋ณด์•ˆ ์„ค์ •: 8.x๋ถ€ํ„ฐ ๊ธฐ๋ณธ์ ์œผ๋กœ TLS์™€ ์ธ์ฆ์ด ํ™œ์„ฑํ™”๋œ๋‹ค. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” xpack.security.enabled=false๋กœ ๋Œ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ํ™œ์„ฑํ™”
  • ํž™ ๋ฉ”๋ชจ๋ฆฌ: ์ „์ฒด ๋ฉ”๋ชจ๋ฆฌ์˜ 50% ์ดํ•˜๋กœ ์„ค์ •ํ•˜๊ณ , 32GB๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค (JVM compressed oops ํ•œ๊ณ„)
  • ๋งคํ•‘ ํญ๋ฐœ ๋ฐฉ์ง€: ๋™์  ๋งคํ•‘์œผ๋กœ ํ•„๋“œ๊ฐ€ ๋ฌดํ•œํžˆ ๋Š˜์–ด๋‚˜์ง€ ์•Š๋„๋ก index.mapping.total_fields.limit ์„ค์ •