앱 밑에 깔린 19개 — 홈랩 K3s 운영 기반의 사용법과 실측 성능
우리 K3s 클러스터(노드 5대)에는 사람이 직접 쓰는 앱 말고도 앱을 돌리기 위한 네임스페이스가 19개 있다. 배포(ArgoCD), 외부 연결(cloudflared), 시크릿(sops-operator), 백업(velero), 빌드(buildkitd·ARC·registry-mirror), 관측(Prometheus·ELK·Falco)과 그 밖의 공용 부품(Kafka·OpenViking·NFS·Dashboard·echo)이다.
이 글은 각각을 어떻게 쓰는지와 지금 실제로 어떤 성능·비용을 내는지를 정리한다. 수치는 모두 2026-10-16 00:10~00:27 KST 에 클러스터에서 직접 잰 값이다. 바로 전날 isagal 노드를 퇴역시킨 직후의 상태다 (isagal 퇴역 글).
먼저 결론 세 가지.
- 운영 기반이 클러스터 메모리의 3분의 1을 쓴다. 그 가운데 절반 이상이 로깅(ELK) 하나다.
- 잘 돌던 것 2개는 사실 일을 안 하고 있었다. registry-mirror 는 141일째 떠 있는데 어느 노드도 그걸 미러로 쓰지 않는다. velero 는 10/13 에 의도적으로 멈춰 지금 클러스터 백업이 0개다.
- 버전이 낡은 곳이 두 군데 있다. Kubernetes Dashboard 는 업스트림이 아카이브됐고, Prometheus 는 아직 2.x 다.
0. 한눈에 보기
kubectl top pods 를 네임스페이스별로 합산한 값이다(순간값).
| 네임스페이스 | 핵심 구성 (버전) | CPU (m) | 메모리 (Mi) |
|---|---|---|---|
| logging | Elasticsearch·Kibana·Logstash 8.16.1, Fluent Bit 5.0.5 | 145 | 9,982 |
| monitoring | Prometheus v2.55.0, Grafana 11.2.2, Alertmanager v0.27.0, Tempo 2.7.1, OTel Collector 0.156.0 | 134 | 2,267 |
| kafka | Strimzi 0.51.0, Kafka 4.2.0 (KRaft) | 165 | 1,736 |
| argocd | Argo CD v3.4.1, Image Updater v0.16.0 | 149 | 804 |
| openviking | OpenViking v0.3.12 + Ollama | 8 | 547 |
| falco | Falco 0.45.0 ×5, Falcosidekick 2.32.0 | 272 | 521 |
| velero | Velero v1.18.1 (+ node-agent ×5) | 19 | 299 |
| ghstats · elastic-system · analytics-prod · nfs-server · arc-system · lemuel-monitor · sops-operator · cloudflared · kubernetes-dashboard · build · echo-webhook · registry-mirror | (아래 본문) | 64 | 489 |
| 합계 | 952 | 16,645 |
같은 시각 노드 5대의 메모리 사용 합계는 50,250Mi 였다. 그러므로
\[\frac{16{,}645}{50{,}250} \approx 33\%,\qquad \frac{9{,}982}{50{,}250} \approx 20\%\]즉 운영 기반이 클러스터 메모리의 약 3분의 1을 쓰고, 로깅 하나가 5분의 1을 쓴다. CPU 쪽은 합쳐도 1코어가 안 되므로 이 클러스터에서 운영 기반의 비용은 거의 전부 메모리다.
1. 배포와 연결
ArgoCD — git 이 곧 클러스터
사용법. 모든 차트와 매니페스트는 helm-deploy 리포에 있다. root-app 이 argocd-applications/ 폴더를 보고 있다가
그 안의 Application 파일 개수만큼 앱을 만든다. 이른바 App of Apps 패턴이다.
automated.prune 과 selfHeal 이 켜져 있어서 다음처럼 동작한다.
- 파일을 지우고 push 하면 Application 이 사라진다.
- 손으로
kubectl set env같은 변경을 하면 90초 안에 되돌아간다.
다만 우리 Application 에는 finalizer 가 없다. 그래서 Application 이 지워져도 그 아래 리소스까지 함께 지워지지는(cascade) 않는다. 어제 network-agent 를 내릴 때도 네임스페이스는 따로 지웠다.
# 강제 재조정
kubectl -n argocd annotate application root-app argocd.argoproj.io/refresh=hard --overwrite
# 전체 상태 집계
kubectl -n argocd get applications -o jsonpath='{range .items[*]}{.status.sync.status}/{.status.health.status}{"\n"}{end}' | sort | uniq -c
실측.
- 앱 73개가 전부 Synced/Healthy 다.
- 파드 8개가 합쳐 CPU 149m, 메모리 804Mi 를 쓴다.
- 컨트롤러·repo-server 가 모두 louise 한 노드에 있다. 공식 HA 매니페스트(Argo CD HA 문서)와 달리 단일 레플리카 구성이다.
한계도 있다. ArgoCD 메트릭(argocd_app_reconcile_*)은 Prometheus 가 수집하고 있지 않다. 그래서 재조정 지연 시간은 이번에 잴 수 없었다.
그리고 “Healthy” 가 “쓰이고 있다”는 뜻은 아니다. 파드가 0개인 앱도 Healthy 로 뜬다.
cloudflared — 포트 하나 안 여는 공개 도메인
사용법. 집 공유기에는 열린 포트가 없다. 클러스터 안의 cloudflared 가 Cloudflare 엣지로 나가는 연결을 맺어 두고,
*.lemuel.co.kr 요청을 그 연결을 거꾸로 타고 받아 들인다(Cloudflare Tunnel 문서).
라우팅 규칙은 대시보드 원격 설정(remote config)에 있어서, 바꿀 때는 API 로 config 를 PUT 한다.
- 규칙 목록의 마지막 줄은 항상
http_status:404catch-all 이어야 한다.
실측. --metrics 0.0.0.0:2000 의 Prometheus 엔드포인트(메트릭 문서)에서 읽었다.
- 리소스: 레플리카 1개, CPU 2m, 메모리 32Mi.
- 엣지 연결:
ha_connections=4, 서울 엣지(icn01·icn06 등)에 붙어 있다. - 프로세스 가동 577.5시간 동안 응답 252,987건. 평균하면 시간당 약 438건(\(252{,}987 / 577.5 \approx 438\))이다.
| 상태 코드 | 건수 | 비율 |
|---|---|---|
| 2xx | 223,637 | 88.4% |
| 3xx | 11,081 | 4.4% |
| 4xx | 18,243 | 7.2% |
| 5xx | 26 | 0.01% |
5xx 26건은 내부 서비스가 내린 응답을 cloudflared 가 그대로 전달한 것이다.
터널 자체 오류(cloudflared_tunnel_request_errors)는 따로 2,116건이 집계됐다. 원점(origin)에 닿지 못한 요청들인데, 이번에는 원인별로 나눠 보지 않았다.
sops-operator — git 에 넣어도 되는 시크릿
사용법. 시크릿은 SOPS + age 로 값만 암호화한 YAML 로 git 에 올린다.
클러스터 안의 sops-secrets-operator 가 SopsSecret CR 을 복호화해 일반 Secret 으로 만든다.
그래서 ArgoCD 는 평문을 한 번도 보지 않는다.
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
sops -d secrets/foo.sops.yaml # 확인
sops -e --filename-override secrets/foo.sops.yaml plain.yaml > secrets/foo.sops.yaml
어제 겪은 함정이 하나 있다. 여러 문서가 --- 로 이어진 sops 파일은 MAC 이 파일 전체에 걸려 있다.
그래서 문서 하나를 sed 로 잘라내면 복호화가 MAC mismatch 로 실패한다. 반드시 복호화 → 편집 → 재암호화 순서로 해야 한다.
실측. SopsSecret 70개를 CPU 2m, 메모리 37Mi 로 처리한다. 운영 기반 가운데 비용 대비 효과가 가장 크다.
2. 빌드
ARC (Actions Runner Controller) — 0까지 줄어드는 러너
사용법. GitHub Actions 잡이 들어올 때만 러너 파드를 띄우는 스케일 셋 방식이다(GitHub ARC 문서).
워크플로에서 runs-on: watchman-arc 처럼 스케일 셋 이름을 지정하면 된다.
실측.
- 컨트롤러 0.14.2 와 리스너 2개(합계 3m, 47Mi)만 상주한다.
- 스케일 셋은
watchman-arc(최소 0, 최대 1)와xr-arc(최소 0, 최대 2) 두 개다. - 측정 시점의 러너 수는 0개였다. 잡이 없을 때 러너가 차지하는 메모리는 0이다.
나머지 리포의 러너는 클러스터 밖, louise 호스트의 self-hosted 러너가 맡는다. 이쪽은 메모리가 약한 노드를 보호하려고 전역 뮤텍스로 한 번에 잡 하나만 돌린다.
buildkitd — 클러스터 안 이미지 빌드
사용법. rootless BuildKit v0.20.2 을 solomon 에 하나 띄워 두고, buildctl --addr tcp://... 로 원격 빌드한다.
rootless 라 특권 컨테이너가 필요 없다.
실측. 대기 상태에서 CPU 4m, 메모리 19Mi 다. 빌드할 때의 피크는 이번 측정 범위 밖이다.
registry-mirror — 141일 동안 아무도 안 썼다
사용법(의도). CNCF Distribution(registry:2.8.3)을 proxy.remoteurl: https://ghcr.io 로 띄운 pull-through 캐시다(Distribution 문서).
처음 받은 이미지를 보관해 두었다가 두 번째부터는 로컬에서 내주려는 용도다.
실측.
- 파드 나이는 141일이다.
- 그런데 저장소 크기는 4.0K 다.
- 지난 30일 로그에서 kube-probe 를 뺀 요청은 9줄뿐이다.
원인은 노드 쪽에 있다. K3s 는 각 노드의 /etc/rancher/k3s/registries.yaml 에 mirrors: 항목이 있어야 그 미러로 받는다(K3s 문서).
그런데 노드 5대 모두에 mirrors 항목이 없었다. 미러 서버는 떠 있지만, 그걸 미러로 쓰라고 설정된 노드가 하나도 없다.
비용은 CPU 1m, 메모리 12Mi 로 미미하다. 하지만 “떠 있으니 쓰이고 있겠지”라는 착각을 만든다는 점이 진짜 비용이다.
선택지는 둘이다. 노드 5대에 mirrors."ghcr.io".endpoint 를 넣고 k3s 를 재시작하거나, 내리거나.
3. 백업 — velero 는 지금 멈춰 있다
사용법. Velero 스케줄 두 개가 정의돼 있다.
daily-with-volumes: 매일 03:00, 보관 30일, 전체 네임스페이스 + 볼륨(node-agent 의 파일시스템 백업/Kopia).hourly-critical: 이름과 달리 4시간마다(0 */4 * * *), 보관 7일. 정산 DB·asat·argocd·n8n 이 대상이다.
저장 위치는 Cloudflare R2(S3 호환)다.
실측.
- 백업 오브젝트가 0개다.
- 두 스케줄 모두
spec.paused: true이고, 컨트롤러 로그에는 1분마다schedule ... is paused, skip이 찍힌다. - 마지막 성공은 10/13 이다.
사고는 아니다. 10/13 커밋에서 일부러 멈춘 것이다. 포트폴리오용 홈랩이라 유료 오프사이트 백업은 필요 없다고 판단했고, 같은 날 R2 에 있던 기존 백업도 지웠다.
Velero 문서도 paused 를 “새 백업을 만들지 않는 상태”로 정의한다(Schedule API).
그래도 결과는 결과다. 지금 이 클러스터에서 PV 가 망가지면 되돌릴 사본이 없다. 이건 클러스터를 연구용으로 두고 장애를 감수한다는 결정의 연장선이다.
멈추기 전 7일간의 소요 시간은 히스토그램 버킷으로 보간한 중앙값으로만 남아 있다.
- daily 약 1,350초
- hourly-critical 약 393초
버킷 경계로 보간한 추정치라 정밀한 값은 아니다.
멈춘 상태인데도 리소스는 쓰고 있다. 상주하는 node-agent 5개와 서버 1개가 합쳐 CPU 19m, 메모리 299Mi 를 쓴다.
4. 관측
Prometheus 스택 (kube-prometheus-stack)
사용법. kube-prometheus-stack 차트로 Prometheus·Alertmanager·Grafana·kube-state-metrics·node-exporter 를 한 번에 띄운다.
수집 대상은 ServiceMonitor 로 선언한다. 같은 네임스페이스에 OTel Collector(Claude Code 텔레메트리 수신)와 Tempo(트레이스)도 있다.
kubectl -n monitoring port-forward svc/kps-prometheus 9090
curl -s --get localhost:9090/api/v1/query --data-urlencode 'query=count(up==0)'
실측.
| 지표 | 값 |
|---|---|
| 스크레이프 대상 | 67개, 다운 0 |
| 헤드 시계열 수 | 286,175 |
| 수집 속도 | 초당 약 7,717 샘플 |
| 블록 저장 크기 | 14.2GB (보관 15일 / 상한 25GB) |
/api/v1/query p50 |
약 50ms (24시간, 버킷 보간) |
/api/v1/query_range p90 |
약 90ms (24시간, 버킷 보간) |
| 리소스 (네임스페이스 합) | CPU 134m, 메모리 2,267Mi |
공식 스토리지 문서는 --storage.tsdb.retention.time(시간 기준)과 retention.size(크기 기준) 가운데 먼저 걸리는 쪽이 적용된다고 설명한다.
지금은 14.2GB 로 25GB 상한까지 여유가 있다. 따라서 실제로 적용되는 건 15일 보관 쪽이다.
버전은 v2.55.0 이다. Prometheus 3.0 은 2024년 11월에 나왔고(3.0 발표) 새 UI 와 UTF-8 메트릭 이름 등이 기본값으로 바뀌었다. 메이저 버전 업그레이드라 따로 계획이 필요하다.
ELK — 메모리의 5분의 1
사용법.
- 수집: Fluent Bit DaemonSet 이 노드마다 컨테이너 로그를 읽는다.
- 가공·저장: Logstash 가 가공해 Elasticsearch 로 보낸다. ES 는 ECK 오퍼레이터 2.16.1 가 관리한다.
- 보관: ILM 으로
logstash-k8s-*인덱스를 7일 뒤 삭제한다. - 조회: Kibana.
- 알림:
log-error-alerterCronJob 이 에러 급증을 텔레그램으로 알린다.
설계 배경은 로깅 거시·미시 글에 따로 정리했다.
실측.
| 지표 | 값 |
|---|---|
| 클러스터 상태 | green, 노드 1, 샤드 85, 미할당 0 |
| 문서 수 / 저장 크기 | 12,760,858 / 5.28GB |
일일 유입 (logstash-k8s-*) |
하루 136만~186만 건, 570~893MB |
| 평균 색인 시간 | 문서당 약 0.25ms |
| 평균 검색 시간 | 쿼리당 약 0.40ms (29,595건) |
| ES 파드 리소스 | 요청·한도 모두 메모리 6Gi |
| 리소스 (네임스페이스 합) | CPU 145m, 메모리 9,982Mi |
색인·검색 평균은 _stats 의 누적값을 나눈 것이다.
이 값은 ES 내부 처리 시간이다. Fluent Bit 에서 Kibana 에 보이기까지의 종단 간 지연은 아니다. 실제 검색량은 하루에 많지 않다. 그런데 메모리는 6Gi 를 상시 예약한다. 이 클러스터에서 가장 비싼 상주 비용은 “거의 안 보는 로그를 언제든 검색할 수 있게 해 두는 것”이다.
Falco — 커널에서 본 이상 행동
사용법. Falco 0.45.0 이 노드마다 커널 이벤트(syscall)를 규칙과 대조하고, 걸린 이벤트를 Falcosidekick 이 메트릭과 알림으로 내보낸다. 파수꾼(watchman)은 일부러 규칙에 걸리는 카나리 파드를 띄워서 탐지 경로가 살아 있는지 확인한다.
실측. sidekick 기동(10/4 16:13 UTC) 이후 약 11일간 이벤트는 1,687건이다(warning 1,499, notice 188).
| 규칙 | 건수 |
|---|---|
| Read sensitive file untrusted | 1,305 |
| BPF filter attached to socket (BPFDoor pattern) | 166 |
| Contact K8S API Server From Container | 146 |
| Packet socket created on host | 36 |
| Clear Log Activities | 27 |
| Terminal shell in container | 4 |
상위 항목에는 watchman-falco-canary-* 파드 이름이 찍힌 것이 많다. 일부러 낸 이벤트라는 뜻이다.
Contact K8S API 는 API 서버를 부르는 것이 본업인 파드에서도 나오므로 건수만으로 위협이라 볼 수 없다.
비용은 노드당 CPU 0.03~0.17코어로, 네임스페이스 합계 272m 다. 운영 기반 가운데 CPU 를 가장 많이 쓴다. syscall 을 전수 검사하니 당연한 결과다.
작은 것들
- lemuel-monitor — 노드마다 도는 alpine 텔레그램 감시 봇 4개. 합계 33m, 43Mi.
- analytics-prod — 분석용 PostgreSQL 16 단일 인스턴스. 4m, 62Mi.
- ghstats — GitHub README 통계 카드를 자체 호스팅. 1m, 70Mi.
5. 그 밖의 공용 부품
Kafka (Strimzi)
사용법. Strimzi 0.51.0 오퍼레이터가 Kafka 4.2.0 을 KRaft 모드로 띄운다.
ZooKeeper 가 없고, 컨트롤러와 브로커를 겸하는 dual-role 노드 1개짜리 구성이다.
토픽과 사용자도 KafkaTopic/KafkaUser CR 로 git 에 선언한다. 정산 서비스의 Transactional Outbox 이벤트가 주된 사용처다.
실측.
- 내부 토픽을 뺀 토픽 144개, 컨슈머 그룹 16개.
- 복제 계수 1 이다. 브로커가 하나뿐이라 그 노드가 죽으면 그동안 Kafka 전체가 멈춘다.
- 리소스: CPU 165m, 메모리 1,736Mi (브로커 요청 1Gi / 한도 2Gi).
- 브로커 처리량 메트릭(
kafka_server_brokertopicmetrics_*)은 수집되고 있지 않아 메시지 처리량은 잴 수 없었다.
OpenViking + Ollama
봇들의 장기 기억(메모리 파일 470개)을 검색하는 컨텍스트 DB(OpenViking) v0.3.12 와 로컬 임베딩용 Ollama 다. 두 개가 louise 에서 합계 8m, 547Mi 를 쓴다. LLM 호출은 클러스터 안의 LiteLLM 을 경유한다.
nfs-server · Kubernetes Dashboard · echo-webhook
- nfs-server — 여러 파드가 같은 디렉터리를 쓸 때(RWX)를 위한 alpine NFS. 6m, 54Mi.
- Kubernetes Dashboard v2.7.0 — 3m, 30Mi. 업스트림 프로젝트가 아카이브됐다. 공식 문서와 쿠버네티스 블로그가 Headlamp 로 옮길 것을 권한다(K8s 문서, 전환 안내 블로그). 보안 패치가 더 나오지 않는 웹 UI 를 계속 띄워 둘 이유는 약하다.
- echo-webhook — 받은 요청을 그대로 돌려주는 http-https-echo. 웹훅 디버깅용이다. 0m, 16Mi.
6. 정리 — 무엇을 할 것인가
| 항목 | 현재 상태 | 제안 |
|---|---|---|
| registry-mirror | 141일째 아무도 사용하지 않음 (4K) | 노드 registries.yaml 에 미러 연결, 또는 내림 |
| velero | 의도적 정지, 백업 0개 | 결정 유지. 단 상주 비용(299Mi)과 “백업 없음”을 인지할 것 |
| Kubernetes Dashboard | 업스트림 아카이브 | Headlamp 로 교체, 또는 내림 |
| Prometheus 2.55 | 3.x 미전환 | 업그레이드 별도 계획 |
| ArgoCD·Kafka 메트릭 | 수집 안 됨 | ServiceMonitor 추가 (재조정 지연·처리량 확보) |
| ELK | 메모리 20% | 7일 보관과 6Gi 가 실제 검색량에 맞는지 재검토 |
운영 기반의 비용은 거의 전부 메모리다. 상위 세 개(ELK 9.98Gi, Prometheus 스택 2.27Gi, Kafka 1.74Gi)가 운영 기반 메모리의 약 84%를 차지한다. 반대로 sops-operator·cloudflared·ARC 처럼 수십 Mi 로 큰 역할을 하는 부품도 있다. 성능 문제보다 “떠 있지만 일을 안 하는 것”을 찾는 쪽이 더 큰 수확이었다.
측정 방법과 한계
- 리소스는
kubectl top(metrics-server)의 순간값이다. 피크나 평균이 아니다. - p50·p90 은 Prometheus 히스토그램의
histogram_quantile결과다. 버킷 경계 사이를 선형 보간한 추정치다. - cloudflared·ES·Falco 수치는 각 프로세스의 기동 이후 누적 카운터다. 기간이 서로 다르다(577시간, 인덱스 보관 7일, 약 11일).
- ArgoCD 재조정 지연, Kafka 처리량, buildkitd 빌드 피크는 메트릭이 없거나 측정 범위 밖이라 싣지 않았다.
- 컴포넌트 사이의 성능 비교(예: ELK 대 Loki)는 이 글의 범위가 아니다. 같은 하드웨어에서 맞붙여 잰 중립 벤치마크도 없다.