[EKS] Loki 구성 및 EKS 환경에서의 구축

Loki는 로그 집계 시스템으로, 메타데이터 중심의 인덱싱으로 효율적 로그 관리 및 Grafana 통합을 제공합니다.

[EKS] Loki 구성 및 EKS 환경에서의 구축
Photo by Anne Nygård / Unsplash

개요

Notion Image

Loki는 Prometheus에서 영감을 받은 로그 집계 시스템으로, 로깅 및 이벤트 데이터를 수집, 저장 및 검색하기 위한 오픈 소스 플랫폼이며 아래와 같은 특징을 가지고 있습니다.

주요 기능

  1. Prometheus 및 Grafana와의 통합: Loki는 Prometheus 및 Grafana와 자연스럽게 통합되어 로그, 메트릭, 트레이스를 하나의 UI에서 쉽게 관리할 수 있습니다. 또한 Prometheus에서 사용하고 있는 것과 동일한 레이블을 사용하여 메트릭과 로그간 원할한 전환이 가능합니다.
  2. Pod 결합: Loki는 Pod 레이블과 같은 메타데이터를 자동으로 스크랩하고 인덱싱하기 때문에 파드 로그를 적용하는데 적합합니다.
  3. 실시간 로그 분석: Loki는 실시간 및 특정 시간 로그 조회가 가능합니다.

최소 인덱싱: Loki는 로그 전체 텍스트가 아닌 메타데이터만 인덱싱하는 방식을 취합니다.  이는 저장 공간을 절약하고 비용을 줄이는 데 도움이 됩니다.

Notion Image

용어

Notion Image

Loki의 주요 컴포넌트들은 다음과 같습니다.

  1. 로그 수집기
    • Promtail: Loki의 공식 에이전트로, 로그를 수집하고 레이블을 추가하여 Loki로 전송
  2. Distributor
    • 들어오는 로그 스트림을 받아서 처리
    • 로그 데이터의 유효성을 검사하고 준비
    • 수신된 로그를 여러 Ingester에 분산
  3. Ingester
    • 로그 데이터를 메모리에 임시 저장하고 압축 청크(chunk) 단위로 데이터를 구성
    • 일정 기준이 되면 스토리지에 데이터를 저장
  4. 스토리지
    • 인덱스 스토리지: 로그 검색을 위한 인덱스 정보 저장 (예: Cassandra, DynamoDB)
    • 오브젝트(청크) 스토리지: 실제 로그 데이터 저장 (예: S3, GCS)
  5. Querier
    • 로그 쿼리 처리 담당
    • LogQL을 사용하여 로그 검색 및 필터링
    • Ingester와 스토리지에서 데이터를 조회
  6. Query-Frontend
    • Querier의 보조 역할을 하며 읽기 경로를 가속화
    • 내부적으로 쿼리를 조정하고 큐에 저장합니다

Read 흐름

  1. querier가 읽기 요청을 수신
  2. ingester의 in-memory 데이터를 먼저 조회
  3. 데이터 위치에 따른 처리:
    • 캐시에 있으면 → 즉시 반환
    • 캐시에 없으면 → S3(백업 저장소)에서 조회
  4. querier는 최종적으로 중복 제거 후 로그 제공

Write 흐름

  1. distributor가 데이터 수신
  2. 수신된 데이터 해시 처리
  3. distributor가 해시된 데이터를 ingester로 전달
  4. ingester의 처리:
    • chunk 생성 및 저장
  5. distributor가 성공 응답 반환

WAL(Write Ahead Log)

  • 목적: 예기치 않은 장애 상황에서의 데이터 보호
  • 동작 방식:
    1. ingester가 데이터 수신 시 동시에 두 곳에 저장
      • 메모리 적재
      • WAL에 기록 (로컬 파일시스템)
    2. 장애 발생 시
      • 메모리 데이터는 손실될 수 있음
    3. 복구 시
      • WAL에서 데이터를 읽어와 메모리 복원
      • 장애 이전 상태로 복구

정리해보면 Loki는 Grafana 생태계와 잘 통합되고 레이블 기반 인덱싱으로 비용 효율적인 로그 관리가 가능하다는 장점이 있지만, 전체 텍스트 검색이 제한적이고 복잡한 쿼리에서 성능이 저하될 수 있다는 단점이 있습니다. 대표적으로 ELK 스택과 비교되고 있으며 더 자세한 장단점은 블로그 글을 통해 확인할 수 있습니다.

아키텍쳐

Notion Image

Promtail은 DaemonSet으로 각 노드에 배포되며 로그를 읽어 수집하며 라벨을 붙입니다. 이러한 로그를 Loki에서 받아 처리하며 이후 Grafana로 시각화 할 수 있도록 구성됩니다. LogQL을 사용해 원하는 로그 검색 및 필터링하거나 알람처리가 가능하게 됩니다.

혹은 애플리케이션 레벨에서 제공되는 logger와의 원할한 통합이 가능합니다.

테스트 환경

테스트를 위한 EKS 환경은 아래와 같이 구성하였습니다.

cluster-config.yaml

Loki Install

공식 문서를 참조하여 Loki를 설치합니다.

Loki install Docs

helm repo add grafana https://grafana.github.io/helm-charts

helm repo update

helm install --values values.yaml loki grafana/loki

사용한 YAML(values.yaml) 파일은 다음과 같습니다

namespace: loki
deploymentMode: SimpleScalable

loki:
  schemaConfig:
    configs:
      - from: "2024-04-01"
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h
  ingester:
    chunk_encoding: snappy
  querier:
    max_concurrent: 4
  pattern_ingester:
    enabled: true
  limits_config:
    allow_structured_metadata: true
    volume_enabled: true

backend:
  replicas: 2
  persistence:
    enabled: true
    size: 2Gi
    storageClass: "gp3"

read:
  replicas: 2

write:
  replicas: 3
  persistence:
    enabled: true
    size: 2Gi
    storageClass: "gp3"

minio:
  rootUser: admin
  rootPassword: shark123
  enabled: true
  persistence:
    enabled: true
    size: 2Gi
    storageClass: "gp3"

이때 EKS로 테스트하는 경우 볼륨을 설정해야하므로 주의합니다.

custom한 Loki YAML파일은 아래 링크를 통해 확인해볼 수 있습니다.

helm 설치

1. Loki install

https://grafana.com/docs/loki/latest/setup/install/helm/install-microservices/

helm upgrade --install loki grafana/loki-distributed -f loki-config.yaml -n monitoring

2. promtail 설치

Notion Image

https://grafana.com/docs/loki/latest/send-data/promtail/installation/

# values.yaml
config:
# publish data to loki
  clients:
    - url: http://loki-gateway/loki/api/v1/push
      tenant_id: 1
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
# The default helm configuration deploys promtail as a daemonSet (recommended)
helm upgrade --values promtail-config.yaml --install promtail grafana/promtail -n monitoring

Grafana 연동

EKS의 service 명을 확인하여 URL을 연결합니다.

Notion Image

정상적으로 구성되었다면 연결이 성공한 것을 확인할 수 있습니다.

Notion Image
  • Application Logger로 직접 전송하는 경우
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    defaultMeta: {
        hostname: hostname,
        ...podMetadata
    },
    transports: [
        // Loki Transport
        new LokiTransport({
            host: process.env.LOKI_HOST || "http://loki-loki-distributed-gateway.monitoring:80",
            labels: {
                job: 'nodejs-app',
                namespace: process.env.POD_NAMESPACE || 'default',
                pod: hostname
            },
            json: true,
            debug: true,
            batching: true,
            interval: 5,
            replaceTimestamp: true,
            onConnectionError: (err) => console.error('Loki 연결 오류:', err)
        }),
        // Console Transport (개발 환경용)
        new winston.transports.Console({
            format: winston.format.combine(
                winston.format.colorize(),
                winston.format.simple()
            )
        })
    ]
});
Notion Image
Notion Image
  • promtail에서 로그를 수집해가는 경우
Notion Image
scrape_configs:
- job_name: was_config
  static_configs:
  - targets:
      - localhost
    labels:
      job: nodejs-app
      __path__: /var/log/containers/dev-test-app*.log # 패턴
root@ip-10-0-151-198:/var/log/containers# ls

argo-rollouts-65867bc597-n69wf_argo-rollouts_argo-rollouts-9e77641b98655f145efad53119b77821cfc7e53fb2f6d098cc1dcc5af8329526.log
aws-node-8zd7j_kube-system_aws-eks-nodeagent-39f0488e5d517972f70fcfcd1c5936730531f20b6d0c4b0186bf431a9592e02f.log
aws-node-8zd7j_kube-system_aws-node-fd194e158448b1fcc015fdf6ba1270b350c3b0c90e4344e39b0e9e1a9ae527bb.log
aws-node-8zd7j_kube-system_aws-vpc-cni-init-ea3e238d6ad40f9e94192b032b3678376554b0942237b514e158e0d169776f51.log
dev-test-app-676b78476-qkqsq_dev_test-app-9d679c81b6ff7d5ba9cc7db42dcc91f1c901ab59f62dce3aee38afc6fb9b47ef.log
ebs-csi-node-hq6vk_kube-system_ebs-plugin-f4ddae252141626ff8b0179055f0211c97347d92ba758ef6addd3a5864c2050d.log
ebs-csi-node-hq6vk_kube-system_liveness-probe-18958951b626389f275f3c24d963727ccd6137f1a88a3595b1690827033a6a8f.log
ebs-csi-node-hq6vk_kube-system_node-driver-registrar-cb86efc95394bc56d65227cb9bcdf0e4828bdab76ad438bdccea014486b7d518.log
eks-pod-identity-agent-4k8dz_kube-system_eks-pod-identity-agent-67bdab26ce745f0efb279fe9dd2eab8eb01ff52a7c421453dc960fae275ed810.log
eks-pod-identity-agent-4k8dz_kube-system_eks-pod-identity-agent-init-3819ca8f963b1c359c63d922f61c147be32f499497128440e9f20c5e2f07fee5.log
kube-proxy-mtqrd_kube-system_kube-proxy-bcfeb6e1a0479cf6ca4a76441f3415b60e0d867d6e0339ec698ec98bb9cd964d.log
loki-loki-distributed-distributor-58bd46cc7-9lmz5_monitoring_distributor-e4f362e8728cc2f15c62fba8e80a37ac064e65444131740b474e89ddca6010d1.log
loki-loki-distributed-gateway-67686fdf98-wq5sg_monitoring_nginx-2062298d0a961c28f7740c22f20f5b5a1af82492264d1d874c5be27b3adfda2c.log
loki-loki-distributed-query-frontend-7dd49cd98c-2vmxt_monitoring_query-frontend-e7e9e1994006c84c851e39737314f564bdad5da36220c6404adf95e05c29030c.log
metrics-server-d5865ff47-k6xt8_kube-system_metrics-server-d60d9a094c15e291b537d0a426fdb7d2800f386fec0182e3a2830675b84968a1.log
my-grafana-74545c54c4-zrmkh_monitoring_grafana-5f4f5049bb36fa140f8197a28a683ba81faf137db21b7f28b1676807d6e697b8.log
my-grafana-74545c54c4-zrmkh_monitoring_init-chown-data-b22ce217882208c650e12326c14c51cae5aed6fca5ceded6b120af94c32d42ee.log
promtail-rxxdv_monitoring_promtail-06d9cd4503cee3ebfc4fdf540093d9ed5ba791bb93efede1cc5de46e652ba786.log

[트러블슈팅] failed to load chunk

failed to load chunk 'ZmFrZS8zMzgxOWRkOTQ3YTg1ZDRlOjE5M2Q4MDJmZjBkOjE5M2Q4MTMwZmYxOjgyMTYxZDVl': open /var/loki/chunks/ZmFrZS8zMzgxOWRkOTQ3YTg1ZDRlOjE5M2Q4MDJmZjBkOjE5M2Q4MTMwZmYxOjgyMTYxZDVl: no such file or directory

Loki가 multi-pod로 운영될 때 file-system(local-storage) 사용합니다.

Loki가 이렇게 여러 pod로 운영된다면, 각 pod는 독립적인 local storage를 가지게 됩니다. 이로 인해 Pod A의 수집기가 로그를 자신의 local storage에 저장하면, Pod B의 쿼리 서버는 Pod A의 storage에 접근할 수 없어 해당 로그 데이터를 찾지 못하게 됩니다. 이렇기 때문에 메모리에 존재하는 경우만 읽을 수 있으며, 만약 메모리에 로그 데이터가 없는 경우 Loki 쿼리 서버는 로그 데이터를 찾지 못해 에러를 발생시키게 됩니다.

이러한 케이스가 굉장히 빈번하기 때문에 s3 같은 외부 저장소로 이관하는게 적합합니다.

  • Chunk로 만들고 Memory를 비우는 경우 정상적으로 조회가 안될 수 있습니다.

[트러블슈팅] Loki Pending 상태 해결하기

PVC Not Created

사용할 PV가 없어서 발생하는 문제로 PV를 미리 만들어두거나 StroageClass를 통해 동적으로 할당해야합니다.

EBS CSI Driver를 설치하면 기본적으로 gp2 stroage class는 생성됩니다.

gp3 를 사용하기 위해선 StorageClass를 추가하여야합니다.

정책은 아래와 같습니다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
provisioner: ebs.csi.aws.com 
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
allowVolumeExpansion: true
reclaimPolicy: Delete
kubectl create -f <GP3_STORAGECLASS_CONFIG.YAML>
k get storageclasses.storage.k8s.io
NAME   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2    kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  4h55m
gp3    ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   6s

기존에 설치한 loki를 제거합니다.

# 설치된 helm release 확인
helm ls

# loki release 삭제
helm uninstall loki

# PVC 삭제
kubectl delete pvc -l app.kubernetes.io/instance=loki
kubectl delete pvc -l app=minio

모두 삭제된 것을 확인할 수 있습니다.

kubectl get pods,pvc,svc -l app.kubernetes.io/instance=loki

이후 persistent > storageclass를 등록한 뒤 재생성합니다.

helm install --values ./values.yaml loki grafana/loki

정상적으로 바인딩된 것을 확인할 수 있습니다.

Notion Image
$ kubectl get pvc

NAME                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
data-loki-backend-0     Bound    pvc-3024440b-7288-49cc-bac6-f53eb20322b1   2Gi        RWO            gp3            <unset>                 6s
data-loki-backend-1     Bound    pvc-c4c6f497-e2ae-4f45-98b6-5d41699e347c   2Gi        RWO            gp3            <unset>                 6s
data-loki-write-0       Bound    pvc-6b8ccba8-3ca8-4ea4-b0d4-36f88b3abc41   2Gi        RWO            gp3            <unset>                 6s
data-loki-write-1       Bound    pvc-a595a1dd-ec78-41cd-bc7d-8dd6b662af27   2Gi        RWO            gp3            <unset>                 6s
data-loki-write-2       Bound    pvc-e2f6d33f-9181-4012-a45f-e84d9e93dd82   2Gi        RWO            gp3            <unset>                 6s
export-0-loki-minio-0   Bound    pvc-3069d73c-3b58-4925-9e0e-0e7ccef308b9   2Gi        RWO            gp3            <unset>                 6s
export-1-loki-minio-0   Bound    pvc-bacc5a26-5015-4940-9bf9-7d8f2cc30ec1   2Gi        RWO            gp3            <unset>                 6s

0/2 nodes are available: 2 node(s) didn't match pod anti-affinity rules. preemption: 0/2 nodes are available:…

Pod anti-affinity 규칙 때문에 스케줄링이 실패하고 있다는 뜻입니다. Loki StatefulSet이 여러 write Pod를 생성할 때 가용성을 위해 서로 다른 노드에 배포하려고 하는데, 노드 수가 부족한 상황입니다.

노드 수를 추가해야합니다.


S3 연동하기

storage 설정을 넣는다면 S3에서 해당 로그를 저장 가능합니다.

  • 예시 코드입니다.
loki:
  auth_enabled: false
  schemaConfig:
    configs:
      - from: "2024-04-01"
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h
  storage_config:
    aws:
      region: ap-northeast-2
      bucketnames: bjchoi-standard-bucket
      s3forcepathstyle: false
  pattern_ingester:
      enabled: true
  limits_config:
    allow_structured_metadata: true
    volume_enabled: true
    retention_period: 72h # 28 days retention
  querier:
    max_concurrent: 4

  storage:
    type: s3
    bucketNames:
        chunks: bjchoi-standard-bucket
        ruler: bjchoi-standard-bucket
        admin: bjchoi-standard-bucket
    s3:
      region: ap-northeast-2


deploymentMode: SimpleScalable

backend:
  replicas: 3
  persistence:
    enabled: true
    storageClass: "gp3"
    size: 5Gi

read:
  replicas: 3
  persistence:
    enabled: true
    storageClass: "gp3"
    size: 5Gi

write:
  replicas: 3
  persistence:
    enabled: true
    storageClass: "gp3"
    size: 5Gi

# Disable minio storage
minio:
  enabled: false

serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::759320821027:role/20241220_Loki_S3Access_Role_bjchoi

Loki 배포 과정 중 auth_enabled: false 옵션을 줘서 단일 테넌시로 배포 되었고 이로 인해 Directory명이 fake로 설정되었습니다.

Notion Image
Notion Image

Taneant

X-Scope-OrgID는 HTTP 헤더의 일종으로, 주로 멀티테넌트(multi-tenant) 시스템이나 마이크로서비스 아키텍처에서 사용되는 식별자입니다.

주요 용도는:

  1. 조직/테넌트 식별: 요청이 어떤 조직이나 테넌트에 속하는지 식별
  2. 접근 제어: 특정 조직의 리소스에만 접근할 수 있도록 제한
  3. 데이터 분리: 여러 조직의 데이터를 논리적으로 분리

예를 들어:

X-Scope-OrgID: org123

이런 식으로 HTTP 요청 헤더에 포함되어, 백엔드 서비스가 어떤 조직의 컨텍스트에서 요청을 처리해야 하는지 알려줍니다. 이는 특히 Grafana, Prometheus와 같은 모니터링 도구나 클라우드 서비스에서 자주 사용됩니다.

Reference