들어가며
네트워크 정책을 강제하기 시작하면 곧바로 새로운 질문이 쏟아집니다. "방금 결제 API가 왜 끊겼지?", "이 드롭은 정책 때문인가 라우팅 때문인가?", "두 클러스터에 걸친 서비스는 어떻게 페일오버하지?" 데이터패스가 커널 안으로 들어간 만큼, 관측 수단도 같은 깊이에서 제공되어야 합니다.
Hubble은 Cilium의 eBPF 데이터패스에서 직접 플로우를 추출하는 관측성 계층이고, ClusterMesh는 여러 클러스터의 데이터패스를 하나의 메시로 묶는 멀티클러스터 계층입니다. 둘 다 별도 에이전트나 사이드카 없이 기존 Cilium 위에서 동작한다는 점이 핵심입니다. 이 글에서는 Hubble의 아키텍처와 실전 쿼리, 메트릭/알림 구성, 플로우 장기 보관, 그리고 ClusterMesh의 구성과 글로벌 서비스, 운영 이슈, 서비스 메시와의 관계까지 다룹니다.
Hubble 아키텍처
+------------------- 노드 1 -------------------+
| [eBPF 데이터패스] |
| | perf ring buffer (플로우 이벤트) |
| v |
| [cilium-agent 내 Hubble 서버] |
| - 노드 로컬 링 버퍼에 플로우 보관 |
| - gRPC API (unix socket / 4244) |
+------------------+---------------------------+
|
+------------------- 노드 2 ... N --------------+
| (동일 구조) | |
+------------------+---------------------------+
|
v (모든 노드의 4244로 연결)
[hubble-relay] :4245
- 클러스터 전체 플로우를 단일 API로 집계
|
+----------+-----------+
v v
[hubble CLI] [hubble-ui]
(관제/디버깅) (서비스 맵 시각화)
구조에서 도출되는 운영 특성 세 가지가 중요합니다.
1. **플로우는 노드 로컬 링 버퍼에 잠시만 존재합니다.** 기본 설정에서 노드당 일정 개수(기본 4095개)의 플로우만 메모리에 유지되므로, 장기 분석이 필요하면 반드시 export를 구성해야 합니다.
2. **relay는 집계자일 뿐 저장소가 아닙니다.** relay가 죽어도 데이터패스나 노드 로컬 관측에는 영향이 없습니다.
3. **플로우 데이터 모델**은 L3/L4/L7 메타데이터(소스/목적지 identity와 라벨, verdict, 드롭 사유, HTTP/DNS 정보)를 담은 구조화 이벤트입니다. 패킷 페이로드는 저장하지 않습니다.
hubble CLI 실전
설치는 cilium-cli와 같은 채널로 배포되며, 로컬에서 relay로 포트포워딩해 사용하는 패턴이 일반적입니다.
cilium hubble enable --ui # Hubble + relay + UI 활성화
cilium hubble port-forward & # localhost:4245 로 relay 연결
hubble status # 플로우 수신 상태 확인
드롭 추적 — 가장 자주 쓰는 쿼리
클러스터 전체에서 드롭된 플로우 실시간 관찰
hubble observe --verdict DROPPED -f
특정 네임스페이스의 드롭 + 사유까지
hubble observe --verdict DROPPED --namespace payments \
--output json | jq -r '.flow.drop_reason_desc' | sort | uniq -c
정책 거부만 골라보기 (드롭 사유 필터)
hubble observe --verdict DROPPED --drop-reason-desc POLICY_DENIED
특정 파드에서 나가는 트래픽 중 거부된 것
hubble observe --from-pod payments/pg-gateway --verdict DROPPED
특정 두 워크로드 사이의 모든 플로우 (방향 무관)
hubble observe --pod shop/frontend --pod shop/backend
서비스 간 의존성 파악
order-api가 호출하는 대상 정리 (egress 의존성)
hubble observe --from-pod shop/order-api --type trace:to-endpoint \
--output json | jq -r '.flow.destination.namespace + "/" + (.flow.destination.labels | join(","))' \
| sort | uniq -c | sort -rn
DNS 질의 내역 (어떤 외부 도메인을 찾는가)
hubble observe --from-namespace payments --protocol dns \
--output json | jq -r '.flow.l7.dns.query' | sort | uniq -c
L7 HTTP 관찰 (L7 정책 또는 가시성 어노테이션 필요)
hubble observe --namespace shop --protocol http \
--output json | jq -r '[.flow.l7.http.method, .flow.l7.http.url, (.flow.l7.http.code|tostring)] | join(" ")'
특정 시간 범위 + 라벨 셀렉터 조합
hubble observe --since 30m --label app=checkout --verdict FORWARDED
verdict 종류는 FORWARDED(허용), DROPPED(차단), AUDIT(감사 모드에서 차단됐을 트래픽), ERROR 등이 있고, 정책 도입 단계에서는 AUDIT 필터가 핵심 도구가 됩니다.
Hubble UI — 서비스 맵
hubble-ui는 네임스페이스 단위 서비스 맵을 그려 줍니다. 워크로드 간 화살표(L4/L7 구분), 실패 비율, DNS 대상까지 표시되므로 "정책을 짜기 전 현재 통신 구조 파악"과 "장애 시 어느 구간이 끊겼는지 즉시 확인"에 유용합니다. 다만 UI는 실시간 뷰 중심이므로, 사후 분석은 뒤에서 다룰 export 데이터로 해야 합니다.
메트릭 — Prometheus 연동
Hubble은 플로우에서 파생한 메트릭을 Prometheus 형식으로 노출합니다. helm 값으로 어떤 메트릭을 생성할지 선택합니다.
hubble:
enabled: true
metrics:
enableOpenMetrics: true
enabled:
- dns:query;labelsContext=source_namespace,destination_namespace
- drop:labelsContext=source_namespace,destination_namespace;sourceContext=workload-name
- tcp
- flow
- port-distribution
- httpV2:exemplars=true;labelsContext=source_namespace,source_workload,destination_namespace,destination_workload
HTTP 골든 시그널
httpV2 메트릭으로 서비스별 요청률, 에러율, 레이턴시 분위수를 코드 수정 없이 얻습니다.
요청률 (RPS)
sum(rate(hubble_http_requests_total{destination_namespace="shop"}[5m]))
by (destination_workload)
5xx 에러율
sum(rate(hubble_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(hubble_http_requests_total[5m]))
p99 레이턴시
histogram_quantile(0.99,
sum(rate(hubble_http_request_duration_seconds_bucket[5m])) by (le, destination_workload))
정책 드롭 알림 룰
groups:
- name: cilium-policy
rules:
- alert: PolicyDropSpike
expr: |
sum(rate(hubble_drop_total{reason="POLICY_DENIED"}[5m]))
by (source_namespace, destination_namespace) > 1
for: 10m
labels:
severity: warning
annotations:
summary: 'Policy drops between namespaces exceeded threshold'
- alert: CiliumAgentDown
expr: up{job="cilium-agent"} == 0
for: 5m
labels:
severity: critical
정책 강제 직후에는 드롭 알림 임계값을 일부러 낮게 잡아 누락된 허용 규칙을 빨리 찾고, 안정화 후 운영 수준으로 올리는 2단계 운영을 권합니다.
플로우 로그 장기 보관 — export와 SIEM 연동
링 버퍼는 분 단위로 회전하므로, 감사 증적과 사후 분석에는 파일 export가 필수입니다.
hubble:
export:
fileMaxSizeMb: 50
fileMaxBackups: 10
dynamic:
enabled: true
config:
content:
- name: security-events
filePath: /var/run/cilium/hubble/security.log
includeFilters:
- verdict: ["DROPPED", "AUDIT"]
- name: dns-all
filePath: /var/run/cilium/hubble/dns.log
includeFilters:
- protocol: ["dns"]
Hubble의 export 파일은 줄 단위 JSON이므로 표준 로그 파이프라인에 바로 태웁니다.
노드: hubble export 파일 (JSON lines)
-> 로그 수집기 (Fluent Bit / Vector / OTel Collector)
-> 버퍼 (Kafka 등, 선택)
-> 저장/분석 (Elasticsearch, Loki, S3+Athena, SIEM)
SIEM 연동 시 유용한 매핑: verdict와 drop_reason_desc는 보안 이벤트 분류로, source/destination의 identity 라벨은 자산 태깅으로, l7.dns.query는 위협 인텔(악성 도메인 대조) 입력으로 사용합니다. 전체 플로우를 다 보내면 비용이 폭발하므로, 위 예시처럼 보안 관련(DROPPED/AUDIT/DNS)만 선별 export하는 것이 일반적입니다.
ClusterMesh 아키텍처
ClusterMesh는 복수의 Cilium 클러스터를 묶어 클러스터 간 서비스 디스커버리, 로드밸런싱, 정책을 제공합니다.
+--------- 클러스터 A (id=1) ----------+ +--------- 클러스터 B (id=2) ----------+
| | | |
| [cilium-agent x N] | | [cilium-agent x N] |
| | (읽기) | | | (읽기) |
| v | | v |
| [clustermesh-apiserver] <----------+------+--- 에이전트가 상대 클러스터의 |
| - 자기 클러스터의 서비스/identity/ | | apiserver에 연결해 상태를 watch |
| 엔드포인트를 etcd 형태로 노출 | | |
| - LoadBalancer/NodePort로 노출 | | [clustermesh-apiserver] |
| | | |
| PodCIDR: 10.1.0.0/16 (중복 금지) | | PodCIDR: 10.2.0.0/16 (중복 금지) |
+--------------------------------------+ +--------------------------------------+
동기화되는 것: 서비스(글로벌), identity, ipcache(원격 파드 IP -> identity)
동기화 후: 파드 간 트래픽은 양 클러스터 데이터패스가 직접 라우팅 (중간 게이트웨이 없음)
설계상 중요한 점:
- **identity가 클러스터 경계를 넘어 의미를 유지합니다.** 클러스터 B의 app=backend 파드 identity가 클러스터 A의 ipcache에도 전파되므로, 멀티클러스터 정책이 같은 모델로 동작합니다.
- **데이터 평면에 추가 홉이 없습니다.** 컨트롤 플레인(clustermesh-apiserver)만 추가되고, 패킷은 기존 라우팅 모드(터널/네이티브)로 직접 흐릅니다.
- **컨트롤 플레인 장애는 신규 변경 전파만 막습니다.** apiserver가 죽어도 이미 동기화된 서비스/identity로 트래픽은 계속 흐릅니다.
ClusterMesh 구성 실전
전제 조건
- 모든 클러스터의 **PodCIDR/노드 IP가 겹치지 않아야** 합니다(가장 흔한 설계 실수).
- 모든 클러스터가 **같은 CA**로 발급된 인증서를 써야 합니다(mTLS 상호 신뢰).
- 클러스터 간 노드끼리 직접 통신 가능해야 합니다(VPC 피어링, 전용선, VPN 등).
- 클러스터마다 고유한 name과 id(1~255)가 필요합니다.
클러스터 A helm values
cluster:
name: cluster-a
id: 1
clustermesh:
useAPIServer: true
apiserver:
service:
type: LoadBalancer
cilium-cli로 연결
1) 클러스터 A의 CA를 클러스터 B에 복사 (같은 CA 공유)
kubectl --context cluster-a -n kube-system get secret cilium-ca -o yaml \
| kubectl --context cluster-b apply -f -
2) 양쪽 ClusterMesh 활성화
cilium clustermesh enable --context cluster-a
cilium clustermesh enable --context cluster-b
3) 상호 연결 (양방향 피어링 자동 구성)
cilium clustermesh connect --context cluster-a --destination-context cluster-b
4) 상태 및 연결성 검증
cilium clustermesh status --context cluster-a --wait
cilium connectivity test --context cluster-a --multi-cluster cluster-b
글로벌 서비스 — 어피니티와 페일오버
같은 이름/네임스페이스의 서비스에 어노테이션을 붙이면 두 클러스터의 백엔드가 하나의 가상 서비스로 합쳐집니다.
양쪽 클러스터에 동일하게 배포
apiVersion: v1
kind: Service
metadata:
name: checkout
namespace: shop
annotations:
service.cilium.io/global: "true"
service.cilium.io/affinity: "local" # 로컬 백엔드 우선, 없으면 원격
spec:
selector:
app: checkout
ports:
- port: 80
targetPort: 8080
| 어노테이션 | 값 | 동작 |
| --- | --- | --- |
| service.cilium.io/global | true | 클러스터 간 백엔드 합산 |
| service.cilium.io/affinity | local | 로컬 정상 백엔드 우선, 전멸 시 원격으로 |
| service.cilium.io/affinity | remote | 원격 우선(드레인/카나리 시나리오) |
| service.cilium.io/affinity | none | 전 클러스터 균등 분산 |
| service.cilium.io/global-sync-endpoint-slices | true | 원격 엔드포인트를 EndpointSlice로 동기화 |
affinity=local 구성이 사실상의 표준입니다. 평상시에는 클러스터 내부에서 처리해 레이턴시를 아끼고, 로컬 백엔드가 전부 unhealthy가 되면 자동으로 원격 클러스터로 넘어가는 페일오버를 얻습니다. 이때 "unhealthy 판정"은 readiness 기반이므로, 애플리케이션 readinessProbe가 정확해야 페일오버도 정확합니다.
멀티클러스터 정책
CNP에서 클러스터를 조건으로 쓸 수 있습니다.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-cross-cluster-checkout
namespace: shop
spec:
endpointSelector:
matchLabels:
app: checkout-db
ingress:
- fromEndpoints:
- matchLabels:
app: checkout
io.cilium.k8s.policy.cluster: cluster-a # 특정 클러스터에서만
클러스터 라벨을 지정하지 않으면 양쪽 클러스터의 app=checkout 모두에 매칭됩니다. "DB는 자기 클러스터의 앱에서만 접근 가능"처럼 클러스터 경계를 정책으로 강제하고 싶을 때 cluster 라벨이 필요합니다.
암호화 — WireGuard와 IPsec
클러스터 간 트래픽이 신뢰할 수 없는 네트워크를 지난다면 전송 암호화를 켭니다. 같은 기능이 클러스터 내부 노드 간에도 적용됩니다.
| 항목 | WireGuard | IPsec |
| --- | --- | --- |
| 설정 난이도 | 매우 낮음(키 자동 관리) | 키 생성/로테이션 직접 관리 |
| 커널 요구 | 5.6 이상 | 광범위 지원 |
| 성능 | 일반적으로 우수, 멀티큐 활용 | 알고리즘/하드웨어 가속 의존 |
| 키 로테이션 | 자동 | 수동 절차(시크릿 교체) 필요 |
| 규제 적합성 | 알고리즘 고정(ChaCha20) | FIPS 요구 환경에서 선호 |
WireGuard 활성화 (helm)
helm upgrade cilium cilium/cilium -n kube-system \
--reuse-values --set encryption.enabled=true --set encryption.type=wireguard
암호화 상태 확인
kubectl -n kube-system exec ds/cilium -- cilium encrypt status
특별한 규제 요구(FIPS 등)가 없다면 WireGuard가 운영상 압도적으로 단순합니다. 암호화 오버헤드만큼 MTU 계산을 다시 해야 한다는 점을 잊지 마세요.
운영 이슈 — 클러스터 추가/제거, 버전 스큐, 모니터링
클러스터 추가/제거
추가: CA 공유 -> enable -> connect (앞 절차와 동일)
제거: 글로벌 서비스 의존부터 정리해야 함
1) 제거 대상 클러스터로의 affinity/트래픽을 먼저 배제
2) 연결 해제
cilium clustermesh disconnect --context cluster-a --destination-context cluster-c
3) 남은 클러스터에서 상태 확인
cilium clustermesh status --context cluster-a
제거 시 글로벌 서비스의 원격 백엔드가 사라지므로, affinity=none으로 원격에 의존하던 서비스가 있는지 먼저 점검해야 합니다.
버전 스큐
ClusterMesh로 연결된 클러스터 간 Cilium 버전은 공식적으로 마이너 1단계 차이까지만 지원됩니다. 업그레이드는 "한 클러스터씩, 전 클러스터가 같은 버전이 될 때까지 다음 마이너로 넘어가지 않기"가 원칙입니다. 쿠버네티스 버전 스큐와는 별개 축이므로 둘 다 매트릭스로 관리해야 합니다.
연결 모니터링
메시 상태 핵심 명령
cilium clustermesh status --context cluster-a
에이전트 관점의 원격 클러스터 동기화 상태
kubectl -n kube-system exec ds/cilium -- cilium status --verbose | grep -A10 ClusterMesh
Prometheus에서는 clustermesh 관련 에이전트 메트릭(원격 클러스터 준비 상태, 동기화 큐)과 함께, 글로벌 서비스의 원격 백엔드 수가 0이 되는 상황을 알림으로 잡아 두면 "조용한 메시 단절"을 빨리 발견할 수 있습니다.
서비스 메시와의 관계 — Istio와 겹침과 차이
"Cilium이 있으면 Istio가 필요 없나?"는 가장 자주 받는 질문입니다. 겹치는 부분과 다른 부분을 분리해서 봐야 합니다.
| 능력 | Cilium (+Hubble/ClusterMesh) | Istio |
| --- | --- | --- |
| L3/L4 정책, 네트워크 격리 | 핵심 영역 | 부차적 |
| L7 정책 (HTTP 경로/메서드) | 가능 (Envoy 선별 경유) | 가능 (전 트래픽 프록시) |
| mTLS (워크로드 간 암호화) | 전송 계층(WireGuard/IPsec) 중심 | 애플리케이션 계층 mTLS + ID 증명 |
| 트래픽 분할 (카나리 가중치) | 제한적 | 핵심 영역 (VirtualService) |
| 재시도/타임아웃/서킷브레이커 | 미제공 | 핵심 영역 |
| 멀티클러스터 서비스 | ClusterMesh | 멀티 프라이머리/리모트 구성 |
| 관측성 | 네트워크 중심(플로우, verdict) | 요청 중심(트레이싱, 앱 메트릭) |
| 오버헤드 | 낮음(커널 내 처리 위주) | 프록시 경유 비용(ambient로 절감 가능) |
실무 정리: **네트워크 보안/격리/관측이 목적이면 Cilium만으로 충분한 경우가 많고**, 카나리 배포·요청 단위 재시도·세밀한 트래픽 제어가 필요하면 서비스 메시를 위에 얹습니다. 실제로 Cilium을 CNI로 깔고 그 위에 Istio ambient를 올리는 조합도 흔하며, 이때 양쪽의 mTLS/L7 기능이 중복되지 않도록 역할 분담(예: 암호화는 WireGuard, L7 라우팅은 Istio)을 명시적으로 정해야 합니다.
운영 체크리스트
- [ ] Hubble relay/UI가 활성화되어 있고 hubble status가 정상인가
- [ ] DROPPED/AUDIT verdict 기반 알림이 구성되어 있는가
- [ ] HTTP 골든 시그널 메트릭(httpV2)이 대시보드에 연결되어 있는가
- [ ] 보안 이벤트(드롭/DNS) export와 장기 보관 파이프라인이 있는가
- [ ] 전 클러스터 PodCIDR/노드 IP 대역이 겹치지 않음을 문서로 확인했는가
- [ ] 클러스터 간 같은 CA 공유가 구성되어 있는가
- [ ] cluster id(1~255)와 name이 전 클러스터에서 고유한가
- [ ] 글로벌 서비스의 affinity 전략(local 권장)과 readinessProbe가 점검되었는가
- [ ] 클러스터 간 버전 스큐가 마이너 1단계 이내로 관리되는가
- [ ] clustermesh status와 원격 백엔드 수가 모니터링에 연결되어 있는가
- [ ] 클러스터 제거 절차(트래픽 배제 → disconnect)가 런북에 있는가
- [ ] 암호화(WireGuard/IPsec) 선택과 MTU 재계산이 완료되었는가
마치며
Hubble과 ClusterMesh는 별개 기능처럼 보이지만, 둘 다 "identity가 커널까지 내려간 데이터패스"라는 같은 기반에서 나옵니다. 플로우에 identity가 붙어 있으니 관측이 의미를 갖고, identity가 클러스터를 넘어 동기화되니 멀티클러스터 정책이 단일 클러스터와 같은 모델로 동작합니다. 운영 관점의 결론은 단순합니다. 정책을 강제하기 전에 Hubble을 먼저 켜고, 클러스터를 늘리기 전에 CIDR과 CA 설계를 먼저 끝내라는 것입니다. 이 순서를 지키면 Cilium 스택은 단일 클러스터에서 멀티클러스터까지 같은 운영 모델로 확장됩니다.
참고 자료
- Hubble 공식 문서: https://docs.cilium.io/en/stable/observability/hubble/
- Hubble 메트릭 문서: https://docs.cilium.io/en/stable/observability/metrics/
- ClusterMesh 공식 문서: https://docs.cilium.io/en/stable/network/clustermesh/clustermesh/
- Cilium 암호화(WireGuard/IPsec) 문서: https://docs.cilium.io/en/stable/security/network/encryption/
- Hubble GitHub 저장소: https://github.com/cilium/hubble
- Prometheus 공식 문서: https://prometheus.io/docs/
- Grafana 공식 문서: https://grafana.com/docs/
- WireGuard 공식 사이트: https://www.wireguard.com/
- IPsec 보안 아키텍처 RFC 4301: https://datatracker.ietf.org/doc/html/rfc4301
- Istio 공식 문서: https://istio.io/latest/docs/
- OpenTelemetry 공식 문서: https://opentelemetry.io/docs/
- Kubernetes 멀티클러스터 서비스 API (KEP-1645): https://kubernetes.io/blog/2021/04/15/multi-cluster-services-api/
현재 단락 (1/240)
네트워크 정책을 강제하기 시작하면 곧바로 새로운 질문이 쏟아집니다. "방금 결제 API가 왜 끊겼지?", "이 드롭은 정책 때문인가 라우팅 때문인가?", "두 클러스터에 걸친 서비...