이번에는 ELK stack (Elastic Search, Logstash, Kibana) 와 kafka의 데이터 파이프 라인을 구축해본다. 일일이 서버에 접속해서 grep이나 tail로 로그를 보는 것은 비효율적이다. 이를 해결하기 위해 각 서버에서 실시간으로 로그를 수집하고 분석하고 시각화까지 가능한 ELK를 사용한다.
FileBeats는 경량화된 로그 전송 모듈이다. web application의 access.log를 취합해서 kafka에 전송하는 역할을 한다. 예전에는 filebeats의 역할도 logstash가 했지만 로그 전송과 처리를 분리해 FileBeats에서는 로그 수집과 전송만을 하도록 한다. LogStash는 여러 데이터 소스로부터 다양한 로그를 수집해서 정비화하거나 ElasticSearch에 특정 포맷으로 전송한거나 모니터링을 할 수 있도록 적정한 포맷으로 로그를 변형해준다. ElasticSearch는 Apache lossing 기반으로 만들어진 검색 엔진으로 로그 검색과 분석을 담당한다. Kibana는 ElasticSearch에 저장된 로그를 기반으로 그래프와 차트같은 시각화를 가능하게 해주는 서비스이다.
kafka는 ELK stack에 문제가 생기는 것을 대비하기 위해 사용한다. 만약에 kafka 없이 filebeats가 직접 logstash 로 로그를 전송하게 구성했을 때, web application에서 대량의 로그가 생겼다고 하면 filebeats가 전송해야할 로그는 급증하게 되는데 logstash가 급증한 로그를 처리하지 못하면 시스템이 불안정해지면서 로그가 유실될수도 있다. 여기서 kafka가 메세지 큐 역할을 하면서 logstash가 자기가 처리 가능할 만큼의 로그만 가져와서 인덱싱을 할 수 있다.
1. log generator 생성
우리는 apache 어플리케이션을 실제로 동작시키지 않고 log를 생성해줄수 있는 fake apache log generator를 가지고 로그를 생성할 것이다.
version: '3'
services:
apache-app:
image: centos:centos8
hostname: apache-app-server1
network_mode: host
command:
- bash
- -c
- >
yum -y install git vim;
yum -y install python2 python2-pip;
curl -s https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.15.1-linux-x86_64.tar.gz -o filebeat.tar.gz && tar xvfz filebeat.tar.gz -C /;
git clone https://github.com/kiritbasu/Fake-Apache-Log-Generator log_generator && cd log_generator && pip2 install -r requirements.txt;
python2 apache-fake-log-gen.py -n 0 -o LOG
2. ELK 구축
ELK docker-compose로 구성한다. https://github.com/deviantony/docker-elk
$ git clone https://github.com/deviantony/docker-elk.git elk
$ cp ../resources/docker-compose-confluent-kafka-and-elk.yml . # 우리가 만든 yml 파일 사용
# Reference: https://github.com/deviantony/docker-elk.git
version: '3.2'
services:
elasticsearch:
build:
context: elasticsearch/
args:
ELK_VERSION: $ELK_VERSION
volumes:
- type: bind
source: ./elasticsearch/config/elasticsearch.yml
target: /usr/share/elasticsearch/config/elasticsearch.yml
read_only: true
- type: volume
source: elasticsearch
target: /usr/share/elasticsearch/data
ports:
- "9200:9200"
- "9300:9300"
environment:
ES_JAVA_OPTS: "-Xmx256m -Xms256m"
ELASTIC_PASSWORD: changeme
ELASTIC_USERNAME: elastic
# Use single node discovery in order to disable production mode and avoid bootstrap checks.
# see: https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html
discovery.type: single-node
logstash:
build:
context: logstash/
args:
ELK_VERSION: $ELK_VERSION
volumes:
- type: bind
source: ./logstash/config/logstash.yml
target: /usr/share/logstash/config/logstash.yml
read_only: true
- type: bind
source: ./logstash/pipeline
target: /usr/share/logstash/pipeline
read_only: true
ports:
- "5044:5044"
- "5000:5000/tcp"
- "5000:5000/udp"
- "9600:9600"
environment:
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
depends_on:
- elasticsearch
- kafka-1
- kafka-2
- kafka-3
kibana:
build:
context: kibana/
args:
ELK_VERSION: $ELK_VERSION
volumes:
- type: bind
source: ./kibana/config/kibana.yml
target: /usr/share/kibana/config/kibana.yml
read_only: true
ports:
- "5601:5601"
depends_on:
- elasticsearch
zookeeper-1:
hostname: zookeeper1
image: confluentinc/cp-zookeeper:6.2.0
environment:
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_CLIENT_PORT: 12181
ZOOKEEPER_DATA_DIR: /zookeeper/data
ZOOKEEPER_SERVERS: zookeeper1:22888:23888;zookeeper2:32888:33888;zookeeper3:42888:43888
ports:
- 12181:12181
- 22888:22888
- 23888:23888
volumes:
- ./zookeeper/data/1:/zookeeper/data
zookeeper-2:
hostname: zookeeper2
image: confluentinc/cp-zookeeper:6.2.0
environment:
ZOOKEEPER_SERVER_ID: 2
ZOOKEEPER_CLIENT_PORT: 22181
ZOOKEEPER_DATA_DIR: /zookeeper/data
ZOOKEEPER_SERVERS: zookeeper1:22888:23888;zookeeper2:32888:33888;zookeeper3:42888:43888
ports:
- 22181:22181
- 32888:32888
- 33888:33888
volumes:
- ./zookeeper/data/2:/zookeeper/data
zookeeper-3:
hostname: zookeeper3
image: confluentinc/cp-zookeeper:6.2.0
environment:
ZOOKEEPER_SERVER_ID: 3
ZOOKEEPER_CLIENT_PORT: 32181
ZOOKEEPER_DATA_DIR: /zookeeper/data
ZOOKEEPER_SERVERS: zookeeper1:22888:23888;zookeeper2:32888:33888;zookeeper3:42888:43888
ports:
- 32181:32181
- 42888:42888
- 43888:43888
volumes:
- ./zookeeper/data/3:/zookeeper/data
kafka-1:
image: confluentinc/cp-kafka:6.2.0
hostname: kafka1
depends_on:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper1:12181,zookeeper2:22181,zookeeper3:32181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:19092
KAFKA_LOG_DIRS: /kafka
ports:
- 19092:19092
volumes:
- ./kafka/logs/1:/kafka
kafka-2:
image: confluentinc/cp-kafka:6.2.0
hostname: kafka2
depends_on:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 2
KAFKA_ZOOKEEPER_CONNECT: zookeeper1:12181,zookeeper2:22181,zookeeper3:32181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka2:29092
KAFKA_LOG_DIRS: /kafka
ports:
- 29092:29092
volumes:
- ./kafka/logs/2:/kafka
kafka-3:
image: confluentinc/cp-kafka:6.2.0
hostname: kafka3
depends_on:
- zookeeper-1
- zookeeper-2
- zookeeper-3
environment:
KAFKA_BROKER_ID: 3
KAFKA_ZOOKEEPER_CONNECT: zookeeper1:12181,zookeeper2:22181,zookeeper3:32181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka3:39092
KAFKA_LOG_DIRS: /kafka
ports:
- 39092:39092
volumes:
- ./kafka/logs/3:/kafka
akhq:
image: tchiotludo/akhq:latest
hostname: akhq
depends_on:
- kafka-1
- kafka-2
- kafka-3
environment:
AKHQ_CONFIGURATION: |
akhq:
connections:
kafka:
properties:
bootstrap.servers: kafka1:19092,kafka2:29092,kafka3:39092
ports:
- 8080:8080
volumes:
elasticsearch:
yml 파일로 도커 컨테이너를 띄우기 전에 몇가지 config를 바꿔준다.
$ vim elasticsearch/config/elasticsearch.yml # elastic xpack이라는 유료 버전 제거
$ vim kibana/config/kibana.yml # xpack이라는 유료 버전 제거
$ vim logstash/config/logstash.yml # xpack이라는 유료 버전 제거
elasticsearch
kibana
logstash
input은 logstash가 어디서 데이터를 가져올 것인지 정하는 부분이고, filter는 가져온 데이터가 정형화 되어있지 않다거나 불필요한 데이터가 있을 때 filter를 추가할 수 있는 부분, output은 정형화시킨 데이터를 어디로 내보낼 것인지 정하는 부분이다. 아래와 같이 변형해준다.
$ vim logstash/pipeline/logstash.conf
이제 docker compose로 컨테이터를 띄워보자.
$ docker compose -f docker-compose-confluent-kafka-and-elk.yml up
다음으로 apache-app 컨테이너로 접속해서 filebeat의 config를 수정해준다.
$ docker ps
$ docker exec -it <container ID> /bin/bash
이 로그들을 kafka broker로 전송하는 것이 목표이다. enabled를 true로 바꾸고, log의 path를 아래와 같이 바꿔준다. 그러면 filebeat가 해당 경로의 access로 시작하는 log 파일들을 팔로우 해서 input으로 처리한다. output은 kafka로 보내도록 바꿔준다.
이제 filebeat를 실행하면 된다.
AKHQ로 확인하면 메세지가 들어오는 것을 볼 수 있다.
elasticSearch 에서도 로그를 확인할 수 있다.