Kubernetes - Kubernetes 로깅 운영(logging), Fluentd

2021. 4. 25. 02:23 컨테이너/Kubernetes

오늘 다루어볼 내용은 쿠버네티스 환경에서의 로깅운영 방법이다. 지금까지는 쿠버네티스에 어떻게 팟을 띄우는지에 대해 집중했다면 오늘 포스팅 내용은 운영단계의 내용이 될 것 같다. 사실 어떻게 보면 가장 중요한 내용중에 하나라고 볼 수 있는 것이 로깅이다. 물리머신에 웹을 띄울 때는 파일로 로그를 날짜별로 남기고, 누적 일수이상된 파일은 제거 혹은 다른 곳으로 파일을 옮기는 등의 작업을 했을 것이다. 하지만 쿠버네티스에서는 파일로 로그를 남기지 않으며 조금 다른 방법으로 로깅운영을 진행한다. 

 

컨테이너 환경에서 로그를 운영하는 구체적인 방법을 설명하기 전에 컨테이너 환경에서 로그가 어떻게 생성되는지 알아본다. 비컨테이너 환경의 애플리케이션에서는 보통 로그를 파일로 많이 남기고 한다. 이에 비해 도커에서는 로그를 파일이 아닌 표준 출력으로 출력하고 이를 다시 Fluentd 같은 로그 컬렉터로 수집하는 경우가 많다. 이런 방법은 애플리케이션 쪽에서 로그 로테이션이 필요없으며 로그 전송을 돕는 로깅 드라이버 기능도 갖추고 있으므로 로그 수집이 편리하다.

 

간단하게 필자가 만든 이미지로 실습을 진행한다.

> docker pull 1223yys/springboot-web:0.2.5
 
> docker container run -it --rm -p 8080:8080 1223yys/springboot-web:0.2.5
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.1.RELEASE)
 
2020-02-23 11:29:15.368  INFO 1 --- [           main] com.kebe.sample.SampleApplication        : Starting SampleApplication on d1e5a6d24e34 with PID 1 (/app/app.jar started by root in /)
2020-02-23 11:29:15.375  INFO 1 --- [           main] com.kebe.sample.SampleApplication        : No active profile set, falling back to default profiles: default
2020-02-23 11:29:17.099  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-02-23 11:29:17.122  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-02-23 11:29:17.124  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.27]
2020-02-23 11:29:17.244  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-02-23 11:29:17.245  INFO 1 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1784 ms
2020-02-23 11:29:17.619  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-02-23 11:29:17.885  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-02-23 11:29:17.890  INFO 1 --- [           main] com.kebe.sample.SampleApplication        : Started SampleApplication in 3.221 seconds (JVM running for 3.936)

 

 

yoonyeoseong/kubernetes-sample

Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

우선 이미지를 내려받고 애플리케이션을 포어그라운드로 실행시킨다. 그 다음 로그가 호스트에서 어떻게 출력되는지 확인해보자.

 

그전에 아래의 명령으로 실행 중인 컨테이너들의 로그가 어떻게 찍히는 지 확인할 수 있다.

> docker run -it --rm -v /var/lib/docker/containers:/json-log alpine ash

 

명령 실행 후에 /json-log 디렉토리에 위에서 실행한 컨테이너의 아이디 디렉토리로 들어가면 현재 우리가 표준출력으로 찍고 있는 로그가 json 타입으로 찍히고 있으며 이 표준출력이 파일로 남고 있는 것을 볼 수 있다.

 

 

다시 말해, 애플리케이션에서 로그를 파일로 출력하지 않았더라도 도커에서 컨테이너의 표준 출력을 로그로 출력해주는 것이다. 따라서 로그 출력 자체를 완전히 도커에 맡길 수 있다.

 

도커 로깅 드라이버

도커 컨테이너의 로그가 JSON 포맷으로 출력되는 이유는 도커에 json-file이라는 기본 로깅 드라이버가 있기 때문이다. 로깅 드라이버는 도커 컨테이너가 출력하는 로그를 어떻게 다룰지를 제어하는 역할을 한다. json-file 외에도 다음과 같은 로깅 드라이버가 존재한다.

 

logging driver description
Syslog syslog로 로그를 관리
Journald systemd로 로그를 관리
Awslogs AWS CloudWatch Logs로 로그를 전송
Gcplogs Google Cloud Logging으로 로그를 전송
Fluentd fluentd로 로그를 관리

 

도커 로그는 fluentd를 사용해 수집하는 것이 정석이다. 

 

컨테이너 로그의 로테이션

애플리케이션에서 표준 출력으로 출력하기만 해도 로그를 파일에 출력할 수 있지만, 웹 애플리케이션처럼 컨테이너 업타임이 길거나 액세스 수에 비례해 로그 출력량이 늘어나는 경우에는 JSON 로그 파일 크기가 점점 커진다. 컨테이너를 오랜 시간 운영하려면 이 로그를 적절히 로테이션할 필요가 있다.

 

도커 컨테이너에는 로깅 동작을 제어하는 옵션인 --log-opt가 있어서 이 옵션으로 도커 컨테이너의 로그 로테이션을 설정할 수 있다. max-size는 로테이션이 발생하는 로그 파일 최대 크기이며 l/m/g 단위로 파일 크기 지정이 가능하다. max-file은 최대 파일 개수를 의미하며 파일 개수가 이 값을 초과할 경우 오래된 파일부터 삭제된다.

docker container run -it --rm -p 8080:8080 --log-opt max-size=1m --log-opt max-file=5 1223yys/springboot-web:0.2.5

 

이 설정을 매번 컨테이너 실행마다 할 필요는 없고, 도커 데몬에서 log-opt를 기본값으로 설정할 수 있다. Preference 화면에서 Deamon > Advanced 항목에서 다음과 같이 JSON 포맷으로 설정할 수 있다.

{
  "experimental" : false,
  "debug" : true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "1m",
    "max-file": "5"
  }
}

 

쿠버네티스에서 로그 관리하기

다른 예제는 뛰어넘고, 바로 쿠버네티스 환경에서 로그관리하는 방법을 알아본다. 우선 가장 대중적인 쿠버네티스 로그 운영은 아래와 같은 플로우로 많이 진행하는 것 같다.

 

app ---> fluentd ---> elasticsearch ---> kibana

 

app에서는 표준 출력으로 로그를 출력하고 fluentd는 로그를 긁어서 엘라스틱서치에 색인한다. 그리고 키바나를 통해 색인된 로그들을 모니터링한다. 쿠버네티스의 로그 관리에서도 역시 컨테이너는 표준 출력으로만 로그를 내보내면 되며, 그에 대한 처리는 컨테이너 외부에서 이루어진다.

 

쿠버네티스는 다수의 도커 호스트를 노드로 운영하는데, 어떤 노드에 어떤 파드가 배치될지는 쿠버네티스 스케줄러가 결정한다. 그러므로 각 컨테이너가 독자적으로 로그를 관리하면 비용이 많이 든다. 먼저 로컬 쿠버네티스 환경에 Elasticsearch와 Kibana를 구축한 다음, 로그를 전송할 fluentd와 DaemonSet을 구축한다.

 

쿠버네티스에 Elasticsearch와 Kibana 구축하기

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: elasticsearch-pvc
  namespace: kube-system
  labels:
    kubernetes.io/cluster-service: "true"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2G
---
apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: kube-system
spec:
  selector:
    app: elasticsearch
  ports:
  - protocol: TCP
    port: 9200
    targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elasticsearch
  namespace: kube-system
  labels:
    app: elasticsearch
spec:
  replicas: 1
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: elasticsearch:5.6-alpine
        ports:
        - containerPort: 9200
          name: http
        volumeMounts:
        - mountPath: /data
          name: elasticsearch-pvc
        - mountPath: /usr/share/elasticsearch/config
          name: elasticsearch-config
      volumes:
      - name: elasticsearch-pvc
        persistentVolumeClaim:
          claimName: elasticsearch-pvc
      - name: elasticsearch-config
        configMap:
          name: elasticsearch-config
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: elasticsearch-config
  namespace: kube-system
data:
  elasticsearch.yml: |-
    http.host: 0.0.0.0
    path.scripts: /tmp/scripts
  log4j2.properties: |-
    status = error
    appender.console.type = Console
    appender.console.name = console
    appender.console.layout.type = PatternLayout
    appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
    rootLogger.level = info
    rootLogger.appenderRef.console.ref = console
  jvm.options: |-
    -Xms128m
    -Xmx256m
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=75
    -XX:+UseCMSInitiatingOccupancyOnly
    -XX:+AlwaysPreTouch
    -server
    -Xss1m
    -Djava.awt.headless=true
    -Dfile.encoding=UTF-8
    -Djna.nosys=true
    -Djdk.io.permissionsUseCanonicalPath=true
    -Dio.netty.noUnsafe=true
    -Dio.netty.noKeySetOptimization=true
    -Dio.netty.recycler.maxCapacityPerThread=0
    -Dlog4j.shutdownHookEnabled=false
    -Dlog4j2.disable.jmx=true
    -Dlog4j.skipJansi=true
    -XX:+HeapDumpOnOutOfMemoryError

 

엘라스틱서치 manifast 파일이다. 볼륨마운트, 컨피그 맵 등의 설정이 추가적으로 들어갔다.

apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: kube-system
spec:
  selector:
    app: kibana
  ports:
  - protocol: TCP
    port: 5601
    targetPort: http
    nodePort: 30050
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: kube-system
  labels:
    app: kibana
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kibana
  template:
    metadata:
      labels:
        app: kibana
    spec:
      containers:
      - name: kibana
        image: kibana:5.6
        ports:
        - containerPort: 5601
          name: http
        env:
        - name: ELASTICSEARCH_URL
          value: "http://elasticsearch:9200"

 

키바나 manifast 파일이다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      app: fluentd-logging
  template:
    metadata:
      labels:
        app: fluentd-logging
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:elasticsearch
        env:
          - name: FLUENT_ELASTICSEARCH_HOST
            value: "elasticsearch"
          - name: FLUENT_ELASTICSEARCH_PORT
            value: "9200"
          - name: FLUENT_ELASTICSEARCH_SCHEME
            value: "http"
          - name: FLUENT_UID
            value: "0"
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

 

마지막으로 fluentd manifast이다. DaemonSet 으로 구성되며 클러스터 노드마다 할당되는 리소스이다. DaemonSet은 클러스터 전반적인 로깅등의 작업을 수행하는 리소스에 할당하는 타입이다. 어떠한 팟마다 같이 뜨는 리소스가 아니고 클러스터 노드마다 뜨는 리소스라고 보면 된다. 로그 컬렉터같이 호스트마다 특정할 역할을 하는 에이전트를 두고자 할 때 적합하다.

 

여기까지 간단하게 쿠버네티스 환경에서 로깅 운영하는 방법을 간단하게 다루어봤다. 다음 예제에서는 기본적인 플로우 이외로 output을 kafka로 보내는 등의 다른 플러그인을 사용하여 로그를 수집하는 예제를 살펴볼 것이다.



출처: https://coding-start.tistory.com/322?category=761720 [코딩스타트]