본문으로 건너뛰기

10.1 로깅

컨테이너에는 로그를 위한 영구 저장소가 없다. 컨테이너가 재시작되면 파일시스템이 초기화되고, Pod가 노드에서 사라지면 그 안에 쌓인 것도 함께 사라진다. 그래서 Kubernetes의 로깅 규약은 단순하다. 애플리케이션은 로그를 파일 대신 stdout과 stderr로 출력하고, 그 스트림을 어디에 어떻게 보존할지는 플랫폼이 책임진다. 이 챕터는 그 스트림이 노드의 파일로 기록되는 경로, kubectl logs가 그 파일을 읽는 방법, 그리고 로그를 클러스터 밖으로 수집하는 세 가지 아키텍처를 다룬다.

stdout에서 노드 파일까지

컨테이너가 stdout/stderr에 쓴 내용은 컨테이너 런타임(containerd 등)이 받아 노드의 파일로 기록한다. kubelet이 CRI를 통해 런타임에 로그 파일 경로를 지정하며, 경로 규칙은 다음과 같다.

/var/log/pods/<namespace>_<pod명>_<pod UID>/<컨테이너명>/0.log

/var/log/containers/에는 이 파일들을 가리키는 심볼릭 링크가 만들어진다. 노드 에이전트 방식의 수집기가 보통 이 링크 디렉토리를 감시한다. 파일 내용은 CRI 로깅 포맷으로, 각 줄에 타임스탬프와 스트림 구분(stdout/stderr)이 붙는다.

kubectl logs를 실행하면 요청이 API server를 거쳐 해당 노드의 kubelet에 전달되고, kubelet이 이 파일을 읽어 돌려준다. 즉 로그 조회는 노드 디스크의 파일에 의존하며, etcd 같은 별도 저장소를 거치지 않는다. 노드가 죽으면 그 노드에 있던 로그도 함께 접근 불가가 된다.

    flowchart TD
  APP["컨테이너 stdout/stderr"] --> RT["컨테이너 런타임"]
  RT --> F["/var/log/pods/…/0.log"]
  F --> KL["kubelet"]
  KL -->|"kubectl logs"| U["사용자"]
  F --> AG["노드 에이전트<br/>(DaemonSet)"]
  AG --> BE["로그 백엔드"]
  

로테이션은 kubelet이 담당한다. kubelet 설정의 containerLogMaxSize(기본 10Mi)를 넘으면 파일을 로테이션하고, containerLogMaxFiles(기본 5)개까지만 보관한다. 주의할 점은 kubectl logs가 반환하는 것이 최신 로그 파일의 내용뿐이라는 사실이다. 로테이션으로 넘어간 이전 파일은 노드에는 남아 있지만 kubectl로는 조회되지 않는다.

kubectl logs 사용법

kubectl logs mypod                    # 단일 컨테이너 Pod의 로그
kubectl logs mypod -c app             # 멀티 컨테이너 Pod에서 컨테이너 지정
kubectl logs mypod -f                 # 실시간 스트리밍 (follow)
kubectl logs mypod -p                 # 직전 인스턴스의 로그 (previous)
kubectl logs mypod --since=1h         # 최근 1시간
kubectl logs mypod --tail=100         # 마지막 100줄
kubectl logs -l app=web --prefix      # label selector로 여러 Pod 한꺼번에

이 중 진단에서 가장 자주 쓰는 옵션이 -p다. CrashLoopBackOff처럼 컨테이너가 재시작을 반복하는 상황에서는 현재 인스턴스가 아직 아무 로그도 남기지 못한 경우가 많다. 크래시 원인은 죽기 직전의 인스턴스가 남긴 로그에 있으므로 -p로 직전 인스턴스를 조회한다. kubelet은 현재 인스턴스와 직전 인스턴스, 두 세대의 로그만 제공한다.

멀티 컨테이너 Pod에서 -c를 생략하면 에러와 함께 컨테이너 목록이 출력된다. --all-containers=true를 지정하면 전부 한꺼번에 출력된다.

수집 아키텍처 세 가지

노드 파일은 로테이션으로 사라지는 임시 보관소다. 로그를 검색 가능한 형태로 오래 보존하려면 클러스터 수준의 수집 구조가 필요한데, 공식 문서는 세 가지 패턴을 제시한다.

가장 널리 쓰는 것은 노드 에이전트 방식이다. Fluent Bit 같은 수집기를 DaemonSet으로 배포해 노드마다 하나씩 두고, /var/log/pods 아래 파일을 tail해 백엔드로 전송한다. 노드당 에이전트 하나면 되므로 오버헤드가 가장 작고, 애플리케이션은 stdout 규약만 지키면 수집 구조를 몰라도 된다. 대신 stdout으로 나오지 않는 로그는 수집하지 못한다.

애플리케이션이 stdout 대신 파일에만 로그를 쓰도록 만들어져 있다면 사이드카 방식으로 보완한다. 같은 Pod에 스트리밍 사이드카 컨테이너를 붙여 로그 파일을 tail하고 자신의 stdout으로 다시 내보내면, 그다음부터는 노드 에이전트 경로에 합류한다. 로그가 디스크에 두 번 기록되는 비용이 생기고 Pod마다 컨테이너가 하나씩 늘어난다. 사이드카에 수집 에이전트 자체를 넣어 백엔드로 직접 전송하는 변형도 있지만, 이 경우 로그가 kubelet의 로그 경로를 거치지 않아 kubectl logs로 조회되지 않는다. 참고로 이런 보조 컨테이너를 restartPolicy: Always인 init container로 선언하는 네이티브 sidecar 컨테이너 기능은 1.29에서 베타로 기본 활성화됐고 1.33에서 GA다.

세 번째는 애플리케이션이 로깅 백엔드로 직접 전송하는 방식이다. Kubernetes가 관여할 것이 없어 가장 단순해 보이지만, 전송 로직과 백엔드 설정이 애플리케이션 코드로 들어가고, stdout에 병행 출력하지 않는 한 kubectl logs로 아무것도 볼 수 없게 된다. 조직 표준 로깅 라이브러리가 이미 자리 잡은 경우가 아니라면 앞의 두 방식을 먼저 검토한다.

control plane 컴포넌트의 로그

진단 대상이 워크로드가 아니라 클러스터 자체일 때도 있다. kubeadm으로 구축한 클러스터에서 kube-apiserver, kube-scheduler, kube-controller-manager는 static Pod로 실행되므로 워크로드와 같은 방법으로 조회한다.

kubectl logs -n kube-system kube-apiserver-<노드명>
kubectl logs -n kube-system kube-scheduler-<노드명>

kubelet 자체는 노드의 systemd 서비스로 동작하므로 해당 노드에서 journalctl을 실행한다.

journalctl -u kubelet --since "1 hour ago"

managed Kubernetes에서는 control plane 노드에 접근하지 못하므로, 각 클라우드가 제공하는 로깅 서비스로 API server 로그와 audit 로그를 확인한다. 반대로 직접 운영하는 클러스터에서 API server가 응답하지 않는 최악의 상황이라면 kubectl 자체가 동작하지 않으므로, control plane 노드에 접속해 /var/log/pods 아래의 static Pod 로그 파일을 직접 읽는 것이 마지막 수단이다.

정리

  • 컨테이너 로그는 런타임이 /var/log/pods에 기록하고 kubelet이 containerLogMaxSize(기본 10Mi), containerLogMaxFiles(기본 5) 기준으로 로테이션한다. kubectl logs는 최신 파일, 그리고 -p로 직전 인스턴스까지만 보여준다.
  • 장기 보존은 노드 에이전트 DaemonSet이 기본형이다. 파일로만 로그를 쓰는 앱은 스트리밍 사이드카로 보완하고, 앱 직접 전송은 kubectl logs를 포기하는 대가가 따른다.
  • control plane은 static Pod 컴포넌트면 kubectl logs로, kubelet은 journalctl로 확인한다.