ELK 스택으로 모니터링 구축

로그 중앙집중화를 위해서 ELK를 설치하고 elastic으로 데이터를 볼 수 있는 절차를 정리했습니다.

내부 로그 수집(단일 노드) 예제이며, 운영 환경에서는 멀티노드/보안/TLS/계정 권한을 별도로 설계해야합니다.

1 설치 및 설정

  • 경로: /home1
  • docker-compose.yml
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch:8.12.2
    container_name: es
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - xpack.security.enrollment.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g # 2core/4G면 1g 권장
    volumes:
      - /opt/elk/esdata:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"

  kibana:
    image: docker.elastic.co/kibana:8.12.2
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
    ports:
      - "5601:5601"

  logstash:
    image: docker.elastic.co/logstash:8.12.2
    container_name: logstash
    depends_on:
      - elasticsearch
    volumes:
      - /opt/elk/logstash/pipeline:/usr/share/logstash/pipeline:ro
    ports:
      - "5514:5514/tcp"
      - "5514:5514/udp"
  • Logstash syslog 파이프라인: /opt/elk/logstash/pipeline/syslog.conf
input {
  tcp { port => 5514 ecs_compatibility => v8 }
  udp { port => 5514 ecs_compatibility => v8 }
}

filter {
  grok {
    match => {
      "message" =>
      "<%{POSINT:[log][syslog][priority]:int}>%{SYSLOGTIMESTAMP:syslog_timestamp} %{HOSTNAME:host_name} %{DATA:program}(?:\\[%{POSINT:pid:int}\\])?: %{GREEDYDATA:syslog_message}"
    }
    tag_on_failure => ["_grok_syslog_fail"]
  }

  date {
    match => ["syslog_timestamp", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss"]
    timezone => "Asia/Seoul"
    target => "@timestamp"
  }

  mutate {
    rename => { "host_name" => "[host][name]" }
    rename => { "program" => "[process][name]" }
    rename => { "syslog_message" => "message" }
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "syslog-%{+YYYY.MM.dd}"
  }
}

1.2 용량/보존(데이터가 너무 많이 쌓일 때)

ELK는 설정을 안 하면 인덱스가 계속 쌓여서 디스크가 꽉 찰 수 있음 → 보존 기간(예: 7/14/30일)과 삭제 정책을 정해두는 게 안전함.

현재 용량/인덱스 확인

# 인덱스별 크기/문서수
curl "http://localhost:9200/_cat/indices/syslog-*?v&s=index"

# 노드 디스크 사용량(단일노드라도 확인)
curl "http://localhost:9200/_cat/allocation?v"

# 호스트에서 ES 데이터 디렉토리 크기(대략)
du -sh /opt/elk/esdata

급할 때(수동 삭제)

# 예: 특정 일자 인덱스 삭제
curl -X DELETE "http://localhost:9200/syslog-YYYY.MM.DD"

권장(자동 삭제): ILM로 오래된 인덱스 자동 정리

현재처럼 syslog-YYYY.MM.dd(일별 인덱스)로 쌓는 구조면 “롤오버”까지는 필요 없고, Delete phase(보존기간 지난 인덱스 삭제) 만 걸어도 충분함.

  1. ILM 정책 생성(예: 30일 보관 후 삭제)
curl -X PUT "http://localhost:9200/_ilm/policy/syslog-delete-30d" \
  -H 'Content-Type: application/json' \
  -d '{
    "policy": {
      "phases": {
        "delete": {
          "min_age": "30d",
          "actions": { "delete": {} }
        }
      }
    }
  }'
  1. syslog-*에 정책 적용(인덱스 템플릿)
curl -X PUT "http://localhost:9200/_index_template/syslog-template" \
  -H 'Content-Type: application/json' \
  -d '{
    "index_patterns": ["syslog-*"],
    "template": {
      "settings": {
        "index.lifecycle.name": "syslog-delete-30d"
      }
    }
  }'
  1. 이미 생성된 기존 인덱스에도 적용(필요 시)
curl -X PUT "http://localhost:9200/syslog-*/_settings" \
  -H 'Content-Type: application/json' \
  -d '{ "index.lifecycle.name": "syslog-delete-30d" }'

디스크 꽉 차면(읽기 전용 걸리는 케이스)

Elasticsearch는 디스크가 임계치에 가까워지면 인덱스를 read_only_allow_delete로 잠글 수 있음.

  1. 먼저 오래된 인덱스 삭제 등으로 디스크 공간 확보
  2. 잠금 해제
curl -X PUT "http://localhost:9200/syslog-*/_settings" \
  -H 'Content-Type: application/json' \
  -d '{ "index.blocks.read_only_allow_delete": null }'

2. 데이터보는 방법

2.1 빠른 확인(권장)

  1. ELK 기동 확인
    • ES 확인: curl http://localhost:9200 (원격이면 `curl http://<ELK_HOST>:9200)
    • Kibana 접속: http://<KIBANA_HOST>:5601 (환경에 맞게)
  2. Syslog 전송(테스트 데이터 적재)
    • TCP: logger -n <ELK_HOST> -P 5514 -T -t cli-test "tcp check $(date)"
    • UDP: logger -n <ELK_HOST> -P 5514 -t cli-test "udp check $(date)"
  3. Elasticsearch에 인덱스/문서가 생겼는지 확인
    • 인덱스 목록: curl "http://localhost:9200/_cat/indices/syslog-*?v"
    • 샘플 조회: curl "http://localhost:9200/syslog-*/_search?size=1&sort=@timestamp:desc"

2.2 Kibana에서 보기(Discover)

  • 1~4 설정을 이미했다면 5번부터
  1. Kibana → Stack ManagementData ViewsCreate data view
  2. Name: 적당히 (예: syslog)
  3. Index pattern: syslog* (logstash output이 syslog-%{+YYYY.MM.dd} 이므로)
  4. Time field: @timestamp 선택 → Create data view
  5. Kibana → AnalyticsDiscover
    • Data view에서 방금 만든 syslog-* 선택
    • 우측 상단 Time range를 방금 보낸 시간으로 맞춤(예: Last 15 minutes)
    • 필드 추가해서 확인(예: host.name, host.ip, source.ip, process.name, message)

2.3 “데이터가 안 보여요” 체크리스트

  • Time range가 너무 짧거나 미래/과거로 잡혀있지 않은지 확인
  • Data view 인덱스 패턴이 syslog-*로 맞는지 확인
  • Logstash가 살아있는지/파이프라인 에러 없는지 확인(docker compose logs -f logstash)

이슈

1. elastic search 안켜짐

  • 로그
failed to obtain node locks, tried [/usr/share/elasticsearch/data]
AccessDeniedException: /usr/share/elasticsearch/data/node.lock
  • 원인: ES 데이터 디렉토리(/opt/elk/esdata) 권한 문제. - Elasticsearch 컨테이너는 UID 1000으로 실행
  • 해결
docker-compose down

sudo chown -R 1000:0 /opt/elk/esdata
sudo chmod -R 775 /opt/elk/esdata
# 개발용이면 초기화
sudo rm -rf /opt/elk/esdata/*

docker compose up -d

2. elastic search 메모리 부족 위험

  • 환경: 2core / 4GB RAM
  • 조치: ES_JAVA_OPTS=-Xms1g -Xmx1g. docker-compose.yml 에서 설정.
  • 이유: 시스템 메모리의 50%이상 넘지않고 정수로 할당해줌. 1.5G로도 해도되긴한데 터질 수도 있음.

3. Syslog를 TCP로 전송하면 host.ip가 elastic에서 안보임

  • 증상: udp로 보내면 host.ip가 보이는데, tcp로 보내면 host.ip가 비어있음.
  • 원인: 현재 설정(ecs_compatibility => v8)에서 udp input은 송신자 IP를 이벤트 필드([host][ip])로 넣어주지만, tcp input은 송신자 IP를 @metadata(예: [@metadata][input][tcp][source][ip])에만 넣음. @metadata는 기본적으로 Elasticsearch로 출력되지 않아서 host.ip가 안 보임.
  • 해결: filter에서 @metadata 값을 이벤트 필드로 복사(권장: source.ip, 필요하면 host.ip도 같이 세팅).

예시(syslog.conffilter에 추가):

filter {
  if [@metadata][input][tcp][source][ip] and ![source][ip] {
    mutate { add_field => { "[source][ip]" => "%{[@metadata][input][tcp][source][ip]}" } }
  }

  # 기존에 host.ip를 보고 있다면(UDP처럼) 같이 넣어줌
  if [@metadata][input][tcp][source][ip] and ![host][ip] {
    mutate { add_field => { "[host][ip]" => "%{[@metadata][input][tcp][source][ip]}" } }
  }

  # ... (기존 grok/date/mutate)
}

4. Logstash가 설정 읽다가 죽음 (non-ascii characters but are not UTF-8 encoded)

  • 증상: Could not fetch all the sources ... syslog.conf ... not UTF-8 encoded 로그가 뜨면서 Logstash가 종료됨.
  • 원인: /opt/elk/logstash/pipeline/syslog.conf 파일 인코딩이 UTF-8이 아님(윈도우에서 편집 후 CP949/UTF-16 등으로 저장해서 생기는 경우가 많음). Logstash는 파이프라인 설정 파일을 UTF-8로 읽어야 함.
  • 해결: syslog.confUTF-8(권장: BOM 없이) 로 변환 후 컨테이너 재시작. 및 conf 파일에 한글 주석 삭제

예시(호스트에서 변환):

# 인코딩 확인(가능하면)
file -bi /opt/elk/logstash/pipeline/syslog.conf

# (자주 겪는 케이스) UTF-16LE -> UTF-8
iconv -f utf-16le -t utf-8 /opt/elk/logstash/pipeline/syslog.conf > /tmp/syslog.conf \
  && mv /tmp/syslog.conf /opt/elk/logstash/pipeline/syslog.conf

# 컨테이너 재시작
docker compose restart logstash

용어 정리(ELK/Elastic)

  • Elastic Stack(ELK): Elasticsearch(저장/검색) + Logstash(수집/가공) + Kibana(시각화/조회) 묶음(요즘은 Beats/Agent까지 포함해서 “Elastic Stack”이라고도 함).
  • Elasticsearch(ES): 문서를 JSON 형태로 저장하고 검색/집계하는 엔진(REST API로 조회/관리).
  • Kibana: ES 데이터를 조회(Discover), 시각화(Dashboard), 관리(인덱스/템플릿/ILM 등)하는 UI.
  • Logstash: 입력(Input) → 필터(Filter) → 출력(Output) 파이프라인으로 로그/이벤트를 수집하고 가공해서 ES로 보냄.
  • Event(이벤트): Logstash/Beats가 처리하는 “한 건의 로그 레코드”(JSON 필드들의 묶음).
  • Document(문서): ES에 저장되는 한 건(대부분 이벤트 1개 = 문서 1개).
  • Field(필드): 문서 안의 키(예: @timestamp, host.name, message).
  • @timestamp: Elastic에서 시간 기반 조회/시각화에 쓰는 표준 시간 필드(Discover의 time field로 자주 선택).
  • Index(인덱스): 문서들이 저장되는 논리적 단위(예: syslog-2025.12.18). 보통 시간 단위(일/주)로 쪼갬.
  • Data view(Kibana): Kibana에서 여러 인덱스를 한 번에 보기 위한 “조회용 이름/패턴”(예: syslog-*).
  • Index pattern(인덱스 패턴): 여러 인덱스를 매칭하는 와일드카드 표현(예: syslog-*).
  • Mapping(매핑): 필드 타입 정의(예: keyword, text, ip, date). 잘못 잡히면 검색/집계가 불편해짐.
  • 적재: 인덱스에 문서를 저장하는 행위.
  • Reindex(재인덱싱): 기존 인덱스의 문서를 “새 인덱스”로 다시 써서(복사해서) 매핑/설정 변경이나 필드 정리를 적용하는 작업.
  • Alias swap(알리아스 컷오버): 재인덱싱 후 “조회/쓰기 대상” 알리아스를 새 인덱스로 바꿔 무중단에 가깝게 전환하는 방식.
  • Shard(샤드): 인덱스를 쪼갠 물리 단위. primary shard(원본) + replica shard(복제본)로 구성 가능.
  • Node(노드): ES가 실행되는 인스턴스(컨테이너/서버). 샤드가 노드에 배치됨.
  • Cluster(클러스터): 여러 노드가 모인 ES 집합(단일 노드도 클러스터로 동작).
  • Index template(인덱스 템플릿): 새 인덱스가 만들어질 때 적용되는 기본 설정/매핑/ILM 연결(패턴 기준).
  • Alias(알리아스): 인덱스를 가리키는 별칭(여러 인덱스를 묶거나, “쓰기용” 대상을 바꾸는 데 사용).
  • Data stream(데이터 스트림): 시간 기반 데이터에 권장되는 저장 방식(내부적으로 backing index들을 관리하며 롤오버/ILM와 잘 맞음).
  • ILM(Index Lifecycle Management): 인덱스 생명주기 정책(핫/웜/콜드/삭제 등). 여기서는 주로 “N일 뒤 삭제”에 사용.
  • Rollover(롤오버): 크기/문서수/기간 조건을 만족하면 “새 backing index”로 넘어가게 하는 동작(샤드가 너무 커지지 않게 관리).
  • Watermark(디스크 워터마크): 디스크 사용량 임계치. 높아지면 샤드 할당이 제한되거나 인덱스가 read_only_allow_delete로 잠길 수 있음.
  • read_only_allow_delete: 디스크 보호를 위해 ES가 인덱스를 읽기 전용으로 잠그는 설정(삭제는 허용).
  • ECS(Elastic Common Schema): host.*, source.* 같은 필드 네이밍 표준(서로 다른 로그도 같은 필드로 통일해서 검색/대시보드가 쉬움).
  • Grok: 로그 문자열을 정규식 패턴으로 파싱해서 필드를 뽑는 Logstash 필터.
  • @metadata: Logstash 내부용 임시 필드(기본적으로 ES로 출력되지 않음). TCP source IP가 여기에만 들어오는 케이스가 있어 필요 시 복사해야 함.