K3s 홈랩 하루치 운영기 — Logstash 파이프라인, etcd leader 안정화, ECK 크래시 디버깅
ELK 학습 목표 5가지 — 구축 / 자동 수집 / Logstash 변환 / ES 고급 집계 / Kibana 대시보드 — 를 한 번에 끝내려고 시작한 하루였는데, 중간에 클러스터 부하 spike·etcd leader 변동·ECK operator 크래시 루프까지 줄줄이 만나서 결국 종합적인 운영 회고가 됐다.
이 글은 그 하루(2026-05-17)에 손댄 것들 중 포트폴리오로 의미 있는 다섯 주제를 추려서 정리한다. 왜 했나 → 어떻게 했나 → 시행착오 → 배운 점 순서로.
⚠️ 보안 — 이 글에는 클러스터 IP·비밀번호·토큰은 포함되지 않는다. 실제 운영 식별자는
<redacted>또는 generic 명칭으로 대체.
TL;DR — 다섯 주제 한눈에
| # | 주제 | 결과 | 핵심 배움 |
|---|---|---|---|
| 1 | Fluent Bit → Logstash → ES 파이프라인 전환 | ✅ ECK Logstash CRD 로 가동 | 시행착오 4번 = ssl_verification_mode/readinessProbe/case-sensitive 매핑/dest 인덱스명 충돌 |
| 2 | Kibana 대시보드 6개 + GitOps 자동 import | ✅ NDJSON 단일 소스, helm Job 으로 import | UI 편집 → export → git push 워크플로우가 진실 소스 |
| 3 | ES 고급 검색·집계 (ILM + 템플릿 + Transform) | ✅ raw 98k events → summary 401 rows | transform role 노드 필요. data_stream 충돌 회피 위해 dest 이름 분리 |
| 4 | etcd raft leader 안정화 (lemuel → ilwon) | ✅ 매일 04:00 cron 으로 자동 재고정 | scheduler 메시지의 “Insufficient memory” 는 거짓말일 수 있음. node 라벨도 거짓말일 수 있음 |
| 5 | ECK operator crash loop 디버깅 (못 고침) | 🟡 부분 완화만, 영향 무해 판정 | “고치지 못함” 도 정확한 진단이면 valid한 결론 |
전체 변경: helm-deploy 레포에 commit 9개, blog/ops 자산 4개 파일 신설.
1. Fluent Bit → Logstash → ES 파이프라인 전환
1.1 왜 Logstash 를 추가했나
기존 구성:
Fluent Bit (DaemonSet, 5 노드) ──► Elasticsearch (직접 HTTP)
Fluent Bit 의 [OUTPUT] Name es 가 ES 에 곧장 적재. 빠르고 단순한데 변환·라우팅이 약하다 — grok 패턴 같은 정규화 DSL 이 없고 conditional output 도 빈약.
학습 목표가 “Logstash 를 활용하여 로그를 수집, 변환, 적재” 였으니 중간에 Logstash 를 끼워 넣는 게 본 과제.
1.2 새 구조
Fluent Bit (DaemonSet)
│ HTTP POST json_lines
▼
Logstash (ECK Logstash CRD, 1 replica)
│ filter: grok / mutate / translate / drop
│ output: ES bulk
▼
Elasticsearch (별도 인덱스 패턴 logstash-k8s-*)
기존 logs-k8s-* 는 그대로 보존해 ILM 으로 자연 만료 — 호환 충격 제로.
1.3 ECK Logstash CRD 의 깔끔함
ECK 2.10+ 부터 kind: Logstash CRD 가 추가됐다. ES, Kibana 와 같은 패턴.
apiVersion: logstash.k8s.elastic.co/v1alpha1
kind: Logstash
metadata:
name: logs
namespace: logging
spec:
count: 1
version: "8.16.1"
elasticsearchRefs:
- name: logs # 같은 ns 의 Elasticsearch CR 이름
clusterName: logs # ECK 가 env 주입 시 prefix 로 사용 → ${LOGS_ES_HOSTS}
pipelines:
- pipeline.id: main
config.string: |
input { http { port => 8080 codec => json } }
filter { ... }
output { elasticsearch { hosts => ["${LOGS_ES_HOSTS}"] ... } }
elasticsearchRefs 만 선언하면 ECK 가 자동으로 SA·인증서·env vars(<CLUSTER>_ES_HOSTS, _ES_USER, _ES_PASSWORD, _ES_SSL_CERTIFICATE_AUTHORITY) 를 주입. 사용자가 인증 정보 손댈 필요 없음.
1.4 시행착오 4번 (commit 단위)
GitOps 레포에 다음 4 개 commit 으로 기록됨. 각자 별도의 함정.
(a) ssl_verification_mode => "certificate" — invalid value
Logstash ES output 의 SSL 검증 모드는 [full|none] 둘만 허용. "certificate" 는 ES 자체 옵션이라 헷갈렸음.
[ERROR] Invalid setting for elasticsearch output plugin:
Expected one of ["full", "none"], got ["certificate"]
→ "full" 로 수정. 그러면 hostname + CA 검증 둘 다 활성화.
(b) Readiness probe 가 HTTP input 포트를 HTTPS 로 친다
ECK 가 자동 생성하는 readiness probe 가 우리 service 의 첫 port (8080) 에 HTTPS GET 시도. 우리는 plain HTTP input → probe 영원히 실패 → pod Ready=false → service endpoint 등록 안 됨 → Fluent Bit “no upstream connections available”.
podTemplate:
spec:
containers:
- name: logstash
readinessProbe:
tcpSocket:
port: 9600 # Logstash API 포트 (TCP 만 확인)
→ 명시적으로 override.
(c) level 매핑 case-sensitive
Logstash translate 필터 사전:
dictionary => { "INFO" => "30", "ERROR" => "50", ... }
근데 Spring Boot 는 INFO 대문자 보내고 klog/controller 들은 info 소문자 보냄. case-sensitive 매칭이라 소문자는 fallback 0 으로 떨어짐.
→ mutate { uppercase => [ "level" ] } 로 정규화 후 매핑. WARNING (klog 가 사용) 도 dictionary 에 추가.
(d) 출력 인덱스명이 기존 data_stream 패턴과 충돌
처음엔 logstash-k8s-YYYY.MM.dd 로 적재하려 했더니 기존 logs-k8s 인덱스 템플릿(data_stream 강제) 패턴 logs-*-* 에 매칭돼 “Could not create destination index” 오류.
→ destination 이름을 k8s-events-summary 로 분리. raw 데이터는 logstash-k8s-*, summary 는 k8s-events-summary — pattern 충돌 회피.
1.5 데이터 평탄화 + 노이즈 drop 필터
filter {
# 1) k8s 메타 평탄화 — Kibana 검색 편하게
if [kubernetes] {
mutate {
add_field => {
"[k8s_namespace]" => "%{[kubernetes][namespace_name]}"
"[k8s_pod]" => "%{[kubernetes][pod_name]}"
"[k8s_container]" => "%{[kubernetes][container_name]}"
}
}
}
# 2) level → severity 숫자 (case-insensitive)
if [level] {
mutate { uppercase => [ "level" ] }
translate {
source => "[level]"
target => "[level_num]"
dictionary => {
"TRACE" => "10" "DEBUG" => "20" "INFO" => "30"
"WARN" => "40" "WARNING" => "40"
"ERROR" => "50" "FATAL" => "60"
}
fallback => "0"
}
mutate { convert => { "[level_num]" => "integer" } }
}
# 3) Exception class 추출 — 알림·검색 편하게
if [level] in ["ERROR", "FATAL"] or [message] =~ "Exception|Traceback" {
grok {
match => { "message" => "(?<exception_first>%{JAVACLASS:exception_class}(:\s*%{DATA:exception_msg})?)" }
tag_on_failure => []
}
}
# 4) 시끄러운 헬스체크 drop
if [message] =~ "/actuator/health" or [message] =~ "/metrics" {
drop {}
}
}
1.6 결과 (적용 30분 후)
- Logstash pipeline
events.in=378, out=376, filtered=376— 정상 흐름 - ES 인덱스
logstash-k8s-YYYY.MM.dd자동 생성, 301 docs 첫 5분 - 샘플 문서 top-level:
@timestamp, cluster_id, k8s_namespace, k8s_pod, k8s_container, level, level_num, received_at, ...
Fluent Bit 의 record_modifier 가 박은 cluster_id 도 보존됨 — Logstash 가 의미 있는 변환만 추가하고 원본은 깨끗하게 유지.
2. Kibana 대시보드 6개 + GitOps 자동 import
2.1 0개에서 6개로
Logstash 가 적재만 잘 해도 Kibana 에 대시보드가 없으면 “지나가는 로그” 일 뿐. 운영 가시화의 핵심은 시각화.
구성한 6 패널 (모두 Lens 시각화):
| 패널 | 타입 | 핵심 인사이트 |
|---|---|---|
| Total Events (24h) | Metric | 24시간 누적 적재량 — 트래픽 sanity check |
| Severity distribution | Donut | INFO/WARN/ERROR 비율 — 정상성 |
| Log volume over time | Area | 시간별 볼륨 — 트래픽 패턴 |
| Events per Namespace top 10 | Bar | 가장 시끄러운 ns — 라우팅 후보 |
| ERROR + FATAL over time | Line | severity ≥ 50 추이 — incident 감지 |
| Top error pods 15 | DataTable | 에러 많은 (ns, pod) — 즉시 조치 대상 |
2.2 GitOps 자동 import
수동 클릭으로 만들면 일회용. 핵심 패턴 — Kibana 의 NDJSON export 파일을 git 에 넣고 매 sync 마다 import.
charts/elk-cluster/
├── dashboards/
│ ├── README.md ← 업데이트 절차
│ └── k8s-logs-overview.ndjson ← 진실 소스
└── templates/
└── dashboards-importer.yaml ← ConfigMap + Job
중요 디테일:
.Files.Glob "dashboards/*.ndjson"로 차트가 모든 NDJSON 자동 발견 — 새 대시보드 추가 시 코드 수정 0- Job 이 Kibana
_import?overwrite=true호출 —overwrite=true가 GitOps 핵심. UI 변경은 다음 sync 때 원복 --form file=@$f로 multipart 업로드 (Kibana saved-objects API 의 표준)
워크플로우 (UI 편집 → Git push):
- Kibana UI 에서 자유롭게 편집
- Stack Management → Saved Objects → 체크 → Export (
Include related objectsON) - 받은 NDJSON 으로
dashboards/k8s-logs-overview.ndjson덮어씀 git push→ ArgoCD sync → Job 재실행 → 1분 내 적용
이게 정착되면 “디자이너처럼 UI 에서 만들고 코드처럼 git 에 저장한다” 라는 깔끔한 흐름이 됨.
3. ES 고급 검색·집계 — ILM + 컴포넌트/인덱스 템플릿 + Transform
3.1 dynamic mapping 의 위험
처음엔 logstash-k8s-YYYY.MM.dd 가 dynamic mapping 으로 그냥 생성됐다. ES 가 알아서 추론하는 건 편하지만 단점이 3개:
- 숫자 vs 텍스트 추론 실수 —
level_num이long(8 byte) 으로 잡힐 수 있음.byte(1 byte) 면 충분. - 모든 keyword 가 multi-field (
text + .keyword) — 메모리 낭비. 우리는 aggregation 만 필요. - ILM 미적용 — 인덱스가 무한정 쌓이고 hot/warm/cold 라우팅 안 됨.
→ 명시 매핑 + ILM 정책을 한 번에 적용해야 한다.
3.2 ILM 정책 — hot 7d → warm 14d → cold 30d → delete 60d
{
"policy": {
"phases": {
"hot": { "min_age": "0ms", "actions": {
"rollover": { "max_age": "7d", "max_primary_shard_size": "30gb" },
"set_priority": { "priority": 100 }
}},
"warm": { "min_age": "7d", "actions": {
"set_priority": { "priority": 50 },
"allocate": { "include": { "_tier_preference": "data_warm" } },
"forcemerge": { "max_num_segments": 1 }
}},
"cold": { "min_age": "30d", "actions": {
"set_priority": { "priority": 10 },
"allocate": { "include": { "_tier_preference": "data_cold" } }
}},
"delete": { "min_age": "60d", "actions": { "delete": {} } }
}
}
}
핵심 효과:
- 7일 이후
forcemerge로 세그먼트 1 개로 합쳐 검색 속도 ↑ - 30일 이후 cold 노드 (큰 HDD) 로 이전 → 비싼 NVMe 공간 회수
- 60일 자동 삭제 — 디스크 무한증가 방지
3.3 Transform — continuous aggregation
raw 데이터 (수백만 docs/day) 를 매 분 집계해 요약 인덱스 유지. 대시보드/알림이 raw 안 스캔.
{
"source": { "index": ["logstash-k8s-*"] },
"dest": { "index": "k8s-events-summary" },
"frequency": "1m",
"sync": { "time": { "field": "@timestamp", "delay": "60s" } },
"pivot": {
"group_by": {
"hour": { "date_histogram": { "field": "@timestamp", "calendar_interval": "1h" } },
"k8s_namespace": { "terms": { "field": "k8s_namespace.keyword" } },
"level": { "terms": { "field": "level.keyword", "missing_bucket": true } }
},
"aggregations": {
"events": { "value_count": { "field": "@timestamp" } },
"max_severity": { "max": { "field": "level_num" } }
}
}
}
1차 검증: raw 98,583 events → summary 401 rows. 244배 압축. “namespace 별 1시간당 ERROR” 쿼리가 사실상 즉답.
3.4 시행착오 — Transform 의 함정
(a) transform role 누락
처음 Transform PUT 시:
"Transform requires the transform node role for at least 1 node, found no transform nodes"
ES 노드 role 을 명시적으로 선언했더니 (master, data_hot, data_content, ingest, remote_cluster_client) transform 이 빠져있었음. → hot 노드에 transform 추가.
(하지만 ECK 가 클러스터 yellow 상태에선 hot 노드 재시작 안 함 — if_yellow_only_restart_upgrading_nodes_with_unassigned_replicas 안전 predicate. 강제 재시작 필요했음.)
(b) k8s_namespace text 필드에 terms 집계 → fielddata 에러
Fielddata is disabled on [k8s_namespace] in [logstash-k8s-2026.05.17].
Text fields are not optimised for operations that require per-document field data...
기존 dynamic mapping 으로 만들어진 인덱스는 k8s_namespace 가 multi-field text + keyword. Transform 쿼리에서 field: "k8s_namespace" (text) 가 아니라 field: "k8s_namespace.keyword" 명시해야 함. → 수정 + 새 컴포넌트 템플릿도 multi-field 로 통일해 이전·이후 호환.
(c) dest 인덱스명이 다른 템플릿에 매칭됨
logs-k8s-summary 라는 destination 이름 → 기존 logs-k8s 인덱스 템플릿(data_stream 강제) 에 매칭 → Transform 이 일반 인덱스로 생성 못함. → k8s-events-summary 로 변경.
3.5 운영자가 알아두면 좋은 명령
GET logstash-k8s-*/_ilm/explain
# 각 인덱스의 phase, 다음 액션, 남은 시간
GET _transform/logs-k8s-daily-summary/_stats
# state, operations_behind, documents_processed/indexed
operations_behind 가 계속 늘면 transform 이 못 따라가는 중 — search 속도가 ingestion 보다 느린 상태. 알람 걸기 좋은 지표.
4. etcd raft leader 안정화 — 부하는 가벼운 노드로
4.1 발견 — 4코어 노드 load5=3.96
평소엔 잠잠하던 control-plane 노드 하나가 load average 4 수준 (4코어 = 100% 포화) 으로 치솟고 있었음. ELK + settlement migration + ArgoCD sync 가 동시에 돈 게 트리거였지만, 진짜 원인은 더 깊이 있었음.
4.2 잘못된 길 — “디스크가 HDD 라서”
처음 진단: “이 노드 라벨에 storage-tier=hdd 박혀있네. etcd 가 HDD 위에서 도니까 fsync 가 느려서 그렇겠지” — 그럴듯한 가설이라 사용자도 동의.
근데 노드 들어가서 확인했더니:
$ cat /sys/block/sda/queue/rotational
0 # 0 = SSD, 1 = HDD
$ # node-exporter info
node_disk_info{model="SAMSUNG_MZNLN512", ...} # Samsung SATA SSD
라벨이 거짓말이었다. 노드는 SATA SSD. 라벨 hdd 는 옛날에 누가 잘못 박은 잔재. fsync 가 진짜 느린 게 아니었음.
운영 인사이트: 노드 라벨은 메타데이터이지 진실이 아니다. 의심되면 OS 레벨에서 확인하자.
4.3 진짜 원인 — CPU 4코어 + etcd raft leader + kubeconfig 기본 endpoint
세 가지가 겹친 결과:
- CPU 4코어 — 5 노드 중 가장 약함. 다른 control-plane 은 8/12 코어.
- etcd raft leader — 25일째 무중단 가동이라 leader 였음. 모든 write proposal 이 여기로 집중.
- kubeconfig 기본 endpoint —
~/.kube/config가 이 노드를 가리킴 → kubectl/helm/ArgoCD 모든 트래픽이 여기로 우선.
4.4 해결 단계
Step 1 — kubeconfig endpoint 전환
cp ~/.kube/config ~/.kube/config.bak.$(date +%Y%m%d-%H%M%S)
sed -i.tmp 's|server: https://<weak-node-ip>:6443|server: https://<beefy-node-ip>:6443|' ~/.kube/config
cert SAN 에 3개 마스터 IP 모두 포함돼있어 TLS 문제 없음. load5 3.96 → 1.88 (즉시 절반).
Step 2 — etcd leader 이전
먼저 ilwon 에 etcd-client 설치 (alpine 패키지) — k3s 는 etcdctl 번들 안 함. 그 다음:
# 현재 leader 확인
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \
--cert=/var/lib/rancher/k3s/server/tls/etcd/server-client.crt \
--key=/var/lib/rancher/k3s/server/tls/etcd/server-client.key \
endpoint status --cluster -w table
# leader 이전 — target 이 이미 leader 면 no-op 으로 성공
etcdctl ... move-leader <target-member-id>
Leadership transferred from X to Y — RAFT TERM 한 단계 증가. load5 1.88 → 1.82.
Step 3 — 매일 04:00 KST cron 으로 재고정
leader 는 노드 재부팅·네트워크 단절·CPU spike 시 재선거됨. 한 번 옮겨도 다음 재선거 때 약한 노드로 돌아갈 수 있음. 매일 자동 재고정:
apiVersion: batch/v1
kind: CronJob
metadata:
name: etcd-leader-pin
spec:
schedule: "0 4 * * *"
timeZone: Asia/Seoul
jobTemplate:
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: <beefy-node> # etcd 인증서가 거기 있음
securityContext: { runAsUser: 0 } # hostPath cert read
containers:
- name: pinner
image: alpine:3.20
command: [/bin/sh, -c]
args:
- |
apk add --no-cache --quiet curl
ETCD_VER=v3.5.16
curl -fsSL "https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz" | tar xz
install -m 0755 etcd-${ETCD_VER}-linux-amd64/etcdctl /usr/local/bin/etcdctl
EP="--endpoints=https://<cp-1>:2379,https://<cp-2>:2379,https://<cp-3>:2379"
CERTS="--cacert=/etcd-certs/server-ca.crt ..."
TARGET=$(ETCDCTL_API=3 etcdctl $EP $CERTS member list \
| grep -E ', <beefy-node>-' | awk -F, '{gsub(/ /,"",$1); print $1}')
ETCDCTL_API=3 etcdctl $EP $CERTS move-leader "$TARGET"
volumeMounts:
- { name: etcd-certs, mountPath: /etcd-certs, readOnly: true }
volumes:
- name: etcd-certs
hostPath: { path: /var/lib/rancher/k3s/server/tls/etcd, type: Directory }
핵심 트릭:
move-leader는 target 이 이미 leader 면 no-op (Leadership transferred from X to X) 으로 성공 응답 → 매일 무조건 실행해도 안전- member ID 를 hardcode 안 함 —
member list에서 prefix grep 으로 동적 조회 (노드 재설치 시 ID 바뀜) - 가벼운 alpine 에 런타임 etcdctl 다운로드 — bitnami/etcd 는 archived, quay.io/coreos/etcd 는 distroless 라 sh 없음 (가장 깔끔한 차선)
4.5 보너스 — WiFi 위 etcd 의 함정
다음날 확인했더니 leader 가 또 바뀜. 이번엔 제 3 의 control-plane 노드가 leader. 그 노드는 VXLAN 이 WiFi 위에서 도는 노드:
ip -d link show flannel.1
# vxlan id 1 local <ip> dev wlp3s0b1 ... ← WiFi 인터페이스
WiFi 위 etcd leader → API write latency 폭증 → ECK operator 같은 lease-renewal 의존 컨트롤러가 줄줄이 crash. 제 4 의 노드 (가벼운 노드) 부하 spike 의 일등 공신.
운영 인사이트: 홈랩에서도 etcd 멤버는 유선 권장. WiFi VXLAN 은 패킷 손실 → heartbeat 미스 → 재선거 → 도미노.
5. ECK Operator Crash Loop — 못 고친 이야기
5.1 증상
10시간 동안 ECK operator pod 가 97번 재시작. 평균 6~9분 주기.
elastic-operator-0 0/1 CrashLoopBackOff 97 (6m14s ago) 10h
5.2 root cause 의심
operator 로그:
E0517 12:08:51.343 leaderelection.go:436] error retrieving resource lock
elastic-system/elastic-operator-leader:
client rate limiter Wait returned an error: context deadline exceeded
{"log.level":"error","message":"Failed to start the controller manager",
"error":"leader election lost", ...}
Error: leader election lost
client rate limiter — client-go 의 QPS 제한기에서 막힘. lease 갱신 요청이 줄을 못 뚫음.
5.3 시도 3가지 — 다 부분 효과
시도 1: chart values config: 에 leader-elect-* 추가
config:
leader-elect-lease-duration: 120s
leader-elect-renew-deadline: 90s
leader-elect-retry-period: 30s
→ chart 가 이 key 인식 안 함. ECK helm chart 2.16.1 의 config: 는 camelCase 키만 받고, leader election timing 은 노출 안 함. ConfigMap rendered output 에서 내 키들이 사라짐.
시도 2: StatefulSet args 에 CLI flag 직접 patch
kubectl patch statefulset elastic-operator --type=json -p='[{
"op":"replace","path":"/spec/template/spec/containers/0/args",
"value":["manager","--config=/conf/eck.yaml",
"--leader-elect-lease-duration=120s",
"--leader-elect-renew-deadline=90s",
"--leader-elect-retry-period=30s"]
}]'
→ ECK 가 Error: unknown flag: --leader-elect-lease-duration. manager --help 확인해보니 ECK 2.16.1 에 lease-duration 플래그 자체가 없었음. 하드코딩.
시도 3: --kube-client-qps=200 + --kube-client-timeout=3m
이건 ECK 가 받는 플래그. QPS 기본은 client-go 의 5/burst 10 — 너무 작음. 200 으로 올리고 timeout 도 1m → 3m.
→ 부분 효과만. crash 주기 6~9분 → 5분. 완치 아님.
5.4 한계 인정
ECK 2.16.1 의 lease duration 은 소스 코드에 하드코딩 되어있어 운영자가 못 바꿈. 진짜 fix 는:
- ECK 2.18+ 업그레이드 (lease duration 노출됐을 수 있음)
- single control-plane 으로 마이그레이션 (etcd 경쟁 사라짐)
둘 다 30분~2시간 작업 + 위험 중-상.
5.5 영향 재평가 — 무해 판정
여기서 잠깐 멈추고 실제 영향을 측정:
| 컴포넌트 | crash 의 영향 |
|---|---|
| ES 데이터 적재 | ✅ 영향 0 (operator 죽어도 ES pod 들은 계속 동작) |
| Kibana 검색 | ✅ 영향 0 |
| Logstash 파이프라인 | ✅ 영향 0 |
| 새 CR 변경 적용 | 🟡 6~9분 지연 (operator 재시작 동안) |
| 클러스터 부하 | 🟡 leader 노드 가끔 spike |
→ 데이터 영향 0. 운영 영향만 살짝. lemuel 부하는 etcd leader 이전 + cron 으로 흡수됨.
5.6 결정 — 그냥 둠
“고치지 못함” 도 정확한 진단이면 valid 한 결론이다. 영향이 무해하고, 진짜 fix 의 비용/위험이 effect 보다 크면 알면서 두는 것 이 합리적.
운영 일지에 기록 ✓, 다음 ECK 메이저 업그레이드 시 자연 fix 기대.
시니어 엔지니어링 정수: 모든 문제를 다 고치려 들지 말 것. 어떤 문제를 안 고치기로 결정했는지 가 더 중요한 시그널.
마무리 — 다섯 주제의 공통 패턴
이 다섯 주제를 관통하는 패턴이 있다.
-
“표준 설정의 가정” 의심 — Logstash readiness probe 가 HTTP input 포트 HTTPS, ECK default lease 15s, dynamic ES mapping 의 multi-field. 모두 “보통 케이스” 에 맞춘 디폴트이고, 우리 케이스에선 깨짐.
-
메타데이터(라벨/이름) ≠ 진실 — 노드 라벨
storage-tier=hdd가 거짓. 인덱스 패턴logs-*가 다른 템플릿에 충돌. 항상 source-of-truth 까지 내려가서 확인. -
“부분 효과” 도 효과 — ECK crash 주기 9분 → 5분이 만족스럽진 않지만 진짜 fix 비용 대비 합리적이면 거기서 멈추는 게 맞음. 완벽주의 vs 실용주의의 균형.
-
GitOps 가 진실 소스 — 손 패치는 ignoreDifferences 짝꿍 필수 — ECK args 패치를 ArgoCD selfHeal 이 되돌리지 못하게
ignoreDifferences등록. 임시 fix 도 GitOps 원칙을 깨지 않게. -
자동화는 멱등(idempotent) 으로 — etcd leader pin cron 은 매일 무조건 실행 (이미 leader 면 no-op). ES advanced setup Job 은 ‘already exists’ 응답을 OK 처리. 재실행 안전한 자동화가 운영 비용을 0 에 수렴시킴.
TL;DR — 하루치 ELK 학습이 5주제 짜리 운영 회고로 진화. 핵심은 시행착오를 빠르게 commit 단위로 기록 하고 못 고친 것도 정확히 진단하는 것. K3s 홈랩이라도 “왜 안 됐는지” 를 추적하는 근육은 그대로 키울 수 있다.
부록 — 이 하루의 helm-deploy 변경 (commit 9개)
feat(elk): Logstash 도입 — Fluent Bit → Logstash(HTTP:8080) → ES
fix(elk): Logstash ssl_verification_mode certificate→full
fix(elk): Logstash readiness probe TCP:9600
fix(elk): Logstash level 매핑 case-insensitive
feat(elk): Kibana 'K8s Logs - Operational Overview' 대시보드 GitOps
feat(elk): ES 고급 셋업 — ILM + 인덱스 템플릿 + Transform
fix(elk): hot 노드에 transform role 추가
fix(elk): Transform .keyword 사용 + dest 인덱스명 충돌 회피
feat(cluster-ops): etcd leader 매일 ilwon 으로 재고정 CronJob
기록을 commit 단위로 남기면 나중의 나 가 똑같은 함정 만났을 때 5분 안에 솔루션 찾는다.