로깅 아키텍처

로깅 아키텍처 (Logging Architecture)

애플리케이션 로그는 애플리케이션 내부에서 무슨 일이 일어나고 있는지 이해하는 데 도움이 돼요. 로그는 특히 문제를 디버깅하고 클러스터 활동을 모니터링하는 데 유용해요. 대부분의 최신 애플리케이션은 어떤 종류의 로깅 메커니즘을 가지고 있어요. 마찬가지로 컨테이너 엔진은 로깅을 지원하도록 설계돼요. 컨테이너화된 애플리케이션에 가장 쉽고 가장 널리 채택된 로깅 방법은 표준 출력(standard output)과 표준 오류(standard error) 스트림에 쓰는 것이에요.

그러나 컨테이너 엔진이나 런타임이 제공하는 기본 기능은 완전한 로깅 솔루션에는 보통 충분하지 않아요.

예를 들어 컨테이너가 충돌하고, 파드가 퇴거되거나, 노드가 죽으면 애플리케이션의 로그에 접근하고 싶을 수 있어요.

클러스터에서 로그는 노드, 파드, 컨테이너와 독립된 별도의 저장소와 수명 주기를 가져야 해요. 이 개념을 클러스터 레벨 로깅(cluster-level logging)이라고 해요.

클러스터 레벨 로깅 아키텍처는 로그를 저장, 분석, 쿼리하기 위해 별도의 백엔드가 필요해요. 쿠버네티스는 로그 데이터에 대한 기본 저장 솔루션을 제공하지 않아요. 대신 쿠버네티스와 통합되는 많은 로깅 솔루션이 있어요. 다음 섹션들은 노드에서 로그를 처리하고 저장하는 방법을 설명해요.

출처: 문서

본문

파드와 컨테이너 로그

쿠버네티스는 실행 중인 파드의 각 컨테이너에서 로그를 캡처해요.

이 예시는 표준 출력 스트림에 초당 한 번 텍스트를 쓰는 컨테이너가 있는 파드의 매니페스트를 사용해요.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

이 파드를 실행하려면 다음 명령을 사용해요:

kubectl apply -f https://k8s.io/examples/debug/counter-pod.yaml

출력은 다음과 같아요:

pod/counter created

로그를 가져오려면 다음과 같이 kubectl logs 명령을 사용해요:

kubectl logs counter

출력은 다음과 비슷해요:

0: Fri Apr  1 11:42:23 UTC 2022
1: Fri Apr  1 11:42:24 UTC 2022
2: Fri Apr  1 11:42:25 UTC 2022

kubectl logs --previous를 사용해 컨테이너의 이전 인스턴스화에서 로그를 검색할 수 있어요. 파드에 여러 컨테이너가 있으면, -c 플래그로 명령에 컨테이너 이름을 추가해 접근하려는 컨테이너의 로그를 지정해요:

kubectl logs counter -c count

컨테이너 로그 스트림

이 기능을 사용하려면 (또는) 클러스터 관리자가 클러스터의 모든 관련 컴포넌트에 대해 PodLogsQuerySplitStreams 기능 게이트를 활성화해야 해요.

기능 게이트 활성화 또는 비활성화에 대한 자세한 내용은 기능 게이트 활성화/비활성화를 참조하세요.

알파 기능으로, kubelet은 컨테이너가 생성하는 두 표준 스트림(표준 출력과 표준 오류)의 로그를 분리할 수 있어요. 이 동작을 사용하려면 PodLogsQuerySplitStreams 기능 게이트를 활성화해야 해요. 이 기능 게이트가 활성화되면 쿠버네티스 1.37은 Pod API를 통해 이러한 로그 스트림에 직접 접근할 수 있게 해줘요. stream 쿼리 문자열을 사용해 스트림 이름(Stdout 또는 Stderr)을 지정해 특정 스트림을 가져올 수 있어요. 그 파드의 log 하위 리소스를 읽을 수 있는 접근 권한이 있어야 해요.

이 기능을 설명하기 위해 표준 출력과 오류 스트림 모두에 주기적으로 텍스트를 쓰는 파드를 만들 수 있어요.

apiVersion: v1
kind: Pod
metadata:
  name: counter-err
spec:
  containers:
  - name: count
    image: busybox:1.28
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; echo "$i: err" >&2 ; i=$((i+1)); sleep 1; done']

이 파드를 실행하려면 다음 명령을 사용해요:

kubectl apply -f https://k8s.io/examples/debug/counter-pod-err.yaml

stderr 로그 스트림만 가져오려면 다음을 실행할 수 있어요:

kubectl get --raw "/api/v1/namespaces/default/pods/counter-err/log?stream=Stderr"

자세한 내용은 kubectl logs 문서를 참조하세요.

노드가 컨테이너 로그를 처리하는 방법

컨테이너 런타임은 컨테이너화된 애플리케이션의 stdout과 stderr 스트림에 생성된 모든 출력을 처리하고 리디렉션해요. 다른 컨테이너 런타임은 이를 다르게 구현하지만, kubelet과의 통합은 CRI 로깅 형식으로 표준화돼 있어요.

기본적으로 컨테이너가 재시작되면 kubelet은 로그가 있는 하나의 종료된 컨테이너를 유지해요. 파드가 노드에서 퇴거되면 모든 해당 컨테이너도 로그와 함께 퇴거돼요.

kubelet은 쿠버네티스 API의 특별한 기능을 통해 클라이언트가 로그를 사용할 수 있게 해줘요. 이것에 접근하는 일반적인 방법은 kubectl logs를 실행하는 것이에요.

로그 회전 (Log rotation)

kubelet은 컨테이너 로그를 회전하고 로깅 디렉터리 구조를 관리할 책임이 있어요. kubelet은 이 정보를 (CRI를 사용해) 컨테이너 런타임에 보내고, 런타임은 주어진 위치에 컨테이너 로그를 써요.

kubelet 구성 파일을 사용해 두 가지 kubelet 구성 설정인 containerLogMaxSize(기본 10Mi)와 containerLogMaxFiles(기본 5)를 구성할 수 있어요. 이 설정들은 각 로그 파일의 최대 크기와 각 컨테이너에 허용되는 최대 파일 수를 각각 구성할 수 있게 해줘요.

워크로드가 생성하는 로그 양이 큰 클러스터에서 효율적인 로그 회전을 수행하기 위해, kubelet은 얼마나 많은 동시 로그 회전을 수행할 수 있는지와 로그가 모니터링되고 필요에 따라 회전되는 간격 측면에서 로그가 회전되는 방식을 조정하는 메커니즘도 제공해요. kubelet 구성 파일을 사용해 containerLogMaxWorkerscontainerLogMonitorInterval 두 가지 kubelet 구성 설정을 구성할 수 있어요.

기본 로깅 예시에서처럼 kubectl logs를 실행하면, 노드의 kubelet이 요청을 처리하고 로그 파일에서 직접 읽어요. kubelet은 로그 파일의 내용을 반환해요.

참고:

kubectl logs를 통해 사용할 수 있는 것은 최신 로그 파일의 내용뿐이에요.

예를 들어 파드가 40 MiB의 로그를 쓰고 kubelet이 10 MiB 후에 로그를 회전한다면, kubectl logs를 실행하면 기껏해야 10MiB의 데이터가 반환돼요.

시스템 컴포넌트 로그

시스템 컴포넌트에는 두 가지 유형이 있어요: 보통 컨테이너에서 실행되는 컴포넌트와, 컨테이너 실행에 직접 관여하는 컴포넌트. 예를 들어:

  • kubelet과 컨테이너 런타임은 컨테이너 안에서 실행되지 않아요. kubelet은 컨테이너를(파드로 그룹화해) 실행해요.
  • 쿠버네티스 스케줄러, 컨트롤러 매니저, API 서버는 파드 안에서(보통 정적 파드(static Pods)) 실행돼요. etcd 컴포넌트는 제어 플레인에서 실행되며, 가장 일반적으로 정적 파드로도 실행돼요. 클러스터가 kube-proxy를 사용한다면, 보통 이것을 DaemonSet으로 실행해요.

로그 위치

kubelet과 컨테이너 런타임이 로그를 쓰는 방식은 노드가 사용하는 운영 체제에 따라 달라져요:

Linux

systemd를 사용하는 Linux 노드에서 kubelet과 컨테이너 런타임은 기본적으로 journald에 써요. systemd 저널을 읽으려면 journalctl을 사용해요; 예: journalctl -u kubelet.

systemd가 없으면 kubelet과 컨테이너 런타임은 /var/log 디렉터리의 .log 파일에 써요. 로그가 다른 곳에 쓰이게 하려면, 도우미 도구인 kube-log-runner를 통해 kubelet을 간접적으로 실행하고 그 도구를 사용해 kubelet 로그를 선택한 디렉터리로 리디렉션할 수 있어요.

기본적으로 kubelet은 컨테이너 런타임이 /var/log/pods 내의 디렉터리에 로그를 쓰도록 지시해요.

kube-log-runner에 대한 자세한 내용은 시스템 로그를 읽으세요.

Windows

기본적으로 kubelet은 C:\var\logs 디렉터리 내의 파일에 로그를 써요 (C:\var\log가 아님에 주의).

C:\var\log는 이러한 로그의 쿠버네티스 기본 위치이지만, 몇몇 클러스터 배포 도구는 Windows 노드가 대신 C:\var\log\kubelet에 로그하도록 설정해요.

로그가 다른 곳에 쓰이게 하려면, 도우미 도구인 kube-log-runner를 통해 kubelet을 간접적으로 실행하고 그 도구를 사용해 kubelet 로그를 선택한 디렉터리로 리디렉션할 수 있어요.

그러나 기본적으로 kubelet은 컨테이너 런타임이 C:\var\log\pods 디렉터리 내에 로그를 쓰도록 지시해요.

kube-log-runner에 대한 자세한 내용은 시스템 로그를 읽으세요.

파드에서 실행되는 쿠버네티스 클러스터 컴포넌트의 경우, 이것들은 기본 로깅 메커니즘을 우회해 /var/log 디렉터리 내의 파일에 써요(컴포넌트는 systemd 저널에 쓰지 않아요). 쿠버네티스의 저장소 메커니즘을 사용해 컴포넌트를 실행하는 컨테이너에 영구 저장소를 매핑할 수 있어요.

kubelet은 파드 로그 디렉터리를 기본 /var/log/pods에서 사용자 지정 경로로 변경할 수 있어요. 이 조정은 kubelet 구성 파일에서 podLogsDir 파라미터를 구성해 할 수 있어요.

주의:

기본 위치 /var/log/pods가 오랫동안 사용되어 왔고 특정 프로세스가 이 경로를 암묵적으로 가정할 수 있다는 점을 유의하는 것이 중요해요. 따라서 이 파라미터 변경은 주의하고 자신의 책임 하에 접근해야 해요.

또 하나 염두에 둘 주의 사항은, kubelet이 그 위치가 /var와 같은 디스크에 있을 것을 지원한다는 것이에요. 그렇지 않으면 로그가 /var와 별도의 파일시스템에 있다면, kubelet이 그 파일시스템의 사용량을 추적하지 않아, 그것이 가득 차면 문제가 발생할 수 있어요.

etcd와 그 로그에 대한 자세한 내용은 etcd 문서를 보세요. 쿠버네티스의 저장소 메커니즘을 사용해 컴포넌트를 실행하는 컨테이너에 영구 저장소를 매핑할 수 있어요.

참고:

쿠버네티스 클러스터 컴포넌트(스케줄러 같은)를 부모 노드에서 공유된 볼륨에 로그하도록 배포한다면, 그러한 로그가 회전되도록 고려하고 보장해야 해요. 쿠버네티스는 그 로그 회전을 관리하지 않아요.

운영 체제가 일부 로그 회전을 자동으로 구현할 수 있어요 - 예를 들어 /var/log 디렉터리를 컴포넌트용 정적 파드에 공유한다면, 노드 레벨 로그 회전은 그 디렉터리의 파일을 쿠버네티스 밖의 어떤 컴포넌트가 쓴 파일과 동일하게 취급해요.

일부 배포 도구는 그 로그 회전을 처리하고 자동화하며, 다른 것은 이를 여러분의 책임으로 남겨 둬요.

클러스터 레벨 로깅 아키텍처

쿠버네티스가 클러스터 레벨 로깅에 대한 기본 솔루션을 제공하지 않지만, 고려할 수 있는 몇 가지 일반적인 접근 방식이 있어요. 몇 가지 옵션은 다음과 같아요:

  • 모든 노드에서 실행되는 노드 레벨 로깅 에이전트 사용하기.
  • 애플리케이션 파드에 로깅용 전용 사이드카 컨테이너 포함하기.
  • 애플리케이션 내에서 직접 백엔드로 로그 푸시하기.

노드 로깅 에이전트 사용

각 노드에 노드 레벨 로깅 에이전트를 포함해 클러스터 레벨 로깅을 구현할 수 있어요. 로깅 에이전트는 로그를 노출하거나 백엔드에 로그를 푸시하는 전용 도구예요. 일반적으로 로깅 에이전트는 그 노드의 모든 애플리케이션 컨테이너의 로그 파일이 있는 디렉터리에 접근할 수 있는 컨테이너예요.

로깅 에이전트는 모든 노드에서 실행되어야 하므로, 에이전트를 DaemonSet으로 실행하는 것이 권장돼요.

노드 레벨 로깅은 노드당 하나의 에이전트만 만들며 노드에서 실행되는 애플리케이션에 어떤 변경도 요구하지 않아요.

컨테이너는 stdout과 stderr에 쓰지만 협의된 형식은 없어요. 노드 레벨 에이전트는 이러한 로그를 수집하고 집계를 위해 전달해요.

로깅 에이전트와 함께 사이드카 컨테이너 사용

사이드카 컨테이너를 다음 방식 중 하나로 사용할 수 있어요:

  • 사이드카 컨테이너가 애플리케이션 로그를 자신의 stdout으로 스트리밍.
  • 사이드카 컨테이너가 애플리케이션 컨테이너에서 로그를 수집하도록 구성된 로깅 에이전트를 실행.

스트리밍 사이드카 컨테이너 (Streaming sidecar container)

사이드카 컨테이너가 자신의 stdout과 stderr 스트림에 쓰게 함으로써, 이미 각 노드에서 실행되는 kubelet과 로깅 에이전트의 이점을 활용할 수 있어요. 사이드카 컨테이너는 파일, 소켓 또는 journald에서 로그를 읽어요. 각 사이드카 컨테이너는 로그를 자신의 stdout이나 stderr 스트림에 출력해요.

이 접근 방식은 애플리케이션의 다른 부분에서 여러 로그 스트림을 분리할 수 있게 해주며, 일부는 stdout이나 stderr에 쓰기를 지원하지 않을 수 있어요. 로그를 리디렉션하는 논리는 최소이므로 큰 오버헤드가 아니에요. 또한 stdout과 stderr가 kubelet에 의해 처리되므로 kubectl logs 같은 내장 도구를 사용할 수 있어요.

예를 들어 파드가 단일 컨테이너를 실행하고, 그 컨테이너가 두 개의 다른 형식을 사용해 두 개의 다른 로그 파일에 쓴다고 해요. 다음은 그 파드의 매니페스트예요:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
        i=0;
        while true;
        do
          echo "$i: $(date)" >> /var/log/1.log;
          echo "$(date) INFO $i" >> /var/log/2.log;
          i=$((i+1));
          sleep 1;
        done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

두 컴포넌트를 컨테이너의 stdout 스트림으로 리디렉션했더라도, 다른 형식으로 로그 항목을 같은 로그 스트림에 쓰는 것은 권장되지 않아요. 대신 두 개의 사이드카 컨테이너를 만들 수 있어요. 각 사이드카 컨테이너는 공유 볼륨에서 특정 로그 파일을 tail한 다음 로그를 자신의 stdout 스트림으로 리디렉션할 수 있어요.

다음은 두 개의 사이드카 컨테이너가 있는 파드의 매니페스트예요:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
        i=0;
        while true;
        do
          echo "$i: $(date)" >> /var/log/1.log;
          echo "$(date) INFO $i" >> /var/log/2.log;
          i=$((i+1));
          sleep 1;
        done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-1
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/2.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

이제 이 파드를 실행하면 다음 명령을 실행해 각 로그 스트림에 개별적으로 접근할 수 있어요:

kubectl logs counter count-log-1

출력은 다음과 비슷해요:

0: Fri Apr  1 11:42:26 UTC 2022
1: Fri Apr  1 11:42:27 UTC 2022
2: Fri Apr  1 11:42:28 UTC 2022
...
kubectl logs counter count-log-2

출력은 다음과 비슷해요:

Fri Apr  1 11:42:29 UTC 2022 INFO 0
Fri Apr  1 11:42:30 UTC 2022 INFO 0
Fri Apr  1 11:42:31 UTC 2022 INFO 0
...

클러스터에 노드 레벨 에이전트를 설치했다면, 그 에이전트는 추가 구성 없이 그러한 로그 스트림을 자동으로 수집해요. 원한다면 에이전트를 구성해 소스 컨테이너에 따라 로그 줄을 파싱할 수 있어요.

낮은 CPU와 메모리 사용량만 있는 파드(CPU는 수 밀리코어, 메모리는 수 메가바이트 정도)조차도, 로그를 파일에 쓰고 stdout으로 스트리밍하면 노드에 필요한 저장소를 두 배 늘릴 수 있어요. 단일 파일에 쓰는 애플리케이션이 있다면, 스트리밍 사이드카 컨테이너 접근 방식을 구현하는 대신 대상으로 /dev/stdout을 설정하는 것이 권장돼요.

사이드카 컨테이너는 애플리케이션 자체가 회전할 수 없는 로그 파일을 회전하는 데도 사용할 수 있어요. 이 접근 방식의 예시는 logrotate를 주기적으로 실행하는 작은 컨테이너예요. 그러나 stdout과 stderr를 직접 사용하고 회전과 보존 정책은 kubelet에 맡기는 것이 더 간단해요.

로깅 에이전트가 있는 사이드카 컨테이너

노드 레벨 로깅 에이전트가 상황에 충분히 유연하지 않다면, 애플리케이션과 함께 실행되도록 특별히 구성한 별도의 로깅 에이전트가 있는 사이드카 컨테이너를 만들 수 있어요.

참고:

다음은 로깅 에이전트와 함께 사이드카 컨테이너를 구현하는 데 사용할 수 있는 두 예시 매니페스트예요. 첫 번째 매니페스트는 fluentd를 구성하는 ConfigMap을 포함해요.

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluentd.conf: |
    <source>
      type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag count.format1
    </source>

    <source>
      type tail
      format none
      path /var/log/2.log
      pos_file /var/log/2.log.pos
      tag count.format2
    </source>

    <match **>
      type google_cloud
    </match>

참고:

두 번째 매니페스트는 fluentd를 실행하는 사이드카 컨테이너가 있는 파드를 설명해요. 파드는 fluentd가 구성 데이터를 수집할 수 있는 볼륨을 마운트해요.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
        i=0;
        while true;
        do
          echo "$i: $(date)" >> /var/log/1.log;
          echo "$(date) INFO $i" >> /var/log/2.log;
          i=$((i+1));
          sleep 1;
        done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-agent
    image: registry.k8s.io/fluentd-gcp:1.30
    env:
    - name: FLUENTD_ARGS
      value: -c /etc/fluentd-config/fluentd.conf
    volumeMounts:
    - name: varlog
      mountPath: /var/log
    - name: config-volume
      mountPath: /etc/fluentd-config
  volumes:
  - name: varlog
    emptyDir: {}
  - name: config-volume
    configMap:
      name: fluentd-config

애플리케이션에서 직접 로그 노출

모든 애플리케이션에서 직접 로그를 노출하거나 푸시하는 클러스터 로깅은 쿠버네티스의 범위 밖이에요.

더 알아보기 (Learn more)

  • 쿠버네티스 시스템 로그에 대해 읽기.
  • 쿠버네티스 시스템 컴포넌트용 트레이스(Traces)에 대해 배우기.
  • 쿠버네티스가 파드 실패 시 기록하는 종료 메시지를 사용자 지정하는 방법 배우기.