이 글에서 다루는 것

dev/prod 완전 분리된 Loki + Promtail 로그 파이프라인을 구축하고, LogQL 기반으로 FastAPI/Airflow 로그를 중앙에서 조회할 수 있는 구조를 만드는 과정을 다룹니다.

선수지식


이 단계에서 해결하려는 문제

운영형 MLOps에서 가장 위험한 장애는 모델이 아닌 로그가 안 보이는 순간이다. FastAPI, MLflow, Airflow가 모두 돌아가도, 로그가 한 번 섞이거나 누락되면 장애 원인을 찾지 못해 운영 자체가 멈춰버린다. dev/prod를 완전히 분리한 Loki + Promtail 로그 파이프라인을 구축해, 모델 핫스왑, 에러 패턴, API 이상 징후까지 한눈에 보이도록 만든다.


🎯 핵심 요약

  • 로그 파이프라인 전체 구성: Promtail -> Loki -> Grafana(LogQL), dev/prod 모두 완전 분리
  • Ingress + TLS + GitOps 자동 적용
  • fastapi-dev / fastapi-prod / airflow-dev / airflow-prod 로그 전부 정상 수집 확인
  • Query-Range 기반 로그 조회 (Instant Query 불가 원리 포함)
  • LogQL 핵심 패턴 포함

1️⃣ 전체 구조 개요

🧩 전체 파이프라인

[Pod Log] → Promtail → Loki Distributor → Ingester → TSDB → Querier → Grafana

🌱 환경별 완전 분리

  • loki-dev / loki-prod
  • promtail-dev / promtail-prod
  • grafana-dev.local / grafana-prod.local

2️⃣ Loki Helm 구성 (single-binary + NFS PVC)

2-1. 왜 single-binary?

dev/prod 각각 1개의 Loki 인스턴스이므로 single-binary 모드가 최적이다. 필요 시 distributed 모드로 확장 가능하다(distributor, ingester, querier, compactor 분리).

2-2. 도입한 주요 옵션

loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: filesystem
    filesystem:
      chunks_directory: /var/loki/chunks
      rules_directory: /var/loki/rules
  persistence:
    enabled: true
    storageClassName: nfs-observability
    size: 20Gi

TSDB는 쓰기 빈도가 Prometheus보다 낮아 NFS로도 충분하다. Prometheus와는 반대로, Loki는 NFS 기반으로 오래 보관하는 구조가 안정적이다.


3️⃣ Promtail Helm 구성 (네임스페이스별 + 라벨 기반)

3-1. 핵심: dev/prod 라벨 구분

config:
  clients:
    - url: http://loki-dev:3100/loki/api/v1/push
  snippets:
    pipelineStages:
      - docker: {}
      - labeldrop:
          - filename
      - match:
          selector: '{namespace="fastapi-dev"}'
          stages:
            - regex:
                expression: '.*'
  • clients.url로 환경별 Loki 엔드포인트 분리
  • match.selector로 네임스페이스 기준 필터링

3-2. worker2 종료 지연 문제 해결

원인: Promtail DaemonSet + NFS Unmount 지연으로 종료 대기 시간 증가

terminationGracePeriodSeconds: 10

종료 대기 시간을 짧게 잡아, 노드 종료 지연을 완화한다.


4️⃣ Grafana / Loki API 쿼리 (LogQL + Instant Query 이슈)

4-1. Instant Query 오류

증상: log queries are not supported as an instant query type

원인: /loki/api/v1/query는 특정 시점의 메트릭 조회용 엔드포인트이다. 로그는 시간 범위를 지정하는 스트림 데이터이므로 Instant Query 개념 자체와 충돌한다.

4-2. Query Range 사용

curl -G http://loki-dev:3100/loki/api/v1/query_range \
  --data-urlencode 'query={namespace="fastapi-dev"}' \
  --data-urlencode 'start=1732440000000000000' \
  --data-urlencode 'end=1732440600000000000'

start, end는 나노초 단위 Unix 타임스탬프이다. 범위를 지정해야만 로그 스트림이 올바르게 조회된다.


5️⃣ Grafana Log Dashboard 구성

5-1. FastAPI 전용 관측판

패널 구성 예시: 최근 1시간 FastAPI 로그 스트림, level="ERROR" 필터 패널, /variant/{alias} API 로그 상태 패널, 모델 reload 성공/실패 로그 패널.

5-2. 대표 LogQL 예시

fastapi-dev 전체 로그:

{namespace="fastapi-dev", container="fastapi"}

ERROR만:

{namespace="fastapi-dev"} |= "ERROR"

모델 Reload 성공/실패 필터:

{namespace="fastapi-dev"} |= "Reload"

이상 요청 탐지 (5xx 응답):

{namespace="fastapi-prod"} |= "predict" |~ "5\d{2}"

6️⃣ GitOps 기반 전체 구조

6-1. ArgoCD Application (loki-dev 예시)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: loki-dev
spec:
  project: dev
  source:
    repoURL: https://grafana.github.io/helm-charts
    chart: loki
    targetRevision: 5.41.3
    helm:
      values: |
        loki:
          commonConfig:
            replication_factor: 1
        persistence:
          enabled: true
          storageClassName: nfs-observability
  destination:
    namespace: loki-dev
    server: https://kubernetes.default.svc

prod는 project: prod, namespace: loki-prod로 동일 구조.

6-2. Promtail Application (values 예시)

promtail:
  config:
    clients:
      - url: http://loki-dev:3100/loki/api/v1/push
    positions:
      filename: /var/log/positions.yaml
  persistence:
    enabled: true
    storageClassName: nfs-observability

dev/prod 각각 별도 namespace + 별도 PVC로 구성한다. positions.yaml이 환경 간 섞이지 않도록 분리해야 한다.


7️⃣ Ingress + TLS + 인증

Grafana/Alertmanager와 동일한 패턴을 사용한다.

ingress:
  enabled: true
  className: nginx
  hosts:
    - loki-dev.local
  tls:
    - secretName: loki-tls
      hosts:
        - loki-dev.local

prod: loki-prod.local, loki-prod-tls 등으로 분리. TLS Secret은 SealedSecret 기반으로 Git에 저장 가능하다.


8️⃣ 대표 검증 명령

# fastapi-dev 로그 확인 (Loki API)
curl -G http://loki-dev:3100/loki/api/v1/query_range \
  --data-urlencode 'query={namespace="fastapi-dev"}' \
  --data-urlencode 'limit=50'

# Promtail -> Loki Path 확인
kubectl -n promtail-dev logs daemonset/promtail | grep "loki-dev"

# Loki 스토리지 사용량 확인
kubectl -n loki-dev exec -it deploy/loki-dev -- df -h | grep loki

🧩 팁

  • 로그 필터링에서 가장 중요한 축은 namespace + container
  • FastAPI 로그 포맷이 일관되면, Promtail에서 전용 파서(logfmt/json/datetime regex)를 붙여 메트릭/라벨로 뽑을 수 있음
  • Promtail positions.yaml이 NFS 전역에서 공유되면 오염 가능성이 있으므로, dev/prod 각각 PVC를 분리하는 것이 안전
  • LogQL은 |= (substring), |~ (regex), !~ (negative regex) 이 3개만으로도 실제 운영에서 80~90%는 해결 가능

설계 판단 (Why This Way?)

Loki는 single-binary로 시작하여 운영 복잡도를 최소화하고, append-only 쓰기 패턴에 적합한 NFS를 스토리지로 선택했습니다. Promtail의 positions.yaml을 dev/prod로 분리하여 로그 읽기 오프셋 공유로 인한 누락·중복 수집을 방지합니다.


다음에 읽을 글

Observability 5단계: 운영 중 실제 이슈 & 해결 과정 — 실전 트러블슈팅 사례