로키를 활용한 쿠버네티스 로그 수집
쿠버네티스 환경에서 로그를 수집하고 관리하는 건 굉장히 중요한 작업입니다.
여러 서비스가 동적으로 운영되고, 컨테이너화된 애플리케이션들이 많기 때문에, 제대로 된 로그 수집 시스템이 없다면 장애나 성능 문제를 추적하기 어려워져요.
오늘은 로키(Loki)를 사용해서 쿠버네티스에서 로그를 어떻게 수집하고 모니터링할 수 있는지 다뤄보겠습니다.
1. 로그 수집의 필요성: 로그, 왜 중요한가?
시스템이 잘 작동하고 있는지, 혹은 문제가 발생했는지를 파악하기 위해 가장 먼저 확인해야 할 것이 바로 로그입니다.
로그가 없다면, 장애나 이슈가 생겨도 원인을 파악하기 어려워요.
로그 수집이 중요한 이유
-
문제 추적: 시스템에 장애나 에러가 발생했을 때 로그는 가장 중요한 단서가 됩니다.
-
보안 모니터링: 악의적인 요청이나 접근을 로그에서 쉽게 찾아낼 수 있어요.
-
성능 모니터링: 로그를 통해 성능을 모니터링하고, 병목 현상이나 리소스 문제를 파악할 수 있죠.
-
트래픽 분석: 서비스 사용 패턴을 분석하고, 사용자 경험을 개선할 수 있는 인사이트를 얻을 수 있습니다.
2. 로그 수집 오픈소스: 어떤 툴을 써야 할까?
로그 수집을 위한 오픈소스 툴은 다양합니다. 전통적으로는 ELK Stack(Elasticsearch, Logstash, Kibana)이나 Fluentd + Elasticsearch 조합이 많이 쓰였어요.
하지만 쿠버네티스(Kubernetes) 환경에서는 더 가볍고, 유연하며, 확장 가능한 로그 수집 시스템이 필요합니다.
이 점에서 주목받는 도구가 바로 Grafana Loki입니다.
왜 로키(Loki)일까?
-
쿠버네티스 친화적: Loki는 컨테이너 기반 환경에서 로그를 수집하고 저장하는 데 매우 적합하며, Pod 레이블 기반으로 로그를 분류할 수 있어 쿠버네티스와의 궁합이 뛰어납니다.
-
간단한 설정: Prometheus와 유사한 방식으로 작동하고, 설정이 간단해 기존에 Prometheus를 사용하고 있는 팀이라면 빠르게 익숙해질 수 있습니다.
-
리소스 절감: 로그 전체를 인덱싱하지 않고 메타데이터만 인덱싱하기 때문에 리소스 사용량이 적고, 저장소 비용도 낮습니다.
-
확장성과 유연성: 읽기(Read)와 쓰기(Write) 컴포넌트가 분리되어 있어, 시스템 확장이 필요할 때 특정 구성요소만 독립적으로 스케일링이 가능하며, 클러스터 규모가 커져도 안정적인 운영이 가능합니다.
이러한 장점으로 “우아한형제들” 기업에서는 기존에 사용하던 ELK Stack에서 Loki 기반 로그 시스템으로 전환을 진행했어요. 관련 기술 블로그 글의 댓글을 살펴보면, 타기업도 Loki 도입을 적극 검토하고 있는 모습을 확인할 수 있어요.

[ELK에서 Loki로 전환 – 출처: 우아한 형제들 기술 블로그]
그 밖에도 Loki 공식 문서를 보면 다양한 신뢰할 수 있는 기업들이 이미 Loki를 실무에 도입해 활용하고 있는 사례를 다수 소개하고 있습니다.

[로키 성공 사례 – 출처: 로키 공식 문서]
3. 로키 구성 요소
자 그러면, 로키를 가지고 쿠버네티스 로그를 수집해보도록 하겠습니다. 설치하기에 앞서 구성 요소들이 어떻게 동작하는지 이해하는 것이 중요해요. 설치 옵션들을 이해하고 설정해야 하며, 문제가 발생하였을 때 어떤 컴포넌트의 로그를 확인해야 하는지 알아야 하니까요.

일반적인 로키 기반 로깅 스택은 다음의 세 가지 구성 요소로 이루어져 있습니다:
-
에이전트: 에이전트는 로그를 수집한 뒤, 라벨을 붙여 스트림으로 가공하고 HTTP API를 통해 Loki로 전송합니다.
-
Loki 서버: 로그를 수신하고 저장하며, 쿼리를 처리하는 핵심 서버입니다. 운영 목적에 따라 단일 바이너리로도, 마이크로서비스 형태로도 배포할 수 있으며, 쿠버네티스 환경에 자연스럽게 녹아듭니다.
-
Grafana: 수집된 로그를 시각화하거나 쿼리할 때 주로 사용하는 도구입니다. CLI 도구인
logcli나 Loki API를 직접 이용할 수도 있습니다.
그 중 핵심이 되는 로키 서버의 내부 동작 방식을 좀 더 자세히 알아보도록 하겠습니다.
로키의 저장 방식
로키에는 두 가지 주요 파일 유형이 있습니다:
-
인덱스(index): 특정 라벨 집합에 대한 로그를 어디서 찾을 수 있는지에 대한 목차 역할을 합니다.
-
청크(chunk): 특정 라벨 집합에 해당하는 로그 엔트리들을 담고 있는 컨테이너입니다.
로키는 로그 내용을 색인화하지 않고, 로그에 대한 메타데이터인 레이블만 인덱싱합니다.
따라서 쿼리 시, 레이블로 일치하는 청크를 찾아 실제 로그 데이터를 확인하는 구조 입니다.
로키의 컴포넌트 구성을 살펴보며, 로그를 쓰고 읽는 과정이 어떻게 이루어지는지 자세히 살펴보겠습니다.
로키 컴포넌트 구성
|
컴포넌트 |
주요 역할 |
특징 |
|---|---|---|
|
Distributor |
|
|
|
Ingester |
|
|
|
Compactor |
|
|
|
Query Frontend |
|
|
|
Querier |
|
|

[로키에서 로그 읽기, 쓰기 동작 흐름]
Write Path: 로그 쓰기 흐름
-
Distributor가 로그 스트림과 메시지를 포함한 HTTP POST 요청을 수신합니다.
-
내부 해시 링 정보를 기반으로 각 스트림을 어느 Ingester에게 보낼지 결정합니다.
-
해당 스트림을 설정된 복제 수만큼 Ingester에 전송합니다.
-
Ingester는 데이터를 새로운 청크에 저장하거나 기존 청크에 추가합니다.
-
쓰기 완료 후, Distributor에 성공 응답을 전송합니다.
-
Distributor는 설정된 복제 수의 과반수(quorum) 이상에서 응답을 받을 경우 성공 응답(2xx)을 반환합니다.
Read Path: 로그 읽기 흐름
-
쿼리 프론트엔드가 LogQL 쿼리가 포함된 HTTP GET 요청을 수신합니다.
-
쿼리 프론트엔드는 쿼리를 하위 쿼리(Sub-query) 로 분할하고, Query Scheduler에 전달합니다.
-
Querier가 Scheduler로부터 하위 쿼리를 받아 처리합니다.
-
Querier는 모든 Ingester에 쿼리를 전달하고, Ingester의 메모리 내 로그를 우선 조회합니다.
-
데이터가 부족할 경우, 백엔드 스토리지(예: S3)에서 청크를 로드하여 조회합니다.
-
중복 제거(deduplication)를 수행한 후, 하위 쿼리 결과를 Query Frontend로 반환합니다.
-
Query Frontend는 결과를 병합해 최종 응답을 클라이언트에 전달합니다.
4. 로키 설치하기
로키에 대한 이해를 바탕으로 이제 설치해보도록 하겠습니다.
로키는 유연한 확장을 위해 여러 마이크로서비스로 구성된 분산 시스템 입니다. 크게 세 가지 모드로 설치할 수 있습니다.
본 문서에서는 공식 사이트에서 권장하는 “Simple Scalable” 방식으로 설치해보도록 하겠습니다.
Helm에 Grafana 차트 저장소 추가:
helm repo add grafana https://grafana.github.io/helm-charts
차트 저장소 업데이트:
helm repo update
구성 파일을 만듭니다:
Helm 차트를 이용해 MinIO를 저장소로 사용하는 Loki를 단일 노드에서도 실행 가능하도록 구성합니다.
운영 환경이 아닌 테스트 목적의 설정이므로, 일부 설정값은 권장 기준보다 낮게 지정되어 있습니다.
아래 구성은 테스트 목적의 단일 노드 환경을 위한 설정입니다. backend, read, write 모두 기본적으로 노드 안티어피니티(Anti-Affinity) 규칙이 적용되어 동일 노드에 여러 파드가 스케줄되지 않습니다.
단일 노드 환경에서 replicas 값을 권장 값으로 변경하여 테스트하려면 차트의 affinity 설정을 사용자 정의 값으로 오버라이드해 적용해주세요. (참고)
|
항목 |
설명 |
권장 설정 |
|---|---|---|
|
|
로그 복제 수 설정 |
|
|
|
로그 테이블 스키마 정의 |
– |
|
|
로그 데이터 압축 방식 |
|
|
|
DrillDown 기능 활성화 |
|
|
|
배포 모드 설정 |
|
|
백엔드 구성 요소 |
|
|
|
|
Distributor 및 Ingester 포함 (StatefulSet) |
|
|
|
Querier 및 Query-frontend 포함 (Deployment) |
|
|
|
Compactor 포함 (StatefulSet) |
|
|
기타 설정 |
|
|
|
|
Memcached 기반 쿼리 캐싱 설정 옵션 |
설정 필요 |
|
|
내부 저장소 MinIO 사용 |
|
|
|
외부 접근 서비스 타입 |
|
vi loki.yaml
loki:
commonConfig:
replication_factor: 1
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
deploymentMode: SimpleScalable
backend:
replicas: 1
read:
replicas: 1
write:
replicas: 1
chunksCache:
resources:
limits:
memory: 4096Mi
requests:
cpu: 500m
memory: 4096Mi
maxItemMemory: 2m
allocatedMemory: 4096Mi
minio:
enabled: true
gateway:
service:
type: LoadBalancer
설정 파일 작성이 끝났으면, 설치합니다. log-system 이름의 네임스페이스에 로키를 설치합니다.
helm install --values loki.yaml loki grafana/loki --namespace log-system --create-namespace
정상적으로 설치가 되었으면 아래 리소스들이 표시됩니다.
kubectl get pod -n log-system

로그를 정상적으로 저장하고, 조회할 수 있는지 간단하게 테스트 해보도록 하겠습니다.
curl 명령어를 실행할 수 있는 임시 nginx 파드를 띄우고, 해당 파드 내에서 로그 쓰기 및 로그 조회 요청을 실행합니다.
# 임시 nginx 파드 실행
$ kubectl run -i --tty --rm test-pod \
--image=nginx \
--restart=Never \
--namespace=log-system \
-- /bin/bash
# 로그 쓰기
root@test-pod:/# curl -H "Content-Type: application/json" \
-X POST -s "http://loki-gateway/loki/api/v1/push" \
-H "X-Scope-OrgId: foo" \
--data-raw '{
"streams": [
{
"stream": {"job": "test"},
"values": [["'"$(date +%s)000000000"'", "fizzbuzz"]]
}
]
}'
# 로그 읽기
root@test-pod:/# curl "http://loki-gateway/loki/api/v1/query_range" \
--data-urlencode 'query={job="test"}' \
-H "X-Scope-OrgId: foo"
정상적으로 로그 쓰기에 성공했다면, 읽기 요청 시, 아래와 같은 데이터를 반환하는 것을 확인할 수 있어요.
{"status":"success","data":{"resultType":"streams","result":[{"stream":{"detected_level":"unknown","job":"test","service_name":"test"},"values":[["1742445896000000000","fizzbuzz"]]}],"stats":{"summary":{"bytesProcessedPerSecond":217,"linesProcessedPerSecond":13,"totalBytesProcessed":16,"totalLinesProcessed":1,"execTime":0.073556,"queueTime":0.000487,"subqueries":0,"totalEntriesReturned":1,"splits":5,"shards":5,"totalPostFilterLines":1,"totalStructuredMetadataBytesProcessed":8},"querier":{"store":{"totalChunksRef":0,"totalChunksDownloaded":0,"chunksDownloadTime":0,"queryReferencedStructuredMetadata":false,"chunk":{"headChunkBytes":0,"headChunkLines":0,"decompressedBytes":0,"decompressedLines":0,"compressedBytes":0,"totalDuplicates":0,"postFilterLines":0,"headChunkStructuredMetadataBytes":0,"decompressedStructuredMetadataBytes":0},"chunkRefsFetchTime":0,"congestionControlLatency":0,"pipelineWrapperFilteredLines":0}},"ingester":{"totalReached":5,"totalChunksMatched":1,"totalBatches":6,"totalLinesSent":1,"store":{"totalChunksRef":0,"totalChunksDownloaded":0,"chunksDownloadTime":0,"queryReferencedStructuredMetadata":false,"chunk":{"headChunkBytes":16,"headChunkLines":1,"decompressedBytes":0,"decompressedLines":0,"compressedBytes":0,"totalDuplicates":0,"postFilterLines":1,"headChunkStructuredMetadataBytes":8,"decompressedStructuredMetadataBytes":0},"chunkRefsFetchTime":26350054,"congestionControlLatency":0,"pipelineWrapperFilteredLines":0}},"cache":{"chunk":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"index":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"result":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"statsResult":{"entriesFound":2,"entriesRequested":3,"entriesStored":3,"bytesReceived":348,"bytesSent":0,"requests":6,"downloadTime":373190,"queryLengthServed":60000000000},"volumeResult":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"seriesResult":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"labelResult":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0},"instantMetricResult":{"entriesFound":0,"entriesRequested":0,"entriesStored":0,"bytesReceived":0,"bytesSent":0,"requests":0,"downloadTime":0,"queryLengthServed":0}},"index":{"totalChunks":0,"postFilterChunks":0,"shardsDuration":0,"usedBloomFilters":false}}}}
5. Alloy를 활용한 쿠버네티스 로그 수집
앞에서 로그를 저장하고 조회할 수 있는 로키 서버를 설치했다면, 이제 로그를 수집하는 에이전트를 설치해보도록 하겠습니다.
Alloy는 쿠버네티스 환경에서 로그를 더 효율적으로 수집하고 분석하는 데 도움이 되는 툴이에요. Alloy는 로키와 쉽게 연동됩니다.
헬름 차트를 통하여 Alloy를 설치해보고 쿠버네티스 파드의 로그를 수집해보도록 하겠습니다.
Helm에 Grafana 차트 저장소 추가:
앞에서 로키 설치할 때 이미 추가한 저장소 입니다.
helm repo add grafana https://grafana.github.io/helm-charts
차트 저장소 업데이트:
helm repo update
구성 파일을 만듭니다:
쿠버네티스 환경에서 파드의 네임스페이스, 파드 이름 등 다양한 메타 정보를 라벨로 추가하여 Loki로 로그를 전송하는 구성을 작성해보도록 하겠습니다. 자세한 내용은 공식 문서를 참고해주세요.
|
구성 요소 |
설명 |
|---|---|
|
|
Kubernetes API에서 파드 정보를 검색 |
|
|
검색된 파드 목록에 리라벨링 전략 적용 |
|
|
Kubernetes 파드로부터 로그 수집 |
|
|
정적 라벨(job) 추가, 네임스페이스 기반 테넌트 ID 지정 등의 처리를 수행합니다. |
|
|
처리된 로그를 Loki로 전송 |
vi alloy.yaml
alloy:
configMap:
content: |-
logging {
level = "debug"
format = "logfmt"
}
loki.write "pod_log" {
endpoint {
url = "http://loki-gateway:80/loki/api/v1/push"
}
}
discovery.kubernetes "pod" {
role = "pod"
}
discovery.relabel "pod_logs" {
targets = discovery.kubernetes.pod.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
action = "replace"
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_image"]
action = "replace"
target_label = "container_image"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
action = "replace"
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_port_number"]
action = "replace"
target_label = "container_port"
}
rule {
source_labels = ["__meta_kubernetes_pod_ip"]
action = "replace"
target_label = "pod_ip"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
action = "replace"
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_node_name"]
action = "replace"
target_label = "node"
}
rule {
source_labels = ["__meta_kubernetes_pod_phase"]
action = "replace"
target_label = "status"
}
}
loki.source.kubernetes "pod_logs" {
targets = discovery.relabel.pod_logs.output
forward_to = [loki.process.pod_logs.receiver]
}
loki.process "pod_logs" {
stage.static_labels {
values = {
job = "k8s-pod-log",
}
}
stage.tenant {
source = "namespace"
}
forward_to = [loki.write.pod_log.receiver]
}
자, 이제 아래 명령어를 통하여 Alloy를 설치합니다.
helm install --values alloy.yaml alloy grafana/alloy --namespace log-system
6. 그라파나를 이용하여 수집된 로그 조회하기
열심히 수집은 했는데, 수집한 로그를 보기 힘드시다구요?😭
그라파나를 활용하면 수집된 로그를 대시보드 형태로 시각화 하여 한 눈에 파악할 수 있습니다.
그라파나를 설치하여, 지금까지 수집한 로그 정보를 확인해보도록 할게요.
그라파나 설정하기
-
Helm에 Grafana 차트 저장소 추가:
앞에서 로키 설치할 때 이미 추가한 저장소 입니다.
helm repo add grafana https://grafana.github.io/helm-charts -
차트 저장소 업데이트:
helm repo update -
그라파나 설치:
helm install grafana grafana/grafana \ --namespace log-system \ --set adminUser=admin \ --set adminPassword=admin -
그라파나 접속
포트 포워딩을 통해 외부에서 Grafana 서비스에 접속합니다.
Docker 드라이버 위에 Minikube를 설치한 경우, 다음 명령어를 사용하여 로컬 호스트에서 클러스터 내부 리소스를 포트 포워딩할 수 있습니다.
minikube service grafana -n log-system -
로그인
포트포워딩 한 URL로 대시보드에 접속하여 로그인 합니다. (아이디:
admin, 패스워드:admin) -
데이터 소스 추가
Data sources 메뉴에 접속하여 설치한 로키 서버의 정보로 데이터 소스를 추가합니다.
-
좌측 메뉴 > Dashboards > Import
-
Dashboard ID에
13639입력 -
데이터 소스로
Log System선택 후 완료
-
-
로그 탐색
수집된 로그와 라벨을 확인하려면 필터링 조건이 반드시 필요합니다. 앞서 수집한 로그에는 모두 정적 라벨로
job=k8s-pod-log가 붙어 있습니다. 이 조건으로 조회하여 전체 로그를 확인합니다.-
좌측 메뉴 > Explore
-
라벨 선택 후 새로고침 아이콘 선택
-

Explore 접속 화면
-
로그 시각화 (드릴 다운)
그라파나는 이 밖에도 수집된 로그를 그래프 형태로 시각화하여 제공하는 기능도 지원합니다.
수집된 로그에서 오류 로그의 범위가 어느 정도 되는지 시각적으로 확인할 수 있어요.
-
좌측 메뉴 > Drilldown
-
새로고침 아이콘 선택
-

Drilldown 접속 화면
7. 마무리 하며..
지금까지 Loki 기반 쿠버네티스 로그 수집 시스템 구축의 전체 흐름을 따라가 봤습니다.
✅ 왜 로그 수집이 중요한지,
✅ 기존의 ELK 스택 대비 Loki가 가진 장점은 무엇인지,
✅ Loki의 내부 아키텍처와 로그가 쓰이고 읽히는 방식은 어떤지,
✅ Helm을 활용한 실전 설치 방법과,
✅ Alloy를 활용해 쿠버네티스 로그를 수집하는 방법,
✅ 그리고 Grafana를 통해 수집된 로그를 탐색하고 시각화하는 방법까지 함께 다뤄보았어요.
로그를 다루는 일은 단순히 데이터를 모으는 것을 넘어서, 어떤 정보를 추출하고 어떻게 시각화할지, 무엇을 분석하고 어떻게 활용할지에 대한 끊임없는 고민이 필요한 여정입니다.
이번 리서치를 통해, 개인적으로는 솔루션을 개발할 때 어떤 로그를 남겨야 할지 깊이 고민해보는 계기가 되었습니다. 로키의 효율적인 로그 저장과 분석 방법을 조사하면서, 안정적인 로그 수집 환경을 구축하기 위해 인프라 관점에서 고려해야 할 요소들에 대해서도 많은 통찰을 얻을 수 있었습니다.
로그 수집과 분석은 단순한 데이터 처리 이상의 의미를 지니며, 시스템의 건강을 진단하고 관리하는 핵심 도구입니다. 저를 비롯한 이 글을 읽으시는 여러분 또한 로그를 통해 문제를 조기에 발견하고, 성능을 최적화하는 능력을 키워나가길 바라면서..
긴 글 읽어주셔서 감사합니다😄
레퍼런스
-
로그 수집 오픈소스
-
로키 구성 요소
-
로키 헬름 차트 설치하기
-
Alloy를 활용한 쿠버네티스 로그 수집
-
그라파나를 이용하여 수집된 로그 조회하기
