logo

EFK Stack, 그 중에 Fluentd와 Fluent Bit

EFK Stack으로 Docker Swarm Logging 구축

devops


현재 사내에서는 컨테이너 오케스트레이션 툴로 Docker Swarm을 사용하고 있습니다. Kubernetes 전환 얘기가 솔솔 나오는 와중에 새로운 프로덕트를 내놓게 되었는데 이 과정에서 EFK를 사용하여 Logging을 구축한 경험, 특히 Fluent Bit과 Fluentd를 중심으로 기본 설정에 관해 이야기해볼까 합니다.

👀 ELK와 EFK Stack?

EFK란 Elasticsearch, Fluentd 그리고 Kibana의 첫 글자를 딴 약자입니다. 대표적으로 알려진 ELK는 Elasticsearch, Logstash 와 Kibana의 약자이며 두 Stack의 차이로는 로그 수집기를 Fluentd로 사용할 것인지 아니면 Logstash로 사용할 것인지 말고는 없습니다.

한 단계 더 가서 Logstash와 Fluentd의 차이점을 얘기해보자면 Logstash는 자바 런타임이 필요한 JRuby로 되어있으며 Fluentd는 자바 런타임이 필요하지 않은 CRuby로 되어있습니다. 그리고 Logstash에 비해 Fluentd는 조금 더 가벼우며 때에 따라 로그 전송만 담당하는, 더욱 경량화된 Fluent Bit을 사용 할 수 있습니다.

📐 Architecture

기본적인 구조는 아래와 같습니다.

EFK

각 클라이언트에서 내부 네트워크로 구성된 서버에 Fluent Bit으로 log를 수집 후 외부 네트워크가 가능한 서버로 전송합니다. 데이터를 받는 서버에서는 Fluentd를 Aggregator의 역할로 구성하고 다시 Elasticsearch로 데이터를 전송, Kibana는 Elasticsearch의 데이터를 시각화합니다.

🤲 Fluent Bit과 Fluentd

앞서 언급했듯이 Fluent Bit은 Fluentd의 경량화된 버전으로 C로 만들어졌습니다. 서버 환경이 워낙 좋아져서 크게 상관은 없지만 서버에서 Forwarder만 담당한다면 좀 더 가벼운 Fluent Bit을 사용하는 것이 좋습니다.

👉 Input → Parser → Filter → Output

Fluent Bit과 Fluentd의 파이프라인은 중간중간 추가되는 과정이 있어서 조금은 다르지만 크게는 이러한 과정으로 진행됩니다.

  1. Input: 서로 다른 소스에서 어떤 데이터를 수집할 지 결정
  2. Parser: 수집하는 데이터를 구조화하는 파싱
  3. Filter: 데이터를 특정 대상으로 전달하기 전에 데이터를 가공
  4. Output: 가공된 데이터를 내보내는 방식을 정의

각 과정에서 다양한 플러그인들이 제공되며 설정을 정의함으로써 원하는 값을 도출하게끔 제어할 수 있습니다.

🚢 Forwarder (Fluent Bit)

프로덕트는 보안상의 이유로 여러 클라이언트에게 on-premise 형태로 제공하고 있습니다. 내부 네트워크로 구성된 서버에는 Docker Swarm으로 오케스트레이션 되니 먼저 많은 Docker들의 Log를 추적해야 했습니다. (Ubuntu 기준으로 Docker Log 위치는 /var/lib/docker/containers/<container id>/<container id>-json.log)

각 클라이언트 서버에선 Forwarder로만 사용하기에 Docker Build로 Fluent Bit를 설정하여 서비스합니다.

# inputs.conf

[INPUT]
  Name              tail
  Parser            docker
  Path              /var/lib/docker/containers/*/*.log
  Docker_Mode       On
  Tag               docker.<file_name>
  Tag_Regex         (?<file_name>[a-z0-9]*)-json.log
	...

Logging은 Docker 외에도 여러 개 Input을 작성하다 보면 설정 파일이 길어지게 됩니다. 거기에다 각종 Filter 등 다른 과정까지 포함하다 보면 하나의 설정 파일에 감당하기 어렵습니다. 그래서 각 과정을 @INCLUDE 를 사용하여 여러 파일로 모듈화를 하면 좋습니다.

# fluent-bit.conf

@INCLUDE /fluent-bit/etc/inputs.conf

@INCLUDE /fluent-bit/etc/filters.conf

...

Docker Log를 이렇게 수집한 뒤 여러 Filter를 거쳐서 데이터를 원하는 형태로 가공해야 합니다. 그런고로 Filter야말로 핵심이라 할 수 있습니다.

무엇보다 Input 설정에서 Tag를 container id (file_name) 로 받기 때문에 각 Docker에서 나오는 Log를 인식할 수 있는 값으로 변환해야 하는 것이 중요합니다. 이를 위해 lua 스크립트를 사용해서 docker_metadata를 매칭한 뒤 태그를 변경하는 방법으로 필터링합니다.(참고 lua script)

# filters.conf

[FILTER]
  Name              lua
  Match             docker.*
  script            /fluent-bit/bin/docker-metadata.lua
  call              encrich_with_docker_metadata

[FILTER]
  Name              record_modifier
  Match             docker.*
  Whitelist_key     log
  Whitelist_key     docker.container_name
  ...

[FILTER]
  Name              modify
  Match             docker.*
  Rename            docker.container_name container_name
	...

[FILTER]
  Name              rewrite_tag
  Match             docker.*
  Rule              $container_name (.*) docker.$0 false

...

또한 grep을 써서 선택적으로 데이터를 취하거나 제외할 수 있습니다.

# filters.conf

[FILTER]
  Name              grep
  Match             docker.*
  Exclude           container_name (.*fluent).+
  ...

이 후 Output에 forward로 Aggregator에게 데이터를 보내면 됩니다.

🗃 Aggregator (Fluentd)

각 클라이언트에서 저마다 보내오는 데이터를 Fluentd가 수집해서 Elasticsearch로 전송합니다. 이 역할은 한 곳에서만 담당하며 Elasticsearch와 Kibana를 같은 서버에서 함께 사용해도 됩니다.

Elasticsearch로 데이터를 전송하기 위해 Fluentd의 fluent-plugin-elasticsearch라는 플러그인의 설치가 우선되어야 합니다.

# Dockerfile

FROM fluent/fluentd:v1.11-1

USER root

RUN apk add --no-cache --update --virtual .build-deps \
  sudo build-base ruby-dev \
  && sudo gem install \
    fluent-plugin-concat \
    fluent-plugin-elasticsearch \
  && apk del .build-deps \
  && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

COPY conf/* /fluentd/etc/
COPY entrypoint.sh /bin/

RUN chmod +x /bin/entrypoint.sh

USER fluent
# fluent.conf

<match docker.swarm_reception-api**>
  @type elasticsearch_dynamic
  logstash_format true
	logstash_prefix ${record['biz_client']}-reception-api
	...
</match>

...

다양한 클라이언트에서 데이터를 전송받으니 어떤 클라이언트인지 식별할 필요가 있습니다. Forwarder가 식별할 record를 하나 지정해서 보내주면 type을 elasticsearch_dynamic 로 선언해서 유동적으로 처리할 수 있습니다.

다중 match와 label등을 활용하여 데이터를 다시 분류 후 Elasticsearch로 전송하면 이를 바탕으로 드디어 Kibana에서 시각화가 가능해집니다.

📊 Elasticsearch와 Kibana

Elasticsearch와 Kibana는 직접 설치해서 사용해도 되지만 최근에는 여러 클라우드에서 쉽게 배포까지 자동으로 하는 환경을 만들어 줍니다. 더군다나 elastic.co에서는 사이트 내에서 클라우드로 자동 배포하고 매니지먼트까지 할 수 있는 기능들을 제공합니다.

배포 후 Kibana에 접속하여 Stack Management에서 Index Management를 확인해보면 index가 <client_name>-<docker_name>-<date>의 형식으로 생성되는 것을 확인할 수 있습니다.

이를 토대로 Kibana 인덱스 패턴을 만들면 EFK의 기본 과정은 끝이 납니다.

📃 끝으로

큰 Pipeline은 이렇게 구성되었습니다. 실제로는 더 다양하고 많은 Filter들과 설정들로 다소 복잡하게 구성됩니다. 그래도 제대로 가공된 데이터만 Elasticsearch로 전송할 수 있다면 Kibana에서 정말 편하게 다양한 정보를 확인할 수 있습니다.

그럼 여기까지 MSA, Docker Swarm Loggin을 위한 EFK 였습니다.