본문 바로가기

인프라,데이터/Fluentd, Fluent Bit

Kubernetes - Fluent Bit 로 사용자 로그 S3에 output 설정하기

부제 : 온갖 예외 케이스를 가정하여 사용자 로그의 S3 경로 잡기

 

1. Fluent Bit에서 S3으로 output 보내기

Fluent Bit 에서 S3 output을 쓰는 건 쉽다. 

[OUTPUT]
    Name                         s3
    Match                        *
    bucket                       my-bucket
    region                       us-west-2
    s3_key_format                /$TAG[2]/$TAG[0]/%Y/%m/%d/$UUID.gz
    s3_key_format_tag_delimiters .-

이렇게 s3 output 설정을 넣어주고 butket과 key format을 지정해주면 된다.

나의 경우는 EKS 안의 애플리케이션에서 발생한 사용자 로그를 시스템 로그(var/log/containers/$podname~.log 식으로 저장되는 파드의 로그)와 분리하여 S3 경로를 잡아주려고 했는데,

그러려면 먼저 애플리케이션의 사용자 로그가 저장되는 폴더를 var/log/containers 하위의 특정 폴더에 잡아주어야 한다.

2. Kubernetes 애플리케이션의 Deployment 설정 - 폴더 마운트

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: demo
  name: springboot-logger
  namespace: test
spec:
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: test-image
        name: kubernetes-spring
        env:
        - name: pod_namespace
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: pod_name
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: testmount
          mountPath: /userlog
          subPath: userlog
        imagePullPolicy: Always
        ports:
        - containerPort: 80
      terminationGracePeriodSeconds: 30
      volumes:
        - name: testmount
          hostPath:
            path: /var/log/containers

spec.template.spec.containers.volumeMounts 의 mountpath를 애플리케이션의 사용자 로그가 저장될 폴더로 잡아준다.
name은 아래의 spec.template.spec.volumes 의 name과 통일한다.

spec.template.spec.volumes의 path를 /var/log/containers로 잡아주면 워커노드의 /var/log/containers/userlog-위에서 마운트한 경로- 아래에 사용자 로그들이 저장된다.

3. 스프링부트 애플리케이션의 로그가 특정 폴더에 저장되게 하기

<configuration>

    <property name="LOG_PATTERN" value="* %-30(%date [%thread]) %-5level %marker|%logger{36}: %msg%n" />
    <property name="LOG_BASEDIR" value="userlog"/>

    <appender name="appLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_BASEDIR}/test_app+${pod_namespace}+${pod_name}.log</file>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
   
</configuration>

logback에서 RollingFileAppender를 사용하면서, file이 저장되는 directory를 아까 마운트한 폴더인 userlog로 잡아주면 
사용자 로그들이 userlog 안에 저장된다.

<file>${LOG_BASEDIR}/test_app+${pod_namespace}+${pod_name.log</file>
${pod_namespace}과 ${pod_name}은 Kubernetes에서 스프링부트 애플리케이션을 만들 때 세팅해준 환경변수로, 로그가 쌓일 때는 환경변수가 대신 들어간다.
+를 넣은 이유는 Fluent Bit의 특성 때문인데, 아래에서 자세히 설명하겠다. 

4. Fluent Bit에서 로그 수집 시 TAG 변환하기

    [INPUT]
        Name              tail
        Tag               kube.*
        Path              /var/log/containers/*/test*.log
        Parser            docker
        DB                /var/log/flb_kube.db
        Mem_Buf_Limit     50MB
        Skip_Long_Lines   On
        Refresh_Interval  10

TAG는 Fluent Bit에서 Input 값을 kube.* 처럼 .*을 붙이면 파일의 경로~파일 이름 전체가 된다. ( / 대신에 .로 구분된다.)

위에서 쌓이는 로그의 TAG는 kube.var.log.containers.userlog.test_app+${pod_namespace}+${pod_name}.log 이다. 

    [FILTER]
        Name                  rewrite_tag
        Match                 kube.*
        Rule                  $key (.*) $TAG[5] false
        Emitter_Name          emitter

rewrite_tag filter를 사용하여 TAG를 새로 지정해줄 수 있는데, 기존 TAG는 .를 구분자로 인덱스화 할 수 있다.
.으로 TAG를 나누면 TAG[5]는 test_app+${pod_namespace}+${pod_name} 이 된다.

Rule       $sample_key (.*) $TAG[5] false 는
로그 안의 key값이 "sample_key" 인 항목의 값이 존재하는 로그에 한해, 
기존 TAG를 TAG[5]로 바꿔주고, 기존 TAG가 있던 로그는 삭제한다는 설정이다.

그러면 로그는 test_app+${pod_namespace}+${pod_name} 의 TAG로 쌓인다.

 

5. TAG를 파싱하여 S3 output 경로 잡기

    [OUTPUT]
        Name                         s3
        Match                        test*
        bucket                       test-bucket
        region                       ap-northeast-2
        s3_key_format                /fluent-bit/eks-$TAG[1]/$TAG[2]/%Y/%m/%d/$TAG[0]_$UUID.txt.gz
        s3_key_format_tag_delimiters +
        upload_timeout               30m

s3 output 설정에서 새 태그를 또 특정 구분자(delimiter) 로 나눌 수 있는데, +을 사용한 이유는
kubernetes의 podname은 podname-123456  식으로 중간에 - 가 들어가고, 
.이나 _ 같은 일반적인 구분자는 개발자들이 사용자 로그의 이름을 지정하면서 들어갈 수도 있는, 내가 통제할 수 있는 영역이 아니기 때문이다.
(만약의 상황에서, 개발자가 test_app 대신 test_nginx_app과 같이 _를 하나 더 써버리면 TAG의 인덱스가 달라지고, 그러면 s3에 저장되는 폴더의 규칙이 어그러진다.)

그래서 +를 사용하였는데, 사실 개발자들이 네이밍을 철저히 한다면 _를 사용해도 된다. 


하지만 여기서 복병이 하나 더 있었으니.....

6. 사용자 로그 파일 설정에서의 예외 상황 가정

개발자들이 test_app+${pod_namespace}+${pod_name}.log 형식을 쓰지 않고 test_app.log와 같이 남겨버릴 수도 있다.

이 경우는 그나마 무난한데, 그래도 test_app이 TAG 전체로 인식되어
/fluent-bit/eks-$TAG[1]/$TAG[2]/%Y/%m/%d/$TAG[0]_$UUID.txt.gz 와 같은 output 설정에서도 S3에 들어가긴 하기 때문이다. 
(TAG[1]과 TAG[2]를 뒤에 '[1]' 과 '[2]' 같은 string이 추가된 걸로 인식하여, 
/fluent-bit/eks-test_app[1]/test_app[2]/%Y/%m/%d/test_app[0]_$UUID.txt.gz 과 같은 형식으로 s3에 저장된다.)

하지만 test_app+${pod_name}.log 같이 ${pod_namespace}를 로그 이름 설정에서 빼먹었거나
test_app++${pod_name}.log 같이 ${pod_namespace}가 들어갈 자리가 빈 경우, (만일의 경우 이런 상황도 있을 수 있으니까...)

<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>

에러가 뜨면서 s3에 파일이 저장되지 않는다.

+로 인덱스를 구분해주는데, [1]번째 인덱스는 있어도 [2]번째 인덱스가 없기 때문이다.

이거 때문에 엄청 고생했다....

그리고 좋은 방법은 아닐지라도, 찾은 해결책은 
애플리케이션에서 로그 파일을 설정해줄 때 

<file>${LOG_BASEDIR}/test_app+ns_${pod_namespace}+pn_${pod_name}.log</file>

와 같이 앞에 ns_ , pn_ 과 같은 문자열을 넣어주는 것이다.

이러면 ${pod_namespace}가 애플리케이션에 따라 빈 값일 경우에도 
/fluent-bit/eks-ns_/ps_podname/%Y/%m/%d/test_app_$UUID.txt.gz 과 같은 형태로 로그가 쌓인다.


이상으로 며칠을 삽질한.... 예외상황을 가정한 Fluent Bit에서 S3으로 output path 잡기 글을 마친다.