[EKS] Loki 구성 및 EKS 환경에서의 구축
Loki는 로그 집계 시스템으로, 메타데이터 중심의 인덱싱으로 효율적 로그 관리 및 Grafana 통합을 제공합니다.
개요

Loki는 Prometheus에서 영감을 받은 로그 집계 시스템으로, 로깅 및 이벤트 데이터를 수집, 저장 및 검색하기 위한 오픈 소스 플랫폼이며 아래와 같은 특징을 가지고 있습니다.
주요 기능
- Prometheus 및 Grafana와의 통합: Loki는 Prometheus 및 Grafana와 자연스럽게 통합되어 로그, 메트릭, 트레이스를 하나의 UI에서 쉽게 관리할 수 있습니다. 또한 Prometheus에서 사용하고 있는 것과 동일한 레이블을 사용하여 메트릭과 로그간 원할한 전환이 가능합니다.
- Pod 결합: Loki는 Pod 레이블과 같은 메타데이터를 자동으로 스크랩하고 인덱싱하기 때문에 파드 로그를 적용하는데 적합합니다.
- 실시간 로그 분석: Loki는 실시간 및 특정 시간 로그 조회가 가능합니다.
최소 인덱싱: Loki는 로그 전체 텍스트가 아닌 메타데이터만 인덱싱하는 방식을 취합니다. 이는 저장 공간을 절약하고 비용을 줄이는 데 도움이 됩니다.

용어

Loki의 주요 컴포넌트들은 다음과 같습니다.
- 로그 수집기
- Promtail: Loki의 공식 에이전트로, 로그를 수집하고 레이블을 추가하여 Loki로 전송
- Distributor
- 들어오는 로그 스트림을 받아서 처리
- 로그 데이터의 유효성을 검사하고 준비
- 수신된 로그를 여러 Ingester에 분산
- Ingester
- 로그 데이터를 메모리에 임시 저장하고 압축 청크(chunk) 단위로 데이터를 구성
- 일정 기준이 되면 스토리지에 데이터를 저장
- 스토리지
- 인덱스 스토리지: 로그 검색을 위한 인덱스 정보 저장 (예: Cassandra, DynamoDB)
- 오브젝트(청크) 스토리지: 실제 로그 데이터 저장 (예: S3, GCS)
- Querier
- 로그 쿼리 처리 담당
- LogQL을 사용하여 로그 검색 및 필터링
- Ingester와 스토리지에서 데이터를 조회
- Query-Frontend
- Querier의 보조 역할을 하며 읽기 경로를 가속화
- 내부적으로 쿼리를 조정하고 큐에 저장합니다
Read 흐름
- querier가 읽기 요청을 수신
- ingester의 in-memory 데이터를 먼저 조회
- 데이터 위치에 따른 처리:
- 캐시에 있으면 → 즉시 반환
- 캐시에 없으면 → S3(백업 저장소)에서 조회
- querier는 최종적으로 중복 제거 후 로그 제공
Write 흐름
- distributor가 데이터 수신
- 수신된 데이터 해시 처리
- distributor가 해시된 데이터를 ingester로 전달
- ingester의 처리:
- chunk 생성 및 저장
- distributor가 성공 응답 반환
WAL(Write Ahead Log)
- 목적: 예기치 않은 장애 상황에서의 데이터 보호
- 동작 방식:
- ingester가 데이터 수신 시 동시에 두 곳에 저장
- 메모리 적재
- WAL에 기록 (로컬 파일시스템)
- 장애 발생 시
- 메모리 데이터는 손실될 수 있음
- 복구 시
- WAL에서 데이터를 읽어와 메모리 복원
- 장애 이전 상태로 복구
- ingester가 데이터 수신 시 동시에 두 곳에 저장
정리해보면 Loki는 Grafana 생태계와 잘 통합되고 레이블 기반 인덱싱으로 비용 효율적인 로그 관리가 가능하다는 장점이 있지만, 전체 텍스트 검색이 제한적이고 복잡한 쿼리에서 성능이 저하될 수 있다는 단점이 있습니다. 대표적으로 ELK 스택과 비교되고 있으며 더 자세한 장단점은 블로그 글을 통해 확인할 수 있습니다.
아키텍쳐

Promtail은 DaemonSet으로 각 노드에 배포되며 로그를 읽어 수집하며 라벨을 붙입니다. 이러한 로그를 Loki에서 받아 처리하며 이후 Grafana로 시각화 할 수 있도록 구성됩니다. LogQL을 사용해 원하는 로그 검색 및 필터링하거나 알람처리가 가능하게 됩니다.
혹은 애플리케이션 레벨에서 제공되는 logger와의 원할한 통합이 가능합니다.
테스트 환경
테스트를 위한 EKS 환경은 아래와 같이 구성하였습니다.
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 설치

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을 연결합니다.

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

- 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()
)
})
]
});


- promtail에서 로그를 수집해가는 경우

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
정상적으로 바인딩된 것을 확인할 수 있습니다.

$ 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로 설정되었습니다.


Taneant
X-Scope-OrgID는 HTTP 헤더의 일종으로, 주로 멀티테넌트(multi-tenant) 시스템이나 마이크로서비스 아키텍처에서 사용되는 식별자입니다.
주요 용도는:
- 조직/테넌트 식별: 요청이 어떤 조직이나 테넌트에 속하는지 식별
- 접근 제어: 특정 조직의 리소스에만 접근할 수 있도록 제한
- 데이터 분리: 여러 조직의 데이터를 논리적으로 분리
예를 들어:
X-Scope-OrgID: org123
이런 식으로 HTTP 요청 헤더에 포함되어, 백엔드 서비스가 어떤 조직의 컨텍스트에서 요청을 처리해야 하는지 알려줍니다. 이는 특히 Grafana, Prometheus와 같은 모니터링 도구나 클라우드 서비스에서 자주 사용됩니다.