최근 기말 프로젝트 발표 시간에 유명 기업 백엔드 개발자로 근무하고 계시는 선배님께서 ELK를 사용한 MSA 아키텍처를 구축하신 걸 보고 인상깊었다. 현업과 학부생의 차이를 체감하면서 나도 저렇게 성장하고 싶다는 생각이 들었다. 단순히 서비스를 개발하는 것을 넘어서, 실제 운영 환경에서 발생하는 다양한 로그를 효과적으로 수집하고 분석하는 인프라를 구축하는 모습이 인상적이었다. 특히 MSA 환경에서는 여러 서비스에서 발생하는 로그를 통합적으로 관리하고 분석하는 것이 매우 중요하다는 점을 깨달았다. 로그 데이터를 통해 서비스의 성능을 모니터링하고 장애를 사전에 감지하며, 사용자 행동을 분석할 수 있다는 점이 매력적으로 다가왔다. 이에 나도 ELK 스택에 대해 학습하고 간단한 시스템을 구축해보고자 했다.
이 글에서는 FastAPI 기반의 웹 애플리케이션에 ELK 스택을 구축하는 전체 과정을 다뤄볼 예정이다. 학습 목적이므로 MacOS 환경에서 로컬로 진행하지만, 실제 프로덕션 환경에서 활용할 수 있는 기본적인 구조를 갖추는 것을 목표로 했다. Docker와 Docker Compose를 활용하여 컨테이너화된 환경을 구성함으로써 확장 가능하고 이식성 있는 시스템을 만들고자 했다.
ELK stack이란 무엇인가?
ELK 스택은 로그 데이터를 수집하고 분석하기 위한 오픈소스 도구들의 조합이다. Elasticsearch, Logstash, Kibana로 구성되어 있으며 각각의 도구는 다음과 같은 역할을 수행한다.
Elasticsearch는 Apache Lucene 기반의 분산형 검색 엔진이다. 로그 데이터를 저장하고 검색하는 역할을 담당하며, REST API를 통해 데이터 접근이 가능하다. 특히 대량의 데이터를 실시간으로 처리할 수 있는 능력이 있어 로그 분석에 적합하다. Logstash는 데이터 수집 및 변환 도구이다. 다양한 소스로부터 데이터를 입력받아 필터를 통해 가공한 후, 지정된 저장소로 전송하는 파이프라인을 구성할 수 있다. 로그 파일, TCP/UDP 소켓, Kafka 등 다양한 입력 소스를 지원한다. Kibana는 Elasticsearch에 저장된 데이터를 시각화하는 도구이다. 웹 인터페이스를 통해 데이터를 탐색하고 대시보드를 구성할 수 있으며, 실시간 모니터링도 가능하다.
애플리케이션 모니터링 시 ELK를 사용하여 분석한다면 아래와 같은 구체적인 모니터링 및 성능 분석을 진행할 수 있다.
- 성능 병목 현상이 특정 엔드포인트에서 발생함을 확인 > 처리 시간 및 응답 시간 로그를 기반으로 병목 구간 및 원인 식별 가능
- 전체 요청의 5%가 500 Internal Server Error로 처리 > 로그 데이터를 통해 예외 발생 위치 및 원인 분석 가능
- 요청의 70%가 동일한 IP에서 발생, 특정 사용자의 반복 요청 확인 > 악의적인 트래픽 여부 분석 가능
- 접속의 80%가 Chrome 브라우저에서 발생하였으며, 나머지는 모바일 Safari 브라우저에서 발생 > 브라우저 및 기기별 응답 성능 차이 분석 가능
위와 같은 분석을 통해 ELK 스택은 단순한 로그 저장소를 넘어 운영 모니터링 및 성능 분석 도구로 활용할 수 있다. 이는 실제 프로덕션 환경에서 시스템 운영 중 실시간으로 로그를 수집 및 분석하여 장애 발생 즉시 대응할 수 있게 되며, 장기간의 로그 데이터를 분석하여 사용량 변화 및 트래픽 패턴을 파악할 수 있다. 또한 브라우저 및 디바이스별 성능 차이를 분석하여 사용자 환경에 최적화된 서비스 제공이 가능해지며 데이터 기반 의사결정을 통해 성능 최적화 및 서버 확장 계획을 수립할 수 있는 등등.. 엄청나게 많은 장점이 있다.
로그 모니터링 시스템
이번 글에서 개발할 간단한 로그 모니터링 시스템의 구조는 다음과 같다. 전체 시스템은 Docker Host 위에서 동작하며 Docker Compose를 통해 여러 컨테이너가 하나의 네트워크로 연결되어 있다. 각 컨테이너의 역할과 데이터 흐름을 살펴보자.
먼저 외부의 Client Requests는 HTTP 프로토콜을 통해 FastAPI Container와 통신한다. FastAPI Container 내부에서는 FastAPI Application이 요청을 처리하며, Logging Middleware를 통해 모든 요청에 대한 로그를 생성한다. 이때 생성되는 로그에는 요청 시간, 클라이언트 IP, HTTP 메서드, 엔드포인트, 응답 상태 코드, 처리 시간 등의 정보가 포함된다. 생성된 로그는 ELK Stack Containers로 전달되는데, 세 개의 주요 컨테이너로 구성된다.
- Logstash Container는 데이터를 수집하고 처리하는 역할을 한다. FastAPI에서 생성된 로그 파일을 실시간으로 감시하며, 설정된 파이프라인에 따라 데이터를 가공한다. 이 과정에서 로그 메시지를 파싱하고 필요한 필드를 추출하거나 변환하는 작업이 이루어진다.
- Elasticsearch Container는 가공된 로그 데이터를 저장하고 검색할 수 있게 하는 데이터 스토리지다. Logstash로부터 받은 데이터를 인덱싱하여 저장하며, 효율적인 검색과 집계가 가능하도록 한다. 분산 아키텍처를 지원하여 대량의 로그 데이터도 안정적으로 처리할 수 있다.
- Kibana Container는 저장된 로그 데이터를 시각화하고 분석하는 웹 인터페이스를 제공한다. 사용자는 Kibana의 대시보드를 통해 로그 데이터를 실시간으로 모니터링하고 다양한 차트와 그래프를 통해 데이터를 분석할 수 있다.
MacOS 환경에서 ELK 프로젝트를 구축하기 위한 docker, docker compose, python 및 FastAPI 환경은 이미 준비되어 있다고 가정하고, 간단한 FastAPI 애플리케이션을 만들어 보았다. main.py에 아래 코드를 작성한다.
from fastapi import FastAPI, Request
import logging
import time
from datetime import datetime
import json
app = FastAPI()
logging.basicConfig(
filename="logs/app.log",
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
@app.get("/")
async def root(request: Request):
start_time = time.time()
response = {"message": "Hello, ELK!"}
process_time = time.time() - start_time
log_data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"client_ip": request.client.host,
"method": request.method,
"path": str(request.url.path),
"status_code": 200,
"process_time_ms": round(process_time * 1000, 2),
"user_agent": str(request.headers.get("user-agent")),
"request_info": {
"headers": dict(request.headers),
"query_params": dict(request.query_params)
}
}
json_log = json.dumps(log_data, ensure_ascii=False, separators=(',', ':'))
logging.info("Request processed: " + json_log)
return response
@app.get("/error")
async def trigger_error(request: Request):
start_time = time.time()
process_time = time.time() - start_time
log_data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"client_ip": request.client.host,
"method": request.method,
"path": str(request.url.path),
"status_code": 500,
"process_time_ms": round(process_time * 1000, 2),
"user_agent": str(request.headers.get("user-agent")),
"error_type": "Internal Server Error",
"request_info": {
"headers": dict(request.headers),
"query_params": dict(request.query_params)
}
}
json_log = json.dumps(log_data, ensure_ascii=False, separators=(',', ':'))
logging.error("Error occurred: " + json_log)
return {"message": "Error occurred"}, 500
이 코드는 ELK 스택과 연동하여 로그를 분석하기에 적합한 구조화된 로그 데이터를 생성한다. 로그 파일은 logs/app.log에 저장하며, 시간 - 로그레벨 - 메시지 형식으로 기록된다. 로그 데이터는 타임스탬프, 클라이언트 IP, HTTP 메서드, 요청 경로, 쿼리 파라미터, 요청 헤더, 상태 코드, 처리 시간, 사용자 에이전트 정보로 구성하였다. 에러 엔드포인트는 의도적으로 에러를 발생시키는 테스트용 엔드포인트로, 에러 상황에서의 로깅을 테스트하기 위한 용도로 작성하였다.
실제 애플리케이션을 실행시키면 다음과 같은 로그가 찍히는 것을 볼 수 있다.
이제 ELK stack을 사용하기 위해 구성 요소를 세팅하고 docker-compose.yml를 작성해 보자. 프로젝트 전체 폴더 구조는 다음과 같이 구성하면 된다.
elk-project/
├── app/
│ ├── main.py
│ └── logs/
│ └── app.log
├── logstash/
│ └── pipeline/
│ └── logstash.conf
└── docker-compose.yml
version: '3.8'
services:
fastapi:
build:
context: ./app
dockerfile: Dockerfile
ports:
- "8080:8000"
volumes:
- ./app/logs:/app/logs
networks:
- elk_network
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.11
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
networks:
- elk_network
logstash:
image: docker.elastic.co/logstash/logstash:7.17.11
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
- ./app/logs:/usr/share/logstash/logs
ports:
- "5044:5044"
environment:
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
depends_on:
- elasticsearch
networks:
- elk_network
kibana:
image: docker.elastic.co/kibana/kibana:7.17.11
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
networks:
- elk_network
networks:
elk_network:
driver: bridge
volumes:
elasticsearch_data:
FastAPI 애플리케이션에 대한 Dockerfile도 작성해야 한다.
FROM python:3.10
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Logstash 설정을 위해 logstash/pipeline/logstash.conf 파일을 작성한다.
input {
file {
path => "/usr/share/logstash/logs/app.log"
start_position => "beginning"
sincedb_path => "/dev/null"
}
}
filter {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:log_timestamp} - %{LOGLEVEL:log_level} - %{DATA:event_type}: %{GREEDYDATA:json_string}" }
}
json {
source => "json_string"
target => "log_data"
remove_field => ["message", "json_string"]
}
date {
match => [ "log_timestamp", "yyyy-MM-dd HH:mm:ss,SSS" ]
target => "@timestamp"
remove_field => ["log_timestamp"]
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "fastapi-logs-%{+YYYY.MM.dd}"
}
stdout { codec => rubydebug }
}
주요 설정을 살펴보면, 모든 서비스는 elk_network라는 Docker 네트워크로 연결되어 있으며 각 서비스 통신이 가능하도록 구성하였다. Elasticsearch 데이터는 영구 볼륨에 저장되며, 로그 파일과 Logstash 설정은 호스트와 공유된다.
docker-compose up -d 명령어를 실행하면 각각의 포트에서 서비스 접근이 가능하다.
- FastAPI: http://localhost:8000
- Elasticsearch: http://localhost:9200
- Kibana: http://localhost:5601
- Logstash: 5044 포트로 로그 수집
이렇게 구성된 환경에서 FastAPI 애플리케이션의 로그는 자동으로 수집되어 Elasticsearch에 저장되고, Kibana를 통해 시각화할 수 있다.
Elasticsearch 서버 실행 상태 확인
먼저 9200 포트로 접속하면 Elasticsearch 서버의 기본 정보를 보여주는 응답이 출력된다. 아래 화면은 현재 Elasticsearch의 상태와 버전 정보를 JSON 형태로 보여준다.
- luster_name: docker-cluster (현재 실행 중인 Elasticsearch 클러스터의 이름)
- cluster_uuid: 클러스터의 고유 식별자
- number: 7.17.11 (현재 실행 중인 Elasticsearch의 버전)
- build_date: 2023-06-23T05:33:12.261262042Z (빌드된 날짜)
- lucene_version: 8.11.1 (사용 중인 Lucene 엔진의 버전
이러한 응답값은 Elasticsearch 서버가 정상적으로 실행되고 있음을 확인할 수 있는 지표이다. 특히 cluster_name("docker-cluster")를 통해 Docker 환경에서 실행 중임을 알 수 있다.
curl http://localhost:8080/
curl http://localhost:8080/error
이제 FastAPI 애플리케이션으로 정상적인 요청/에러 요청을 반복해서 보낸 후, Logstash 로그를 확인해보면 아래와 같이 로그 데이터가 쌓이고 있는 것을 볼 수 있다.
Kibana 설정 과정
5601 포트로 접속하면 아래와 같이 Kibana 시작 화면이 표시된다. Add integration 클릭 후 Stack Management > Index Pattern 에 접속하여 새로운 인덱스 패턴을 만들어야 한다.
새로운 인덱스 패턴을 만들면 Kibana의 Index Pattern 설정 화면에서 Elasticsearch에 저장된 로그 데이터의 필드들을 볼 수 있다.
대시보드를 만들고 x값을 timestamp, y값을 count of records로 설정하면 아래와 같이 시간대별 로그 발생 추이를 시각화할 수 있다.
split row 값을 event_type, metrics값을 count로 설정하면 이벤트 타입 분석도 가능하다.
Kibana 대시보드를 통해 위에서 만든 FastAPI 애플리케이션 로그에 대한 분석 결과를 확인할 수 있었다. 다만 INFO/Error 두 가지의 간단한 테스트용 엔드포인트를 대상으로 로그를 수집하고 분석했기 때문에 실제 서비스 운영 환경에서 발생할 수 있는 다양한 상황을 충분히 반영하지는 못했다.
간단하게나마 ELK 스택을 활용한 로그 모니터링 시스템을 직접 만들어보면서 로그 수집, 분석, 시각화의 중요성을 깨닫게 되었다. 다음 포스팅에서는 조금 더 복잡한 애플리케이션을 개발하고 이에 대한 성능 모니터링과 로그 데이터 분석을 다시 진행해보려고 한다.
To be continued...