이번에는 ELK stack (Elastic Search, Logstash, Kibana) 와 kafka의 데이터 파이프 라인을 구축해본다. 일일이 서버에 접속해서 grep이나 tail로 로그를 보는 것은 비효율적이다. 이를 해결하기 위해 각 서버에서 실시간으로 로그를 수집하고 분석하고 시각화까지 가능한 ELK를 사용한다.

image

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

image

kibana

image

logstash

image

input은 logstash가 어디서 데이터를 가져올 것인지 정하는 부분이고, filter는 가져온 데이터가 정형화 되어있지 않다거나 불필요한 데이터가 있을 때 filter를 추가할 수 있는 부분, output은 정형화시킨 데이터를 어디로 내보낼 것인지 정하는 부분이다. 아래와 같이 변형해준다.

$ vim logstash/pipeline/logstash.conf 

image

이제 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 

image

image

이 로그들을 kafka broker로 전송하는 것이 목표이다. enabled를 true로 바꾸고, log의 path를 아래와 같이 바꿔준다. 그러면 filebeat가 해당 경로의 access로 시작하는 log 파일들을 팔로우 해서 input으로 처리한다. output은 kafka로 보내도록 바꿔준다.

image

image

이제 filebeat를 실행하면 된다.

image

AKHQ로 확인하면 메세지가 들어오는 것을 볼 수 있다.

image

elasticSearch 에서도 로그를 확인할 수 있다.

image