예상 독자
- uvicorn + k8s로 로그를 찍으려는데 로그가 안 나오는 슬픈 사람들
개발환경
- NKS, FastAPI, Docker, K8S, Source Pipeline
문제 상황
- NKS로 구성된 API 서버 로그를 좀 더 구체적으로 보기 위해서 logger.info로 로그를 찍고 kubectl log <pod이름> 으로 log를 보고 싶었다.
- Uvicorn이 기본적으로 남겨주는 API서버 로그들은 제대로 출력이 되는 반면 내가 logger.info로 찍는 로그들은 제대로 나오지 않았다.
- CI과정에 NCP Container Registry에 도커 이미지를 올려두는데 똑같은 이미지를 일반 Server + Docker 환경에서 pull 받은 경우 커스텀 로그가 제대로 찍혔다.
- 작은 애플리케이션을 운영하고 있고, 인수인계가 3개월단위로 이루어지고 있기 때문에 elk, loki는 염두해 두지 않고 콘솔+파일에 로그를 찍는 것을 목적으로 두었다.
k8s 로그 메커니즘 이해하기
k8s는 파일 디스크립터 1번 2번만 로그로 취급한다. 즉, 애플리케이션 로그가 컨테이너 표준 출력 스트림으로 출력되어야 한다!
그래야 쿠버네티스가 로그를 탐지할 수 있다.
https://kubernetes.io/docs/concepts/cluster-administration/logging/
파이썬 log들을 콘솔에 찍으려면 표준출력으로 보내야한다.
아래와 같이 Gemini나 GPT가 logger를 설정하라고 알려줄거다!
import logging
import sys
# 1. 로거 객체 생성
logger = logging.getLogger("my_app_logger")
logger.setLevel(logging.INFO) # 로거의 최소 로그 레벨 설정
# 2. StreamHandler(표준 출력) 객체 생성
# sys.stdout을 인자로 전달하여 표준 출력(콘솔)에 로그를 보냄
stream_handler = logging.StreamHandler(sys.stdout)
# 3. Formatter 객체 생성 및 형식 지정
# 로그 메시지의 형식을 지정. 예: 시간 - 로거이름 - 레벨 - 메시지
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 4. 핸들러와 포매터 연결
stream_handler.setFormatter(formatter)
# 5. 로거에 핸들러 추가
logger.addHandler(stream_handler)
# 로그 메시지 출력
logger.info("Application started.")
logger.warning("Something might be wrong.")
logger.error("An error occurred!")
하지만 이렇게하면 도커에서는 돌아가지만 k8s에서는 돌아가지 않는다!
정말 신기했던 것은 uvicorn이 자동으로 직접 찍는 로그들은 kubectl log로 조회가 되는데
logger.info(f"[docs_main] 현재 데이터 읽기 시작")
이런식으로 내가 직접 커스텀한 로그들은 찍히지 않는 것이다!!
이쯤되면 생각해볼 수 있다
- uvicorn이 내가 만든 로그를 덮어쓰고 있다. 한마디로 내 로그들이 씹히는거다!! ㅠㅠ
- 그러면 uvicorn이 뭘까?
uvicorn 이해하기
Uvicorn은 python으로 만들어진 ASGI 웹서버이다.
WSGI의 비동기 버전으로 WSGI가 싱글 스레드 요청 처리에 초점을 맞춘 반면 ASGI는 비동기 I/O를 통해 여러 요청을 동시에 처리할 수 있다.
나는 FastAPI를 쓰고 있었는데 FastAPI에 사용되는 웹서버가 uvicorn이다.
https://devocean.sk.com/blog/techBoardDetail.do?ID=165922&boardType=techBlog
상단의 링크의 블로그에 uvicorn을 잘 설명해주셔서 이것을 보면 더 좋을 것 같다.
uvicorn log config
이제 왜 내 로그가 uvicorn에게 덮어 씌여지는지 알아볼 차례이다.
여기서부턴 추측성이 많아서 정확하지 않을 수 있다. 이 사람은 이런 생각의 흐름으로 → 이런 방식을 사용해서 → k8s+uvicorn에 로그가 찍히지 않는 문제를 해결했구나 정도로만 보면 좋을듯하다.
https://www.uvicorn.org/settings/#logging
Settings - Uvicorn
Settings Use the following options to configure Uvicorn, when running from the command line. Configuration Methods There are three ways to configure Uvicorn: Command Line: Use command line options when running Uvicorn directly. uvicorn main:app --host 0.0.
www.uvicorn.org
--log-config <path> - Logging configuration file.
Options: dictConfig() formats: .json, .yaml.
Any other format will be processed with fileConfig().
Set the formatters.default.use_colors and formatters.access.use_colors values
to override the auto-detected behavior.
If you wish to use a YAML file for your logging config,
you will need to include PyYAML as a dependency for your project or install uvicorn with the [standard] optional extras.
공식 문서를 보면 —log-config <yml 파일> 을 사용해서 logging을 설정해주라고 한다.
# AS-IS
uvicorn main:app --host 0.0.0.0 --port 8000
# TO-BE
uvicorn main:app --host 0.0.0.0 --port 8000 --log-config logging_config.yaml
두 명령어의 차이점은 uvicorn 웹서버를 실행할때 부터 내가 원하는 로그 설정으로 고정을 해주겠다는 거다.
AS-IS와 같이 명령어를 입력하면 내가 원하는 로그 설정이 아닌 uvicorn 기본 설정으로 돌아가는 것 같았다!
version: 1
disable_existing_loggers: false
formatters:
default:
(): uvicorn.logging.DefaultFormatter
fmt: "%(levelprefix)s %(asctime)s - %(name)s - %(message)s"
use_colors: null
access:
(): uvicorn.logging.AccessFormatter
fmt: '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
handlers:
console:
class: logging.StreamHandler
formatter: default
stream: ext://sys.stdout
level: INFO
file:
class: logging.handlers.RotatingFileHandler
formatter: default
filename: logs/app.log
maxBytes: 10485760
backupCount: 5
encoding: utf-8
level: INFO
loggers:
"": -> 루트 로거
handlers: [console, file]
level: INFO
uvicorn: -> uvicron
handlers: [console, file]
level: INFO
propagate: false
uvicorn.error:
handlers: [console, file]
level: INFO
propagate: false
uvicorn.access:
handlers: [console, file]
level: INFO
propagate: false
yaml 파일은 위와 같이 적어준다. 나는 파일에도 로그를 찍어주고 싶어서 fileHandler도 사용했다.
https://docs.python.org/ko/3/library/logging.handlers.html
파이썬 공식문서를 보면 logging handler에 대한 설명이 나와있다. 보통 StreamHandler로 콘솔을 다루는 듯하고 FileHandler로 로그 파일을 다루는 듯 하다!
우리는 k8s에서 로그를 찍는 것이 목적이니 반드시!! stream을 sys.out으로 설정해야한다.
k8s에서 logging할 수 있게 yaml에 logger 설정해주기!
이제 k8s에 배포를 해야한다.
deployment는 아래와 같이 정의 되어있다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
labels:
app: backend
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
imagePullSecrets:
- name: regcred
containers:
- name: backend-container
image: {경로}
workingDir: /app
ports:
- containerPort: {포트}
command: ["uvicorn"]
args: ["main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "logging_config.yaml"]
envFrom:
- configMapRef:
name: backend-env
volumeMounts:
- name: logging-config-volume
mountPath: /app/logging_config.yaml
subPath: logging_config.yaml
- name: logs-volume
mountPath: /app/logs
volumes:
- name: logging-config-volume
configMap:
name: logging-config
- name: logs-volume
emptyDir: {}
실행명령어에 logging-config.yaml이 있기 때문에 k8s 볼륨에 logging-config.yaml을 올려줘야 한다.
logging-config.yaml을 ConfigMap에 yaml 형태로 저장해 둔 뒤 이 yaml을 k8s volume에 mount하고 uvicorn으로 실행하면 k8s에서도 내가 커스텀한 logging 설정으로 실행될 수 있다.
apiVersion: v1
kind: ConfigMap
metadata:
name: logging-config
data:
logging_config.yaml: |
version: 1
disable_existing_loggers: false
formatters:
default:
(): uvicorn.logging.DefaultFormatter
fmt: "%(levelprefix)s %(asctime)s - %(name)s - %(message)s"
use_colors: null
access:
(): uvicorn.logging.AccessFormatter
fmt: '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
handlers:
console:
class: logging.StreamHandler
formatter: default
stream: ext://sys.stdout
level: INFO
file:
class: logging.handlers.RotatingFileHandler
formatter: default
filename: logs/app.log
maxBytes: 10485760
backupCount: 5
encoding: utf-8
level: INFO
loggers:
"":
handlers: [console, file]
level: INFO
uvicorn:
handlers: [console, file]
level: INFO
propagate: false
uvicorn.error:
handlers: [console, file]
level: INFO
propagate: false
uvicorn.access:
handlers: [console, file]
level: INFO
propagate: false
volume과 volume mount 알아보기
위의 yaml에서는 두 개의 볼륨을 마운트한다.
- logging-config-volume
- logging-config ConfigMap을 /app/logging_config.yaml 경로에 파일로 마운트
- logs-volume
- 로그 파일을 저장할 logs 디렉토리를 위한 임시 저장소(emptyDir)를 생성하여 마운트
이쯤에서 Volume과 Volume mount의 개념을 알고 K8S에서 어떻게 작동하는지 짚고 넘어갈 필요가 있다.
https://kubernetes.io/docs/concepts/storage/volumes/
[Volumes
Kubernetes volumes provide a way for containers in a pod to access and share data via the filesystem. There are different kinds of volume that you can use for different purposes, such as: populating a configuration file based on a ConfigMap or a Secret pro
kubernetes.io](https://kubernetes.io/docs/concepts/storage/volumes/)
Volume
- Pod안에 있는 컨테이너들에게 FileSystem을 통해 data를 공유하고 접근할 수 있게 해준다.
- 이를 테면 Configmap이나 Secret 같은 설정 파일을 저장할 수도 있고, 같은 pod 안에 서로 다른 두개의 컨테이너들의 fs를 공유할 수 있게도 해준다.
Volume mount
호스트의 fs경로를 컨테이너 내부에 연결하는 것을 의미한다. 마운트를 해야 디렉토리나 파일을 도커 컨테이너 내부에서 사용하거나 읽을 수 있다!
Volume의 종류
찾다보니 Volume의 종류는 두가지로 나뉘는 듯하다.
Persistent Volumes와 Ephermal Volumes로 나뉘는데, 전자의 경우 영구적인 친구들, 후자의 경우 일시적인 친구들이다.
앞에서 다룬 ConfigMap은 Ephermal Volumes에 해당하며 Pod안에 Kubernetes 데이터들을 주입하는 역할을 한다.
https://kubernetes.io/docs/concepts/storage/persistent-volumes/
https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/
[Ephemeral Volumes
This document describes ephemeral volumes in Kubernetes. Familiarity with volumes is suggested, in particular PersistentVolumeClaim and PersistentVolume. Some applications need additional storage but don't care whether that data is stored persistently acro
kubernetes.io](https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/)
앞으로 더 나아가기 위해서는?
- 로그들은 팀원 모두 볼 수 있도록 스토리지에 적재한다.
- ELK, Loki등 로깅 솔루션들을 붙여본다!!
참고 자료들
'K8S & Docker' 카테고리의 다른 글
| [NKS] NKS + Prometheus + Loki + Promtail로 로그 수집하고 Ncloud Storage(또는 Object Storage) 에 로그 보관하기 (0) | 2025.11.06 |
|---|---|
| [NKS] NKS NAS 볼륨 CSI를 사용해 두개 이상의 pod끼리 파일 공유하기 (1) | 2025.10.27 |
| [NKS] K8S Cron Job으로 PVC 청소하기 🫧🧽 (0) | 2025.10.17 |
