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 | ๋ค์ํ ์์ค์์ ๋ฐ์ดํฐ๋ฅผ ์์งยท๋ณํยท์ ์กํ๋ ํ์ดํ๋ผ์ธ |
| Kibana | Elasticsearch ๋ฐ์ดํฐ๋ฅผ ์๊ฐํํ๋ ์น ๋์๋ณด๋ |
ํ์ฌ๋ 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 Stack | Grafana Loki | Datadog |
|---|---|---|---|
| ํ์ | ์คํ์์ค (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์ค์