
프로젝트의 성능을 테스트하고 Kafka, MySQL, RAVO-SPRING 등의 로그를 분석하기 위해서 초기에는 ELK 스택 도입을 고려하였습니다.
하지만 클러스터를 다중화한다고 하더라도, 최소 4GB의 메모리와 3vCPU가 요구되는 해당 형태의 로깅 인프라는 현재 상황에 적합하지 않다고 고려하였습니다.
프로젝트의 규모가 크지 않기에, 우선적으로 단순 로그 수집 및 전달을 위해서 JVM 위에서 동작하는 Logstash를 사용하기에는 운영 부담이 존재했습니다.
따라서 Logstash를 Fluent Bit으로 교체한 형태의 EFK 아키텍처를 구성하였습니다.
(E) Elasticsearch: 강력한 검색과 저장 기능은 그대로 유지합니다.(F) Fluent Bit: Logstash를 대체할 초경량, 고성능 로그 수집기입니다.(K) Kibana를: 로그를 보여줄 대시보드입니다.Fluent Bit은 Logstash 대비 수십 MB 수준의 매우 낮은 메모리 점유율을 가집니다.EFK 스택을 도입할 때의 기존 ELK 스택에 비한 장점은 위와 같으며
Elasticsearch 인덱스 관리
데이터 보관 주기에 맞춰 오래된 인덱스를 자동으로 삭제하거나 아카이빙하는 ILM(Index Lifecycle Management) 정책을 반드시 설정해 디스크 용량을 관리해야 합니다.
로그 처리의 부재
Fluent Bit은 가벼운 만큼 Logstash가 제공하는 복잡한 데이터 변환이나 외부 API 연동을 통한 데이터 Enrichment 기능은 부족합니다.
만약 로그에 특별한 처리가 필요하다면, 로그를 생성하는 애플리케이션단에서 구조화된 로그(JSON)를 출력하도록 구현해야 합니다.
주의해야 할 점은 위와 같습니다.
[root@master logging]# k create ns logging
namespace/logging created
[root@master logging]# k get ns
NAME STATUS AGE
default Active 148d
kafka Active 93d
kube-node-lease Active 148d
kube-public Active 148d
kube-system Active 148d
logging Active 3s
monitoring Active 142d
우선 위와 같이 logging 네임스페이스를 생성해줍니다.
![]()
Elasticsearch는 분산형 검색 및 분석 엔진으로, 클러스터, 노드, 샤드, 인덱스, 리플리카, 분석기 등의 핵심 구성 요소를 가지고 있습니다. 이러한 구성 요소들이 상호 작용하며 데이터를 저장, 검색, 분석하는 데 사용됩니다.
- Cluster:
여러 노드로 구성된 Elasticsearch의 최상위 단위로, 데이터를 분산 저장하고 검색 요청을 처리합니다.- Node:
Elasticsearch 인스턴스가 실행되는 서버 또는 가상 머신입니다. 각 노드는 클러스터의 일부이며, 데이터를 저장하거나 검색 요청을 처리하는 역할을 담당합니다.- Index:
유사한 특징을 가진 문서들의 논리적인 컬렉션입니다. 예를 들어, 쇼핑몰 데이터의 경우 상품, 주문, 사용자 정보를 각각 다른 인덱스로 구성할 수 있습니다.- Shard:
인덱스는 여러 개의 샤드로 나누어집니다. 샤드는 Lucene 인스턴스이며, 데이터를 저장하고 검색하는 기본 단위입니다.
- Primary Shard: 데이터의 원본을 저장합니다.
- Replica Shard: 프라이머리 샤드의 복사본으로, 고가용성을 위해 사용됩니다. 장애 발생 시 레플리카 샤드가 프라이머리 샤드의 역할을 대체합니다.
- Analyzer:
텍스트 데이터를 인덱싱하거나 검색하기 전에 처리하는 구성 요소입니다. 토큰화, 필터링, 정규화 등의 작업을 수행합니다.
데이터 샤딩은 대용량의 데이터를 여러 개의 작은 단위, 즉 Shard로 분할하여 서로 다른 서버에 분산 저장 및 처리하는 기술입니다.
데이터베이스의 Horizontal Scaling을 위한 가장 대표적인 방법 중 하나입니다.
A, B, C, D)을 Hash Function에 입력합니다.| 기법 | 설명 | 장점 | 단점 |
|---|---|---|---|
| 범위 기반 샤딩 (Range Based Sharding) | 샤딩 키의 범위를 기준으로 데이터를 분할합니다. (예: 우편번호 10000~19999는 A서버, 20000~29999는 B서버) | 구조가 간단하고 특정 범위의 데이터를 가져오는 쿼리에 효율적입니다. | 데이터가 특정 범위에 몰릴 경우(Hot Spot) 부하가 집중될 수 있습니다. |
| 해시 기반 샤딩 (Hash Based Sharding) | 샤딩 키를 해시 함수(Hash Function)에 적용한 결과값을 기준으로 데이터를 분할합니다. | 데이터를 각 샤드에 비교적 균등하게 분배할 수 있어 트래픽 분산에 유리합니다. | 특정 범위의 데이터를 조회하기 어렵고, 샤드 개수를 변경할 때 데이터 재정렬이 복잡합니다. |
| 디렉토리 기반 샤딩 (Directory Based Sharding) | 샤딩 키와 샤드의 매핑 정보를 별도의 조회 테이블(Lookup Table)에 저장하여 관리합니다. | 샤딩 키를 유연하게 관리하고 동적으로 샤드를 추가/삭제하기 용이합니다. | 조회 테이블 자체에 병목이 발생할 수 있으며, 관리 포인트가 늘어납니다. |
helm repo add elastic https://helm.elastic.co
helm repo update
Elasticsearch의 공식 Helm 차트를 사용하기 위해 리포지토리를 추가합니다.
# 단일 노드 클러스터로 설정
replicas: 1
minimumMasterNodes: 1
# 리소스 제한 (환경에 맞게 조정 가능)
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
# Persistent Volume 설정
volumeClaimTemplate:
accessModes: [ "ReadWriteOnce" ]
# `local-path' 스토리지 클래스 지정
storageClassName: "local-path"
resources:
requests:
# 데이터 저장 공간 (필요시 증설)
storage: 20Gi
이후 values.yaml 메타데이터를 위와 같이 조정합니다.
단일 노드 ElasticSearch 클러스터로 설정하고 제한된 리소스 설정을 추가합니다.
또한 local-path 스토리지 클래스를 지정하여 로그 데이터를 저장하도록 설정합니다.
helm install elasticsearch elastic/elasticsearch \
--namespace logging \
--version 7.17.3 \
-f elasticsearch-values.yaml
Helm 차트 버전을 명시하면 예기치 않은 버전 변경으로 인한 문제를 예방할 수 있기에 널리 사용되는 7.17.3 버전을 사용하여 Helm 차트를 설치합니다.
[root@master k8s]# k get all
NAME READY STATUS RESTARTS AGE
pod/elasticsearch-master-0 1/1 Running 0 4m23s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/elasticsearch-master ClusterIP 10.97.176.38 <none> 9200/TCP,9300/TCP 4m24s
service/elasticsearch-master-headless ClusterIP None <none> 9200/TCP,9300/TCP 4m24s
NAME READY AGE
statefulset.apps/elasticsearch-master 1/1 4m24s
이후 위와 같이 성장적으로 elasticsearch Pod가 배포된 모습을 볼 수 있습니다.
# Kibana가 통신할 Elasticsearch 클러스터의 서비스 주소
elasticsearchHosts: "http://elasticsearch-master.logging.svc.cluster.local:9200"
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"
# 외부 접근을 위한 NodePort 설정
service:
type: NodePort
port: 5601
nodePort: 30561
Kibana는 Elasticsearch처럼 JVM 기반이 아니라 Node.js 기반이기 때문에 더 적은 리소스를 할당합니다.
또한 Kibana가 통신할 Elasticsearch 클러스터의 서비스 주소를 value.yaml에 명시해줍니다.
helm install kibana elastic/kibana \
--namespace logging \
--version 7.17.3 \
-f kibana-values.yaml
이후 7.17.3 버전임을 명시하고 설치를 진행하면
[root@master k8s]# k get pod
NAME READY STATUS RESTARTS AGE
elasticsearch-master-0 1/1 Running 0 16m
kibana-kibana-6546bbf5b5-7l6ts 1/1 Running 0 11m
Pod들이 정상적으로 배포되는 모습을 볼 수 있습니다.
$ sudo semanage port -a -t http_port_t -p tcp 5601
$ vim /etc/nginx/conf.d/k8s-kibana.conf
server {
listen 5601;
server_name _;
location / {
proxy_pass http://172.20.112.101:30561;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
또한 위와 같이 NodePort로 포워딩해주는 nginx 프록시를 설정해주면
위와 같이 외부에서 Kibana 대시보드에 정상적으로 접근이 가능한 것을 볼 수 있습니다.
1. 각 노드에 있는 Fluent Bit Pod는 해당 노드에서 실행되는 다른 모든 애플리케이션 Pod들의 로그를 수집하고 필터링하여 중앙 저장소로 전달하는 경량 로그 수집기(Shipper) 역할을 합니다.
2. 클러스터의 모든 노드에서 수집된 로그는 중앙 로그 저장소인 Elasticsearch로 전송됩니다.
3. 사용자나 개발자는 Kibana를 통해 Elasticsearch에 저장된 로그를 확인합니다.
helm repo add fluent https://fluent.github.io/helm-charts
helm repo update
우선 fluent Helm Repo를 추가해줍니다.
# Kubernetes API 접근을 위한 RBAC 생성
rbac:
create: true
# DaemonSet으로 배포하여 모든 노드에 Pod가 하나씩 실행되도록 보장
kind: DaemonSet
resources:
requests:
cpu: "50m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "100Mi"
# 마스터 노드의 Taint를 무시하고 파드를 스케줄링하도록 설정
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
config:
# INPUT: 컨테이너 로그 수집
inputs: |
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser cri
DB /var/log/flb_kube.db
Mem_Buf_Limit 5MB
# fluent-bit 자신과 kube-system 네임스페이스 로그는 수집에서 제외
Exclude_Path *fluent-bit*,*kube-system*
# FILTER: Kubernetes 메타데이터(pod, namespace, labels 등) 추가
filters: |
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
# OUTPUT: Elasticsearch로 로그 전송
outputs: |
[OUTPUT]
Name es
Match *
# Elasticsearch 서비스 주소
Host elasticsearch-master.logging.svc.cluster.local
Port 9200
# 이 설정을 통해 'k8s-log-YYYY.MM.DD' 형식의 일별 인덱스가 생성됨
Logstash_Format On
Logstash_Prefix k8s-log
# Index 이름에 '.' 이 들어가는 것을 허용
Replace_Dots On
Retry_Limit False
이후 위와 같은 value.yaml을 설정하여 준 뒤
helm install fluent-bit fluent/fluent-bit \
--namespace logging \
-f fluent-bit-values.yaml
Helm을 통해 이를 설치하면
[root@master logging]# k logs -f fluent-bit-hfc4t
Fluent Bit v4.0.7
* Copyright (C) 2015-2025 The Fluent Bit Authors
* Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
* https://fluentbit.io
______ _ _ ______ _ _ ___ _____
| ___| | | | | ___ (_) | / || _ |
| |_ | |_ _ ___ _ __ | |_ | |_/ /_| |_ __ __/ /| || |/' |
| _| | | | | |/ _ \ '_ \| __| | ___ \ | __| \ \ / / /_| || /| |
| | | | |_| | __/ | | | |_ | |_/ / | |_ \ V /\___ |\ |_/ /
\_| |_|\__,_|\___|_| |_|\__| \____/|_|\__| \_/ |_(_)___/
[2025/08/30 07:39:54] [ info] [fluent bit] version=4.0.7, commit=8c5cbc1bd8, pid=1
[2025/08/30 07:39:54] [ info] [storage] ver=1.5.3, type=memory, sync=normal, checksum=off, max_chunks_up=128
[2025/08/30 07:39:54] [ info] [simd ] SSE2
[2025/08/30 07:39:54] [ info] [cmetrics] version=1.0.5
[2025/08/30 07:39:54] [ info] [ctraces ] version=0.6.6
[2025/08/30 07:39:54] [ info] [input:tail:tail.0] initializing
[2025/08/30 07:39:54] [ info] [input:tail:tail.0] storage_strategy='memory' (memory only)
[2025/08/30 07:39:54] [ info] [input:tail:tail.0] db: delete unmonitored stale inodes from the database: count=0
[2025/08/30 07:39:54] [ info] [filter:kubernetes:kubernetes.0] https=1 host=kubernetes.default.svc port=443
각 Pod의 로그가 위와 같이 정상적으로 보이는 모습을 볼 수 있으며
[root@master logging]# k get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
fluent-bit-gmb6b 1/1 Running 0 24m 10.244.0.107 master <none> <none>
fluent-bit-lxxss 1/1 Running 0 15m 10.244.17.12 k8s-worker-1 <none> <none>
fluent-bit-xrgbz 1/1 Running 0 24m 10.244.12.157 k8s-worker-2 <none> <none>
모든 Node들에 대해 fluent-bit Pod가 작동하는 모습을 볼 수 있습니다.
kubectl port-forward --namespace logging svc/elasticsearch-master 9200:9200
이후 Log Rotation을 적용하기 위해 Master Node에서 Elasticsearch API를 호출하기 위해 kubectl port-forward 명령을 사용합니다.
curl -X PUT "http://localhost:9200/_ilm/policy/k8s_log_policy" -H 'Content-Type: application/json' -d'
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_age": "1d",
"max_size": "25gb"
}
}
},
"delete": {
"min_age": "7d",
"actions": {
"delete": {}
}
}
}
}
}
'
우선 curl 명령을 사용하여 k8s_log_policy라는 이름의 ILM 정책을 생성합니다.
위 정책은 7일 후에 delete 단계로 인덱스를 이동시킵니다.
curl -X PUT "http://localhost:9200/_template/k8s_log_template" -H 'Content-Type: application/json' -d'
{
"index_patterns": ["k8s-log-*"],
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"index.lifecycle.name": "k8s_log_policy",
"index.lifecycle.rollover_alias": "k8s-log"
}
}
'
이후 fluent-bit이 k8s-log-* 패턴으로 생성하는 새로운 인덱스들이 ILM 정책을 자동으로 사용하도록 인덱스 템플릿을 생성하여 일주일 단위의 Log Rotation 설정을 완료할 수 있습니다.
왼쪽 Stack Management > Index Patterns으로 이동하여 Create index pattern 버튼을 클릭합니다.
ndex pattern name에 k8s-log-*를 입력하고 Next step을 클릭합니다.
이후 Time field로 @timestamp를 선택하고 Create index pattern 클릭하여줍니다.
이후 Discover 탭으로 이동하면 클러스터에서 수집된 모든 로그가 표시되는 것을 알 수 있습니다.

kubernetes.labels.app : "wwwm-spring-be-v2"
라이브 서비스되고 있는 애플리케이션 로그만 보기 위해 위와 같이 애플리케이션 레이블로 필터링을 진행하면
실제로 서비스를 이용하였을 때
kubectl logs로 확인한 로그들을
Kibana에서도 확인할 수 있습니다.
$ kubectl get svc -n logging elasticsearch-master
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch-master ClusterIP 10.97.176.38 <none> 9200/TCP,9300/TCP 3h54m
추후 로그들을 외부 애플리케이션에 바로 적용 가능하도록 elasticsearch-master SVC를 확인하고
$ kubectl patch svc elasticsearch-master -n logging -p '{"spec": {"type": "NodePort"}}'
$ kubectl get svc -n logging elasticsearch-master
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch-master NodePort 10.97.176.38 <none> 9200:32171/TCP,9300:32610/TCP 3h55m
kubectl patch를 통해 "type": "NodePort"로 변경하여줍니다.
# sudo semanage port -a -t http_port_t -p tcp 2171
server {
listen 2171;
server_name _;
location / {
proxy_pass http://172.20.112.101:32171;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
이후 이전과 유사하게 nginx 프록시를 설정해주면
$ curl -X GET "http://_:2171/k8s-log-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"kubernetes.pod_name": "wwwm-spring-be-v2"
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
],
"size": 10
}
'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0{
"took" : 307,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [
{
"_index" : "k8s-log-2025.08.30",
"_type" : "_doc",
"_id" : "n0Sj-pgBZwz61aXAx_44",
"_score" : null,
"_source" : {
"@timestamp" : "2025-08-30T11:01:33.565Z",
"time" : "2025-08-30T07:01:33.565529803-04:00",
"stream" : "stdout",
"logtag" : "F",
"message" : "Hibernate: ",
"kubernetes" : {
"pod_name" : "wwwm-spring-be-v2-86f76b65fd-zmzm8",
"namespace_name" : "default",
"pod_id" : "852b804c-37a0-4548-8cb6-0df2e9a5ad25",
"labels" : {
"app" : "wwwm-spring-be-v2",
"pod-template-hash" : "86f76b65fd"
},
"host" : "master",
"pod_ip" : "10.244.0.87",
"container_name" : "wwwm-spring-be-v2",
"docker_id" : "75cd6d26c37c2cd2da9150e157b676024292a6a8d86fb60c70891425a748e37d",
"container_hash" : "docker.io/judemin/wwwm-spring-be@sha256:278c9ea2f9c2b545de06d8df5239d92d353dcb22375da1fad1e60c0c26f64fb4",
"container_image" : "docker.io/judemin/wwwm-spring-be:68a7e54"
}
},
"sort" : [
1756551693565
]
},
{
"_index" : "k8s-log-2025.08.30",
"_type" : "_doc",
"_id" : "oESj-pgBZwz61aXAx_44",
"_score" : null,
"_source" : {
"@timestamp" : "2025-08-30T11:01:33.565Z",
"time" : "2025-08-30T07:01:33.565529803-04:00",
"stream" : "stdout",
"logtag" : "F",
"message" : " select",
"kubernetes" : {
"pod_name" : "wwwm-spring-be-v2-86f76b65fd-zmzm8",
"namespace_name" : "default",
"pod_id" : "852b804c-37a0-4548-8cb6-0df2e9a5ad25",
"labels" : {
"app" : "wwwm-spring-be-v2",
"pod-template-hash" : "86f76b65fd"
},
"host" : "master",
"pod_ip" : "10.244.0.87",
"container_name" : "wwwm-spring-be-v2",
"docker_id" : "75cd6d26c37c2cd2da9150e157b676024292a6a8d86fb60c70891425a748e37d",
"container_hash" : "docker.io/judemin/wwwm-spring-be@sha256:278c9ea2f9c2b545de06d8df5239d92d353dcb22375da1fad1e60c0c26f64fb4",
"container_image" : "docker.io/judemin/wwwm-spring-be:68a7e54"
}
},
"sort" : [
1756551693565
]
},
...
외부에서 curl 명령을 통해 로그를 열람할 수 있는 것을 볼 수 있습니다.
이러한 방법은 로그가 노출되지 않아야 하는 환경에서는 위험하지만 현재는 AIOps, Failover 시스템 구축 등을 위해 임시적으로 포트를 열어두었습니다.