Kubernetes

EFK 구축 후기 - 1편

wath1457 2024. 3. 11. 00:29

최근에 회사에서 쿠버네티스에 배포된 애플리케이션 로그 확인을 위한 EFK 구축을 진행했었다.

 

이번 포스팅은 해당 과정에서 겪었던 문제와... 고민들? 위주로 작성할 것이다.

 

나에게 주어진 문제

단순히 EFK를 배포하는 것에서 F에 해당하는 Fluentd와 Fluent-bit를 비교해보고 결정해보라는 미션(?)이 주어졌다.

 

일단은 부하테스트를 진행해보고 해당 결과를 통해 인사이트를 도출해보라는 힌트가 있었다!

 

그렇다면 우선, fluent-bit와 fluentd 모두 배포를 진행해야 했었다.

 

 

EFK 배포?

 

기본적으로 GitOps 방식을 채택하고, ArgoCD를 이용하여 앱을 배포하고 있는 구조에서 외부의 helm chart를 이용하여 앱 배포를 진행하였다.

 

사실 helm chart를 이용한 방법은 정말 간단하게 리소스들을 한 번에 설치할 수 있는 방법이라, 관련된 설정을 할 수 있는 values.yaml을 잘 작성하면 됐었다.

 

kibana 배포를 진행하는 과정에서, secret 관련 오류가 발생했었는데, elasticsearch 관련 secret이 존재하지 않아서 발생한 오류였었다.

 

  • elasticuser-credential : username & passwrod 정보
  • elastic-certificates : TLS 인증서와 키에 대한 정보

해당 secret들을 생성 후에 해결할 수 있었다..

 

 

Elasticsearch 라이선스와 관련된 문제

사실 오픈소스를 사용하다보면 라이선스를 잘 살펴보아야 한다.

 

정말 회사에서 상업적인 목적으로 사용할 수 있는지, 내가 사용하려는 목적이 해당 라이선스에 위배되지 않는지, 굉장히 중요하다.

 

windows 환경에서 docker desktop과 anaconda... 등 개인과 회사의 입장에서 달라지는 즉, 사용하지 못하는 라이선스가 많다..

 

ElasticSearch의 경우에는 버전 7.10 이후로 Apache2.0에서 SSPL & Elastic License가 적용된다.

 

SSPL 

  • SSPL 라이선스가 부여된 소프트웨어를 사용하여 만든 어떤 서비스나 제품은 그 소스코드를 공개해야만 한다.
  • SSPL 라이선스가 부여된 소프트웨어를 사용하여 만든 서비스나 제품을 무료로 제공해야 한다.

Elastic License

  • 기존 오픈소스 라이선스가 가지는 소스 코드 수정, 사용, 배포 등에 대해서는 거의 모든 자유를 허용
  • 호스팅 등 서비스로 제 3자에게 제공을 금지한다.
  • 라이선스 키 등으로 보호되는 S/W 기능에 대한 변경을 금지한다.

 

그래서 사실은, 지금 당장은 아니더라도, Elasticsearch의 버전을 내려서 설치를 하려고 했었는데... 이 과정에서 문제가 발생했다.

 

 

 

쿠버네티스 버전은 중요하다

현재 Kubernetes 버전이 1.27인 상황이고,

 

Elasticsearch의 버전이 7.10x 버전인 경우에는  kubernetes 1.25 버전부터 사라진 PodDisruptionBudget의 API  policy/v1beta1 을 사용하기에, 배포가 안되는 문제가 있었다.

 

https://kubernetes.io/docs/reference/using-api/deprecation-guide/

 

Deprecated API Migration Guide

As the Kubernetes API evolves, APIs are periodically reorganized or upgraded. When APIs evolve, the old API is deprecated and eventually removed. This page contains information you need to know when migrating from deprecated API versions to newer and more

kubernetes.io

 

그러므로 apiVersion을 policy/v1을 사용해야 한다.

 

문제는 외부 helm repo URL을 사용하기 있었기 때문에, 내가 apiVersion을 변경할 수가 없었다. values.yaml에도 해당 옵션을 제공하진 않았었다.

 

이 부분에 대해서는 helm chart를 새로 생성하거나, 다른 방법을 적용하는 것이 필요해 보인다.

 

 

 

파싱을 어떻게 해야할까?

사실 이 부분에서 굉장히 많은 시간을 소모했다.

 

다양한 애플리케이션에서 발생되는 로그를 일괄적으로 처리하기에는 다소 무리가 있었다.

 

가장 큰 이유는 로그의 포맷이 각각 다르기 때문에, 애플리케이션마다 따로 파싱을 해주어야 효과적으로 로그를 중앙 저장소인 elasticsearch에 저장할 수 있음을 느꼈다.

 

elasticsearch에 잘 적재를 해놓아야, kibana에서 시각화나 분석을 할 때 효과적으로 할 수 있다.

 

 

 

파싱과 관련된 오류 - Elasticsearch dynamic mapping

각 노드의 데몬셋으로 생성된 fluent-bit(fluentd) pod가 /var/log/containers/*.log 위치에 로그를 저장하고, 해당 로그들을 elasticsearch로 보내는 구조였는데, elasticsearch에 저장되는 과정에서 문제가 발생겼다.

 

elasticsearch는 기본적으로 dynamic mapping을 지원하기 때문에, 새로운 구조로 들어오는 데이터에서 자동적으로 index mapping을 진행한다.

 

 

나같은 경우에는 kubernetes_metadata라는 플러그인을 이용하여 로그마다 쿠버네티스 관련 메타데이터를 추가하였는데, 이 부분에서 에러가 발생하였다.

mapper_parsing_exception [reason]: 'object mapping for [kubernetes.labels.app] tried to parse field [app] as object, but found a concrete value'

 

 

왜 이런일이 발생했을까?

일단 elasticsearch 버전이 7이 되면서, 동일한 index 내에서 동일 필드는 같은 형식의 데이터이어야 한다.

 

하지만, index mapping 과정 중에서 kubernetes.label.app 필드는 객체필드인데, concrete value로 새롭게 데이터가 들어왔다는 뜻이다.

 

실제 index_mapping과 로그를 봤을때, kubernetes.labels.app.~ 이런 필드와 kubernetes.labels.app 필드가 동시에 존재하였으며, kubernetes.labels.app 필드의 경우에는 text 형식이었다.

 

이런 문제는 elasticsearch의 경우에는 '.'을 중첩 구조로 인식을 하기 때문에 발생하는 것이다.

 

 

 

해결 방법은?

많은 고민을 하였고, 찾아본 결과 다음과 같이 정리할 수 있었다.

  • 필드의 '.'을 다른 문자로 변환한다.
    • fluentd
      • ruby 스크립트로 de_dot 기능을 하는 코드를 작성한다.
      • de_dot 외부 플러그인을 이용한다.
    • fluent-bit
      • lua script로 de_dot 기능을 하는 코드를 작성한다.
      • Filter-Nest 플러그인 사용 : 기존의 nested(중첩) 구조를 모두 들어낸다(lift) / 다만, depth가 긴 경우 많은 오버헤드를 발생시킨다.
      • elasticsearch OUTPUT 플러그인을 사용중인 경우 replace_dots 옵션을 ON으로 설정한다. (추천)
        • 이 방법은 fluent-bit에서 수집한 데이터를 바로 elasticsearch로 보내는 경우 사용할 수 있는 좋은 방법이다.
        • 다만, fluent-bit를 수집기로 사용하여 다른 곳으로 forward하는 경우, 사용할 수 없으므로 다른 방법이 필요하다.
      • grep Filter 플러그인을 사용하여 특정 field의 key, value 값을 만족하는 로그(document)를 제거한다.
        • ex) kubernets.labels.app 필드의 값이 * 패턴인 로그는 수집x
        • 이 방법은 많은 로그 데이터를 놓치게 된다.
    • 공통
      • 문제가 발생하는 필드 kubernetes.labels.app의 필드와 그 하위필드를 제거한다.
      • kubernetes_metadata 플러그인을 적용하지 않는다.

 

 

나의 경우는 최종적으로 fluent-bit(수집) & fluentd(집계) 구조를 선택하였기 때문에, lua script를 이용하여 fluent-bit에서 파싱하였다. / 구조에 대해서는 2편에서 내용을 작성하겠다.

 

 

중첩(nested) 구조 다루기

사실, dyanamic index mapping 문제를 겪으면서 중첩 구조를 어떻게 다루는지 안적을 수 없었다.

 

결국에는 이 문제를 해결하기 위해서는 중첩구조인 필드에 접근해야 하는데, fluentd와 fluent-bit는 이 부분에 대해서 다른점이 몇가지 있었다.

 

다음과 같은 json 형식의 로그가 있다고 가정하자.

{
  "log": "some message",
  "stream": "stdout",
  "labels": {
     "color": "blue", 
     "unset": null,
     "project": {
         "env": "production"
      }
  }
}

 

만약 labels.project.env에 접근하려면 어떻게 해야할까?

 

fluentd

$.labels.project.env

 

fluent-bit

$labels['project']['env']

 

 

이렇게 중첩 구조에 접근해서 문제를 해결할 수 있다.

 

그러나, fluent-bit의 경우에는 이러한 record-accessor가 모든 Filter 플러그인에 적용이 되지 않는다.

 

fluent-bit 2.2버전 기준, grep에서 record-accessor가 동작하고, 다른 플러그인에서 동작하지 않는 이슈를 확인하였다.

 

반면, fluentd는 다른 내부 플러그인에서 record-accessor 적용이 되는 것을 확인할 수 있었다.

 

 

 

생각보다 글이 길어질 것 같아서, 2편에서 남은 내용을 작성하겠다.

 

2편은 fluent 스택을 비교하기 위해 부하테스트를 진행했던 경험과 그 외 로그를 elasticsearch에 적재하면서 발생했었던 문제들과 본격적인 fluent-bit와 fluentd를 비교하는 다양한 관점에 대해서 추가적으로 내용을 적을 것 같다.