Skip to content

Split View: eBPF 기반 제로 계측 Kubernetes 옵저버빌리티: Cilium Hubble과 Grafana Beyla 실전 가이드

✨ Learn with Quiz
|

eBPF 기반 제로 계측 Kubernetes 옵저버빌리티: Cilium Hubble과 Grafana Beyla 실전 가이드

eBPF Observability

들어가며: 왜 eBPF 기반 제로 계측인가

Kubernetes 환경에서 옵저버빌리티를 확보하기 위해 전통적으로 사용되어 온 방식은 크게 두 가지다. 첫째, 애플리케이션 코드에 SDK를 삽입하여 메트릭과 트레이스를 생성하는 명시적 계측(Explicit Instrumentation) 방식이다. 둘째, Envoy 기반 사이드카 프록시를 각 Pod에 주입하여 L7 트래픽을 관측하는 서비스 메시 방식이다. 두 방식 모두 프로덕션 환경에서 검증된 접근이지만 근본적인 한계가 존재한다.

SDK 기반 계측은 모든 서비스에 라이브러리를 추가하고 계측 코드를 작성해야 한다. 수백 개의 마이크로서비스가 동작하는 대규모 클러스터에서 이를 일관되게 유지하는 것은 막대한 엔지니어링 비용을 수반한다. 사이드카 프록시 방식은 코드 변경 없이 L7 가시성을 확보할 수 있지만, Pod당 추가 컨테이너가 배포되면서 노드당 메모리 사용량이 수백 MB씩 증가하고, 네트워크 홉이 추가되어 P99 레이턴시가 2~5ms 상승하는 것이 일반적이다.

eBPF(extended Berkeley Packet Filter)는 이 두 가지 방식의 한계를 동시에 해결한다. 리눅스 커널 내부에서 샌드박스화된 프로그램을 실행하여, 애플리케이션 코드 변경 없이 커널 레벨에서 네트워크 흐름, HTTP/gRPC 요청, DNS 쿼리, TCP 연결 상태를 관측할 수 있다. 사이드카 컨테이너가 필요 없으므로 리소스 오버헤드가 극히 낮으며, 커널에서 직접 데이터를 수집하기 때문에 애플리케이션의 성능에 거의 영향을 미치지 않는다.

이 글에서는 eBPF 기반 옵저버빌리티의 핵심 도구인 Cilium Hubble과 Grafana Beyla를 중심으로, 제로 코드 변경으로 Kubernetes 클러스터에 네트워크 가시성과 애플리케이션 성능 모니터링(APM)을 구축하는 방법을 실전 운영 관점에서 상세히 다룬다.

eBPF 기술 개요: 커널 레벨 계측의 원리

eBPF 프로그램의 실행 흐름

eBPF는 리눅스 커널 4.x부터 본격적으로 도입된 기술로, 커널 모듈을 직접 수정하지 않고도 커널 공간에서 프로그램을 실행할 수 있게 한다. eBPF 프로그램은 사용자 공간에서 작성되어 커널의 Verifier를 통과한 후 JIT(Just-In-Time) 컴파일러에 의해 네이티브 코드로 변환된다. Verifier는 무한 루프 방지, 메모리 범위 초과 접근 차단, 허용된 헬퍼 함수만 호출 가능하도록 보장하여 커널 안정성을 유지한다.

eBPF 프로그램이 부착(attach)될 수 있는 훅 포인트는 매우 다양하다. 네트워크 관련으로는 TC(Traffic Control), XDP(eXpress Data Path), socket 레벨 훅이 있고, 시스템 전반의 관측을 위해서는 kprobes(커널 함수 진입/종료), tracepoints(미리 정의된 관측 지점), uprobes(사용자 공간 함수 추적)가 활용된다. Cilium Hubble은 주로 TC와 socket 레벨 훅을 사용하여 네트워크 흐름을 관측하고, Grafana Beyla는 uprobes와 kprobes를 활용하여 HTTP/gRPC 요청의 RED(Rate, Error, Duration) 메트릭을 자동으로 추출한다.

커널 버전별 eBPF 기능 지원

eBPF 기능은 커널 버전에 따라 지원 범위가 달라진다. Cilium Hubble의 완전한 기능을 활용하려면 최소 커널 4.19 이상이 필요하며, Grafana Beyla의 자동 계측 기능은 커널 5.8 이상에서 안정적으로 동작한다. 프로덕션 환경에서는 커널 5.15 LTS 이상을 권장한다. 특히 BTF(BPF Type Format) 지원이 활성화된 커널이어야 CO-RE(Compile Once, Run Everywhere) 방식으로 eBPF 프로그램을 배포할 수 있어 다양한 노드 환경에서의 호환성이 보장된다.

# 현재 노드의 커널 버전 및 BTF 지원 여부 확인
uname -r
# 출력 예: 5.15.0-91-generic

# BTF 지원 확인
ls /sys/kernel/btf/vmlinux
# 파일이 존재하면 BTF 지원 활성화 상태

# eBPF 기능 프로브 (bpftool 사용)
bpftool feature probe kernel | grep -E "map_type|program_type|helper"

# Kubernetes 노드의 커널 버전 일괄 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.kernelVersion}{"\n"}{end}'

Cilium Hubble 아키텍처: 네트워크 옵저버빌리티의 핵심

Hubble의 내부 구조

Cilium Hubble은 Cilium CNI의 옵저버빌리티 레이어로, 세 가지 핵심 컴포넌트로 구성된다. 첫째, 각 노드에서 동작하는 Hubble Server가 Cilium Agent에 내장되어 eBPF 데이터플레인에서 네트워크 이벤트를 수집한다. 둘째, Hubble Relay가 클러스터 전체의 Hubble Server로부터 이벤트를 집계하여 단일 API 엔드포인트로 제공한다. 셋째, Hubble UI가 서비스 맵과 네트워크 흐름을 시각적으로 표현한다.

Hubble이 관측할 수 있는 네트워크 이벤트의 범위는 다음과 같다. L3/L4 레벨에서는 TCP/UDP/ICMP 패킷의 소스/목적지 IP, 포트, 프로토콜 정보와 함께 패킷 드롭 사유를 제공한다. L7 레벨에서는 HTTP 요청/응답의 메서드, 경로, 상태 코드, DNS 쿼리와 응답의 도메인, 타입, 응답 코드, 그리고 Kafka 메시지의 토픽과 API 키를 파싱한다. 이 모든 정보가 Kubernetes의 Identity(네임스페이스, Pod, 서비스 레이블)와 자동으로 매핑되어 서비스 간 통신 패턴을 명확하게 파악할 수 있다.

서비스 맵과 플로우 가시성

Hubble의 가장 강력한 기능 중 하나는 자동 생성되는 서비스 맵이다. 전통적인 서비스 메시에서는 Envoy 사이드카가 각 Pod의 인바운드/아웃바운드 트래픽을 프록시하면서 서비스 토폴로지를 구성하지만, Hubble은 커널의 eBPF 프로그램이 수집한 네트워크 흐름 데이터로부터 서비스 간 의존성 그래프를 자동으로 구축한다. 이를 통해 서비스 메시를 도입하지 않고도 어떤 서비스가 어떤 서비스와 통신하는지, 통신 성공률과 지연 시간은 어떤지를 실시간으로 확인할 수 있다.

Hubble 설치와 설정: Helm Chart 기반 배포

Cilium + Hubble 통합 설치

Hubble은 Cilium CNI의 일부로 배포된다. 이미 Cilium을 사용하고 있다면 Hubble 기능을 활성화하기만 하면 되고, 새로 구축하는 클러스터라면 Cilium 설치 시 Hubble을 함께 활성화한다. 아래는 프로덕션 환경에 적합한 Helm values 설정이다.

# cilium-hubble-values.yaml
# Cilium + Hubble 프로덕션 배포를 위한 Helm values
hubble:
  enabled: true
  relay:
    enabled: true
    replicas: 3 # 고가용성을 위한 Relay 복제본
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
    retryTimeout: 30s
    sortBufferLenMax: 5000
    sortBufferDrainTimeout: 1s
  ui:
    enabled: true
    replicas: 2
    ingress:
      enabled: true
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt-prod
      hosts:
        - hubble.internal.example.com
      tls:
        - secretName: hubble-ui-tls
          hosts:
            - hubble.internal.example.com
  metrics:
    enableOpenMetrics: true
    enabled:
      - dns
      - drop
      - tcp
      - flow
      - port-distribution
      - icmp
      - httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction
    serviceMonitor:
      enabled: true # Prometheus Operator ServiceMonitor 자동 생성
      interval: 15s
  tls:
    enabled: true # Hubble Server-Relay 간 mTLS 활성화
    auto:
      enabled: true
      method: certmanager
      certManagerIssuerRef:
        group: cert-manager.io
        kind: ClusterIssuer
        name: hubble-issuer

# Cilium Agent 최적화 설정
operator:
  replicas: 2
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

# 모니터 버퍼 크기 (노드당 이벤트 캐시)
bpf:
  monitorAggregation: medium # none/low/medium/high - 집계 수준
  monitorInterval: 5s
  monitorFlags: all
  mapDynamicSizeRatio: 0.0025
# Cilium + Hubble 설치 (Helm)
helm repo add cilium https://helm.cilium.io
helm repo update

helm upgrade --install cilium cilium/cilium \
  --version 1.16.5 \
  --namespace kube-system \
  --values cilium-hubble-values.yaml \
  --wait

# 설치 검증
cilium status --wait
cilium hubble port-forward &

# Hubble CLI로 클러스터 전체 흐름 확인
hubble observe --since 1m --output compact

# 특정 네임스페이스의 HTTP 트래픽만 필터링
hubble observe \
  --namespace production \
  --protocol http \
  --http-status 500-599 \
  --output json | jq '{
    source: .flow.source.labels,
    destination: .flow.destination.labels,
    http: .flow.l7.http
  }'

# 서비스 간 드롭 패킷 분석
hubble observe \
  --verdict DROPPED \
  --namespace production \
  --output table

Hubble 메트릭 기반 PromQL 쿼리

Hubble이 생성하는 메트릭을 Prometheus로 수집하면 다양한 네트워크 수준의 SLI(Service Level Indicator)를 정의할 수 있다. 아래는 프로덕션에서 자주 사용하는 PromQL 쿼리 모음이다.

# 서비스별 HTTP 요청 성공률 (SLI)
(
  sum(rate(hubble_http_requests_total{http_status_code=~"2.."}[5m])) by (destination_workload, destination_namespace)
  /
  sum(rate(hubble_http_requests_total[5m])) by (destination_workload, destination_namespace)
) * 100

# 네트워크 정책에 의해 드롭된 패킷 비율
sum(rate(hubble_drop_total{reason="POLICY_DENIED"}[5m])) by (source_workload, destination_workload)
/ on(source_workload) group_left
sum(rate(hubble_flows_processed_total[5m])) by (source_workload)

# TCP 연결 설정 P99 지연 시간 (서비스별)
histogram_quantile(0.99,
  sum(rate(hubble_tcp_connect_duration_seconds_bucket[5m])) by (le, destination_workload, destination_namespace)
)

# DNS 쿼리 실패율 상위 10개 워크로드
topk(10,
  sum(rate(hubble_dns_responses_total{rcode!="No Error"}[5m])) by (source_workload, source_namespace, qtypes, rcode)
)

# 네임스페이스 간 트래픽 볼륨 매트릭스
sum(rate(hubble_flows_processed_total[5m])) by (source_namespace, destination_namespace)

Grafana Beyla 자동 계측: 제로 코드 APM

Beyla의 동작 원리

Grafana Beyla는 eBPF를 활용한 자동 계측 도구로, 애플리케이션 코드를 전혀 변경하지 않고 HTTP/HTTPS, gRPC, SQL 요청에 대한 RED(Rate, Error, Duration) 메트릭과 분산 트레이스를 자동으로 생성한다. Beyla는 각 노드에 DaemonSet으로 배포되며, uprobes를 통해 Go, Java, Python, Node.js, Rust, .NET 등 다양한 런타임의 HTTP 핸들러와 gRPC 서버 함수를 자동으로 탐지하여 훅을 설치한다.

Beyla가 생성하는 핵심 메트릭은 다음과 같다. http.server.request.duration은 HTTP 서버의 요청 처리 시간을 히스토그램으로 제공하고, http.client.request.duration은 HTTP 클라이언트의 외부 호출 시간을 추적한다. rpc.server.durationrpc.client.duration은 gRPC 서버와 클라이언트의 호출 시간을 각각 측정한다. 이 메트릭들은 OpenTelemetry 시맨틱 컨벤션을 따르며, OTLP 프로토콜로 직접 내보내거나 Prometheus Remote Write로 전송할 수 있다.

Hubble이 네트워크 레이어(L3/L4/L7)의 가시성을 제공한다면, Beyla는 애플리케이션 레이어의 성능 메트릭과 트레이스를 보완한다. 두 도구를 함께 사용하면 인프라 네트워크부터 애플리케이션 성능까지 전 계층을 코드 변경 없이 관측할 수 있는 완전한 옵저버빌리티 스택이 구성된다.

Beyla의 자동 서비스 탐지

Beyla는 프로세스를 자동으로 탐지하여 계측 대상을 식별한다. 탐지 방식은 두 가지로 나뉜다. 첫째, 실행 바이너리의 심볼 테이블을 분석하여 HTTP 서버나 gRPC 서버 관련 함수가 포함되어 있는지 확인한다. Go 바이너리의 경우 net/http.(*Server).Serve, google.golang.org/grpc.(*Server).Serve 등의 심볼을 탐지한다. 둘째, 리스닝 소켓을 모니터링하여 특정 포트에서 TCP 연결을 수락하는 프로세스를 식별한다. 이 두 방식을 조합하여 어떤 언어로 작성된 서비스든 자동으로 계측 대상에 포함시킬 수 있다.

Beyla 배포와 설정: Kubernetes DaemonSet 구성

DaemonSet 기반 배포

Beyla를 Kubernetes에 배포하는 가장 일반적인 방식은 DaemonSet이다. 각 노드에서 호스트 PID 네임스페이스에 접근하여 노드의 모든 프로세스를 탐지하고 계측한다.

# beyla-daemonset.yaml
# Grafana Beyla DaemonSet 배포 매니페스트
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: beyla
  namespace: monitoring
  labels:
    app.kubernetes.io/name: beyla
    app.kubernetes.io/component: auto-instrumentation
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: beyla
  template:
    metadata:
      labels:
        app.kubernetes.io/name: beyla
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '9090'
        prometheus.io/path: '/metrics'
    spec:
      serviceAccountName: beyla
      hostPID: true # 호스트 PID 네임스페이스 접근 필수
      tolerations:
        - operator: Exists # 모든 노드(마스터 포함)에 배포
      containers:
        - name: beyla
          image: grafana/beyla:1.8.2
          securityContext:
            privileged: true # eBPF 프로그램 로드를 위해 필요
            runAsUser: 0
          ports:
            - containerPort: 9090
              name: metrics
              protocol: TCP
          env:
            - name: BEYLA_OPEN_PORT
              value: '80,443,8080,8443,3000,5000,9090'
            - name: BEYLA_SERVICE_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: BEYLA_KUBE_METADATA_ENABLE
              value: 'autodetect'
          volumeMounts:
            - name: beyla-config
              mountPath: /config
            - name: sys-kernel-security
              mountPath: /sys/kernel/security
              readOnly: true
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
      volumes:
        - name: beyla-config
          configMap:
            name: beyla-config
        - name: sys-kernel-security
          hostPath:
            path: /sys/kernel/security
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: beyla-config
  namespace: monitoring
data:
  beyla-config.yml: |
    # Beyla 메인 설정 파일
    log_level: info

    # 서비스 자동 탐지 설정
    discovery:
      services:
        - k8s_namespace: "production|staging"
          k8s_pod_labels:
            app.kubernetes.io/part-of: ".*"
        - k8s_namespace: ".*"
          k8s_deployment_name: ".*"

    # 메트릭 내보내기 설정
    otel_metrics_export:
      endpoint: http://mimir-distributor.monitoring:4317
      protocol: grpc
      interval: 15s
      features:
        - application
        - application_process
        - application_service_graph
      histograms:
        - explicit:
            boundaries:
              - 0.005
              - 0.01
              - 0.025
              - 0.05
              - 0.1
              - 0.25
              - 0.5
              - 1
              - 2.5
              - 5
              - 10

    # 트레이스 내보내기 설정
    otel_traces_export:
      endpoint: http://tempo-distributor.monitoring:4317
      protocol: grpc
      sampler:
        name: parentbased_traceidratio
        arg: "0.1"                       # 10% 샘플링

    # Prometheus 엔드포인트 설정
    prometheus_export:
      port: 9090
      path: /metrics
      features:
        - application
        - application_process
        - application_service_graph

    # 네트워크 메트릭 (실험적 기능)
    network:
      enable: true
      cidrs:
        - 10.0.0.0/8
        - 172.16.0.0/12

    # 속성 설정
    attributes:
      kubernetes:
        enable: true
      host_id:
        fetch_timeout: 5s
      select:
        beyla_network_flow_bytes:
          include:
            - k8s.src.namespace
            - k8s.dst.namespace
            - direction
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: beyla
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: beyla
rules:
  - apiGroups: ['']
    resources: ['pods', 'nodes', 'services', 'replicationcontrollers']
    verbs: ['get', 'list', 'watch']
  - apiGroups: ['apps']
    resources: ['deployments', 'replicasets', 'statefulsets', 'daemonsets']
    verbs: ['get', 'list', 'watch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: beyla
subjects:
  - kind: ServiceAccount
    name: beyla
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: beyla
  apiGroup: rbac.authorization.k8s.io
# Beyla DaemonSet 배포
kubectl apply -f beyla-daemonset.yaml

# 배포 상태 확인
kubectl rollout status daemonset/beyla -n monitoring

# Beyla 로그에서 자동 탐지된 서비스 확인
kubectl logs -n monitoring -l app.kubernetes.io/name=beyla --tail=50 | grep "instrumenting"
# 출력 예:
# msg="instrumenting process" pid=12345 service=frontend-deployment
# msg="instrumenting process" pid=12346 service=api-server-deployment
# msg="instrumenting process" pid=12347 service=payment-service-deployment

# Beyla가 생성하는 메트릭 확인
kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=beyla -o jsonpath='{.items[0].metadata.name}') \
  -- wget -qO- http://localhost:9090/metrics | head -40

# eBPF 프로그램 로드 상태 확인
kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=beyla -o jsonpath='{.items[0].metadata.name}') \
  -- bpftool prog list | grep beyla

Grafana 스택 통합: Tempo, Mimir, Loki

통합 아키텍처

Cilium Hubble과 Grafana Beyla가 생성하는 텔레메트리 데이터를 Grafana LGTM(Loki, Grafana, Tempo, Mimir) 스택과 통합하면 제로 계측 기반의 완전한 옵저버빌리티 플랫폼이 구성된다. 데이터 흐름은 다음과 같다.

Hubble 메트릭은 Prometheus ServiceMonitor를 통해 수집되어 Mimir에 장기 저장된다. Hubble의 네트워크 흐름 로그는 Fluentd 또는 Vector를 통해 Loki로 전송된다. Beyla가 생성하는 RED 메트릭은 OTLP 프로토콜로 Mimir Distributor에 직접 전송되거나 Prometheus Remote Write를 통해 저장된다. Beyla의 분산 트레이스는 OTLP를 통해 Tempo Distributor로 전송된다.

Grafana 대시보드에서는 Mimir의 메트릭(Hubble 네트워크 + Beyla APM), Tempo의 트레이스(Beyla 자동 생성), Loki의 로그(Hubble 플로우 로그)를 단일 뷰에서 상호 연관(Correlate)시킬 수 있다. 트레이스에서 메트릭으로, 메트릭에서 로그로의 자유로운 탐색이 가능하여 장애 원인 분석(RCA) 시간을 대폭 단축할 수 있다.

Grafana 데이터소스 및 대시보드 설정

# grafana-datasources.yaml
# Grafana 데이터소스 ConfigMap (Hubble + Beyla 통합)
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
  namespace: monitoring
  labels:
    grafana_datasource: '1'
data:
  datasources.yaml: |
    apiVersion: 1
    datasources:
      # Mimir - Hubble 네트워크 메트릭 + Beyla APM 메트릭 통합 저장
      - name: Mimir
        type: prometheus
        url: http://mimir-query-frontend.monitoring:8080/prometheus
        access: proxy
        isDefault: true
        jsonData:
          timeInterval: 15s
          exemplarTraceIdDestinations:
            - name: traceID
              datasourceUid: tempo
              urlDisplayLabel: View Trace
          httpMethod: POST
          prometheusType: Mimir

      # Tempo - Beyla 자동 생성 트레이스 저장
      - name: Tempo
        type: tempo
        uid: tempo
        url: http://tempo-query-frontend.monitoring:3200
        access: proxy
        jsonData:
          tracesToMetrics:
            datasourceUid: mimir
            spanStartTimeShift: "-1h"
            spanEndTimeShift: "1h"
            tags:
              - key: service.name
                value: service
              - key: http.method
                value: method
            queries:
              - name: Request Rate
                query: "sum(rate(http_server_request_duration_seconds_count{service=\"${__span.tags.service.name}\"}[5m]))"
              - name: Error Rate
                query: "sum(rate(http_server_request_duration_seconds_count{service=\"${__span.tags.service.name}\",http_status_code=~\"5..\"}[5m]))"
          tracesToLogs:
            datasourceUid: loki
            spanStartTimeShift: "-5m"
            spanEndTimeShift: "5m"
            filterByTraceID: true
            filterBySpanID: false
            tags:
              - key: k8s.namespace.name
                value: namespace
              - key: k8s.pod.name
                value: pod
          serviceMap:
            datasourceUid: mimir
          nodeGraph:
            enabled: true
          search:
            hide: false
          lokiSearch:
            datasourceUid: loki

      # Loki - Hubble 플로우 로그 + 애플리케이션 로그
      - name: Loki
        type: loki
        uid: loki
        url: http://loki-read.monitoring:3100
        access: proxy
        jsonData:
          derivedFields:
            - datasourceUid: tempo
              matcherRegex: "traceID=(\\w+)"
              name: TraceID
              url: "$${__value.raw}"

eBPF vs 사이드카 vs 에이전트 비교

eBPF 기반 옵저버빌리티, 사이드카 프록시 기반 서비스 메시, 전통적인 에이전트 기반 모니터링의 세 가지 접근 방식을 운영 관점에서 비교한다.

비교 항목eBPF (Hubble + Beyla)사이드카 (Istio/Envoy)에이전트 (SDK 계측)
코드 변경 필요없음없음 (자동 주입)SDK 통합 필요
Pod당 추가 컨테이너없음1개 (Envoy)없음
노드당 메모리 오버헤드~300MB (Cilium+Beyla)~100MB x Pod 수~50MB (에이전트)
P99 레이턴시 영향0.1ms 미만2~5ms0.5~1ms
L3/L4 네트워크 가시성매우 상세제한적없음
L7 HTTP/gRPC 가시성Beyla로 RED 메트릭매우 상세SDK 의존
분산 트레이스Beyla 자동 (context propagation 제한)자동 (완전한 propagation)완전한 제어
DNS 관측Hubble 기본 제공Envoy DNS 프록시별도 구성 필요
커널 버전 의존성5.8+ 권장없음없음
보안 권한 요구privileged 또는 CAP_BPFNET_ADMIN일반 사용자
서비스 메시 기능 (mTLS, 트래픽 제어)Cilium으로 부분 지원완전 지원없음
운영 복잡도중간높음낮음 (SDK 관리 비용은 높음)
다중 언어 지원언어 무관언어 무관언어별 SDK 필요

선택 가이드

eBPF 기반 접근이 적합한 경우는 다음과 같다. 수백 개의 마이크로서비스에 일관된 모니터링을 적용해야 하지만 각 서비스에 SDK를 통합할 엔지니어링 자원이 부족한 경우. 사이드카 프록시의 리소스 오버헤드를 감당하기 어려운 노드 밀도가 높은 클러스터. 레거시 서비스를 포함하여 코드 변경 없이 모니터링을 확보해야 하는 마이그레이션 기간. 네트워크 레벨의 세밀한 가시성(DNS, TCP 연결 상태, 패킷 드롭)이 중요한 인프라 중심 운영 환경.

반면 사이드카 프록시가 더 적합한 경우도 있다. mTLS, 트래픽 분할, 서킷 브레이킹 등 서비스 메시의 트래픽 제어 기능이 필요한 경우. 완전한 분산 트레이스 context propagation이 필수적인 경우. 커널 5.8 미만의 노드를 운영해야 하는 환경. eBPF의 privileged 컨테이너 권한을 허용할 수 없는 보안 정책이 적용된 클러스터.

운영 시 주의사항

커널 버전과 배포판 호환성

eBPF 기반 도구 운영에서 가장 흔한 장애 원인은 커널 버전 불일치다. Cilium Hubble은 커널 4.19 이상에서 기본 기능이 동작하지만, L7 프로토콜 파싱과 고급 메트릭은 커널 5.4 이상이 필요하다. Grafana Beyla의 uprobes 기반 자동 계측은 커널 5.8 이상에서만 안정적으로 동작하며, 특히 BPF ring buffer를 활용하는 기능은 5.8이 최소 요구사항이다.

Amazon EKS의 경우 AL2023 AMI가 커널 6.1을 기본 제공하므로 문제가 없지만, AL2 AMI는 커널 5.10으로 대부분의 기능을 지원하되 일부 고급 기능에 제약이 있을 수 있다. GKE는 Container-Optimized OS(COS)가 커널 5.15 이상을 제공하며, Ubuntu 노드 이미지도 5.15 이상이다. AKS는 Ubuntu 22.04 기반 노드가 커널 5.15를 사용한다.

보안 고려사항

Beyla는 eBPF 프로그램을 커널에 로드하기 위해 CAP_SYS_ADMIN 또는 CAP_BPF + CAP_PERFMON 권한이 필요하다. 가장 간단한 방법은 privileged 컨테이너로 실행하는 것이지만, 프로덕션 환경에서는 최소 권한 원칙에 따라 필요한 capability만 부여하는 것을 권장한다.

# 최소 권한 보안 컨텍스트 (privileged 대신)
securityContext:
  privileged: false
  runAsUser: 0
  capabilities:
    add:
      - BPF # eBPF 프로그램 로드
      - PERFMON # perf 이벤트 접근
      - NET_RAW # raw 소켓 접근 (네트워크 관측)
      - SYS_PTRACE # 프로세스 추적 (uprobes)
      - DAC_READ_SEARCH # 파일 시스템 탐색
    drop:
      - ALL

그러나 일부 Kubernetes 보안 정책(Pod Security Standards의 Restricted 프로파일)에서는 이러한 capability 추가 자체가 거부될 수 있다. 이 경우 Beyla DaemonSet에 대한 예외 정책을 별도로 구성해야 한다. 또한 eBPF 프로그램은 커널 공간에서 실행되므로, Beyla 이미지의 출처와 무결성을 반드시 검증해야 한다. 서명된 이미지를 사용하고, 이미지 풀 정책을 Always로 설정하여 변조된 이미지가 배포되지 않도록 해야 한다.

리소스 관리와 스케일링

대규모 클러스터(500+ 노드)에서 Hubble과 Beyla를 운영할 때는 리소스 사용량을 면밀히 모니터링해야 한다. Hubble Relay는 클러스터의 모든 노드에서 이벤트를 집계하므로, 노드 수가 증가할수록 Relay의 메모리와 CPU 사용량이 선형적으로 증가한다. 노드 100대당 Relay 인스턴스 1개를 기준으로, 500대 이상의 클러스터에서는 Relay를 5개 이상으로 수평 확장하고 로드밸런싱을 구성해야 한다.

Beyla는 노드당 계측 대상 프로세스 수에 따라 메모리 사용량이 변동한다. 프로세스당 약 2~5MB의 추가 메모리가 필요하므로, Pod 밀도가 높은 노드(50+ Pod)에서는 Beyla의 메모리 limit을 1Gi 이상으로 상향 조정해야 할 수 있다. 또한 discovery 설정에서 계측 대상을 특정 네임스페이스나 레이블로 제한하여 불필요한 리소스 소비를 방지하는 것이 좋다.

실패 사례와 복구 절차

사례 1: eBPF 프로그램 로드 실패

가장 흔한 실패 유형은 커널 버전 불일치로 인한 eBPF 프로그램 로드 실패다. 노드 업그레이드 과정에서 일부 노드만 새 커널로 전환되었을 때 발생하기 쉽다.

증상: Beyla Pod이 CrashLoopBackOff 상태에 빠지며, 로그에 failed to load eBPF program: invalid argument 또는 kernel doesn't support bpf_ringbuf 에러가 출력된다.

진단 절차: 먼저 해당 노드의 커널 버전을 확인한다. kubectl get node <node-name> -o jsonpath='{.status.nodeInfo.kernelVersion}'으로 커널 버전을 조회하고, Beyla가 요구하는 최소 버전(5.8+)과 비교한다. BTF 지원 여부도 확인해야 한다. 노드에 접속하여 /sys/kernel/btf/vmlinux 파일의 존재를 확인한다.

복구 방법: 커널 버전이 낮은 노드에는 nodeSelector 또는 nodeAffinity를 사용하여 Beyla DaemonSet이 스케줄되지 않도록 설정한다. 장기적으로는 해당 노드의 OS 이미지를 업그레이드한다.

사례 2: Hubble Relay 메모리 부족(OOMKilled)

Hubble Relay가 대량의 네트워크 흐름을 처리하면서 메모리 limit을 초과하여 OOMKilled되는 경우다.

증상: Hubble Relay Pod이 반복적으로 재시작되며, kubectl describe pod에서 OOMKilled 종료 사유가 확인된다. Hubble CLI와 UI에서 간헐적으로 연결이 끊어진다.

진단 절차: kubectl top pod -n kube-system -l k8s-app=hubble-relay로 현재 메모리 사용량을 확인한다. 또한 hubble_relay_received_flows_total 메트릭을 통해 Relay가 처리하는 이벤트의 초당 수량을 파악한다.

복구 방법: 단기적으로는 Relay의 메모리 limit을 증가시킨다(512Mi에서 1Gi 이상). 중기적으로는 Cilium의 bpf.monitorAggregation 설정을 medium 또는 high로 조정하여 이벤트 수를 줄인다. 장기적으로는 Relay 인스턴스를 수평 확장한다.

사례 3: Beyla 메트릭 누락

Beyla가 정상적으로 실행되고 있지만 특정 서비스의 메트릭이 수집되지 않는 경우다.

증상: Grafana 대시보드에서 특정 서비스의 RED 메트릭이 표시되지 않는다. Beyla 로그에 해당 서비스의 계측 메시지가 없다.

진단 절차: 해당 서비스의 바이너리 형식을 확인한다. Go로 작성된 서비스가 -ldflags="-s -w" 옵션으로 심볼을 제거(strip)하여 빌드되었다면, Beyla가 함수 심볼을 탐지하지 못할 수 있다. 또한 서비스가 비표준 포트를 사용하거나, 커스텀 HTTP 프레임워크를 사용하는 경우에도 자동 탐지가 실패할 수 있다.

복구 방법: Go 바이너리의 경우 -ldflags="-s -w" 대신 심볼을 유지하거나, Beyla 설정에서 BEYLA_OPEN_PORT 환경변수에 해당 서비스의 포트를 명시적으로 추가한다. 비표준 프레임워크의 경우 Beyla의 generic HTTP tracing 모드를 활성화하여 소켓 레벨에서 HTTP 패턴을 감지하도록 설정한다.

사례 4: Hubble과 Beyla 간 메트릭 불일치

Hubble의 네트워크 레벨 HTTP 메트릭과 Beyla의 애플리케이션 레벨 HTTP 메트릭의 수치가 불일치하는 경우다.

증상: Hubble이 보고하는 HTTP 요청 수가 Beyla가 보고하는 수보다 많거나 적다.

원인 분석: Hubble은 네트워크 패킷 레벨에서 HTTP를 파싱하므로 헬스체크, 쿠버네티스 프로브, 사이드카 간 통신 등 모든 HTTP 트래픽을 캡처한다. 반면 Beyla는 애플리케이션 프로세스의 함수 호출을 추적하므로 실제 애플리케이션이 처리한 요청만 계측한다. 이 차이는 정상적인 동작이며, 두 메트릭을 함께 분석하면 인프라 트래픽과 애플리케이션 트래픽을 구분하는 데 유용하다.

운영 체크리스트

프로덕션에 eBPF 기반 옵저버빌리티를 도입할 때 확인해야 할 항목 목록이다.

배포 전 검증:

  • 모든 노드의 커널 버전이 5.8 이상인지 확인
  • BTF(BPF Type Format) 지원이 활성화되어 있는지 확인 (/sys/kernel/btf/vmlinux 존재)
  • 노드의 보안 정책(Pod Security Standards)이 privileged 또는 CAP_BPF를 허용하는지 확인
  • 관리형 Kubernetes(EKS, GKE, AKS)의 경우 CNI 플러그인 교체 가능 여부 확인
  • Cilium이 기존 CNI와 충돌하지 않는지 검증 (kube-proxy 교체 모드 포함)

배포 후 검증:

  • Cilium Agent가 모든 노드에서 Ready 상태인지 확인 (cilium status)
  • Hubble Relay가 모든 노드의 Hubble Server와 연결되었는지 확인
  • Beyla가 예상 서비스를 자동 탐지했는지 로그 확인
  • Mimir/Prometheus에 Hubble 및 Beyla 메트릭이 정상 수집되는지 확인
  • Tempo에 Beyla 트레이스가 정상 전송되는지 확인

모니터링 경보:

  • Hubble Relay의 메모리 사용률이 80% 초과 시 경보
  • Beyla DaemonSet의 Ready Pod 수가 노드 수와 일치하는지 확인
  • eBPF 프로그램 로드 실패 이벤트 감지
  • Hubble 메트릭 수집 지연(scrape duration) 모니터링
  • Beyla의 OTLP export 실패율 모니터링

정기 점검:

  • 월간: 커널 보안 패치 적용 후 eBPF 프로그램 호환성 검증
  • 분기별: Cilium/Beyla 버전 업그레이드 검토 및 테스트
  • 반기별: 커널 버전 업그레이드 계획 수립 (LTS 커널 추적)

참고자료

eBPF-Based Zero Instrumentation Kubernetes Observability: Cilium Hubble and Grafana Beyla Practical Guide

eBPF Observability

Introduction: Why eBPF-Based Zero Instrumentation

There are two traditional approaches to achieving observability in Kubernetes environments. First, explicit instrumentation, which inserts SDKs into application code to generate metrics and traces. Second, the service mesh approach, which injects Envoy-based sidecar proxies into each Pod to observe L7 traffic. Both approaches are proven in production but have fundamental limitations.

SDK-based instrumentation requires adding libraries and writing instrumentation code for every service. Maintaining this consistently across large-scale clusters with hundreds of microservices involves enormous engineering costs. The sidecar proxy approach can achieve L7 visibility without code changes, but deploying an additional container per Pod increases per-node memory usage by hundreds of MB, and adding a network hop typically raises P99 latency by 2-5ms.

eBPF (extended Berkeley Packet Filter) addresses the limitations of both approaches simultaneously. By running sandboxed programs inside the Linux kernel, it can observe network flows, HTTP/gRPC requests, DNS queries, and TCP connection states at the kernel level without any application code changes. No sidecar containers are needed, so resource overhead is extremely low, and since data is collected directly from the kernel, it has virtually no impact on application performance.

This article focuses on Cilium Hubble and Grafana Beyla, the core tools for eBPF-based observability, and provides a detailed practical operations guide for building network visibility and application performance monitoring (APM) in Kubernetes clusters with zero code changes.

eBPF Technology Overview: Principles of Kernel-Level Instrumentation

eBPF Program Execution Flow

eBPF is a technology introduced in earnest from Linux kernel 4.x that enables running programs in kernel space without directly modifying kernel modules. eBPF programs are written in user space, pass through the kernel's Verifier, and are then converted to native code by the JIT (Just-In-Time) compiler. The Verifier ensures kernel stability by preventing infinite loops, blocking out-of-bounds memory access, and allowing only permitted helper function calls.

The hook points where eBPF programs can be attached are highly diverse. For networking, there are TC (Traffic Control), XDP (eXpress Data Path), and socket-level hooks. For system-wide observation, kprobes (kernel function entry/exit), tracepoints (predefined observation points), and uprobes (user-space function tracing) are used. Cilium Hubble primarily uses TC and socket-level hooks to observe network flows, while Grafana Beyla leverages uprobes and kprobes to automatically extract RED (Rate, Error, Duration) metrics from HTTP/gRPC requests.

eBPF Feature Support by Kernel Version

eBPF feature support varies by kernel version. Full Cilium Hubble functionality requires at least kernel 4.19, and Grafana Beyla's auto-instrumentation features operate stably on kernel 5.8 and above. Kernel 5.15 LTS or higher is recommended for production environments. Specifically, kernels with BTF (BPF Type Format) support enabled allow eBPF programs to be deployed in CO-RE (Compile Once, Run Everywhere) mode, ensuring compatibility across diverse node environments.

# Check current node's kernel version and BTF support
uname -r
# Example output: 5.15.0-91-generic

# Check BTF support
ls /sys/kernel/btf/vmlinux
# If the file exists, BTF support is enabled

# eBPF feature probe (using bpftool)
bpftool feature probe kernel | grep -E "map_type|program_type|helper"

# Bulk check kernel versions across Kubernetes nodes
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.kernelVersion}{"\n"}{end}'

Cilium Hubble Architecture: The Core of Network Observability

Hubble Internal Structure

Cilium Hubble is the observability layer of Cilium CNI, composed of three core components. First, Hubble Server runs on each node, embedded in the Cilium Agent, collecting network events from the eBPF dataplane. Second, Hubble Relay aggregates events from Hubble Servers across the entire cluster and provides them through a single API endpoint. Third, Hubble UI visually represents service maps and network flows.

The scope of network events Hubble can observe is as follows. At the L3/L4 level, it provides source/destination IP, port, and protocol information for TCP/UDP/ICMP packets, along with packet drop reasons. At the L7 level, it parses HTTP request/response methods, paths, and status codes; DNS query and response domains, types, and response codes; and Kafka message topics and API keys. All of this information is automatically mapped to Kubernetes identities (namespaces, Pods, service labels), enabling clear understanding of inter-service communication patterns.

Service Maps and Flow Visibility

One of Hubble's most powerful features is automatically generated service maps. In traditional service meshes, Envoy sidecars proxy each Pod's inbound/outbound traffic to construct service topology, but Hubble automatically builds service dependency graphs from network flow data collected by kernel eBPF programs. This allows you to see in real-time which services communicate with which other services, and what the communication success rates and latencies are, all without deploying a service mesh.

Hubble Installation and Configuration: Helm Chart-Based Deployment

Cilium + Hubble Integrated Installation

Hubble is deployed as part of Cilium CNI. If you are already using Cilium, you simply need to enable the Hubble feature. For new clusters, enable Hubble during Cilium installation. Below is a Helm values configuration suitable for production environments.

# cilium-hubble-values.yaml
# Helm values for Cilium + Hubble production deployment
hubble:
  enabled: true
  relay:
    enabled: true
    replicas: 3 # Relay replicas for high availability
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
    retryTimeout: 30s
    sortBufferLenMax: 5000
    sortBufferDrainTimeout: 1s
  ui:
    enabled: true
    replicas: 2
    ingress:
      enabled: true
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt-prod
      hosts:
        - hubble.internal.example.com
      tls:
        - secretName: hubble-ui-tls
          hosts:
            - hubble.internal.example.com
  metrics:
    enableOpenMetrics: true
    enabled:
      - dns
      - drop
      - tcp
      - flow
      - port-distribution
      - icmp
      - httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction
    serviceMonitor:
      enabled: true # Auto-create Prometheus Operator ServiceMonitor
      interval: 15s
  tls:
    enabled: true # Enable mTLS between Hubble Server and Relay
    auto:
      enabled: true
      method: certmanager
      certManagerIssuerRef:
        group: cert-manager.io
        kind: ClusterIssuer
        name: hubble-issuer

# Cilium Agent optimization settings
operator:
  replicas: 2
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

# Monitor buffer size (per-node event cache)
bpf:
  monitorAggregation: medium # none/low/medium/high - aggregation level
  monitorInterval: 5s
  monitorFlags: all
  mapDynamicSizeRatio: 0.0025
# Install Cilium + Hubble (Helm)
helm repo add cilium https://helm.cilium.io
helm repo update

helm upgrade --install cilium cilium/cilium \
  --version 1.16.5 \
  --namespace kube-system \
  --values cilium-hubble-values.yaml \
  --wait

# Verify installation
cilium status --wait
cilium hubble port-forward &

# Check cluster-wide flows with Hubble CLI
hubble observe --since 1m --output compact

# Filter only HTTP traffic in a specific namespace
hubble observe \
  --namespace production \
  --protocol http \
  --http-status 500-599 \
  --output json | jq '{
    source: .flow.source.labels,
    destination: .flow.destination.labels,
    http: .flow.l7.http
  }'

# Analyze dropped packets between services
hubble observe \
  --verdict DROPPED \
  --namespace production \
  --output table

Hubble Metrics-Based PromQL Queries

When Hubble-generated metrics are collected by Prometheus, you can define various network-level SLIs (Service Level Indicators). Below is a collection of PromQL queries commonly used in production.

# Per-service HTTP request success rate (SLI)
(
  sum(rate(hubble_http_requests_total{http_status_code=~"2.."}[5m])) by (destination_workload, destination_namespace)
  /
  sum(rate(hubble_http_requests_total[5m])) by (destination_workload, destination_namespace)
) * 100

# Packet drop rate due to network policy
sum(rate(hubble_drop_total{reason="POLICY_DENIED"}[5m])) by (source_workload, destination_workload)
/ on(source_workload) group_left
sum(rate(hubble_flows_processed_total[5m])) by (source_workload)

# TCP connection setup P99 latency (per service)
histogram_quantile(0.99,
  sum(rate(hubble_tcp_connect_duration_seconds_bucket[5m])) by (le, destination_workload, destination_namespace)
)

# Top 10 workloads by DNS query failure rate
topk(10,
  sum(rate(hubble_dns_responses_total{rcode!="No Error"}[5m])) by (source_workload, source_namespace, qtypes, rcode)
)

# Inter-namespace traffic volume matrix
sum(rate(hubble_flows_processed_total[5m])) by (source_namespace, destination_namespace)

Grafana Beyla Auto-Instrumentation: Zero-Code APM

How Beyla Works

Grafana Beyla is an auto-instrumentation tool using eBPF that automatically generates RED (Rate, Error, Duration) metrics and distributed traces for HTTP/HTTPS, gRPC, and SQL requests without any application code changes. Beyla is deployed as a DaemonSet on each node, and through uprobes, it automatically detects and hooks HTTP handlers and gRPC server functions across various runtimes including Go, Java, Python, Node.js, Rust, and .NET.

The core metrics Beyla generates are as follows. http.server.request.duration provides HTTP server request processing time as a histogram, and http.client.request.duration tracks HTTP client external call times. rpc.server.duration and rpc.client.duration measure gRPC server and client call times respectively. These metrics follow OpenTelemetry semantic conventions and can be exported directly via OTLP protocol or sent through Prometheus Remote Write.

While Hubble provides visibility at the network layer (L3/L4/L7), Beyla complements it with application-layer performance metrics and traces. Using both tools together creates a complete observability stack that can observe everything from infrastructure networking to application performance without any code changes.

Beyla's Automatic Service Discovery

Beyla automatically detects processes to identify instrumentation targets. Detection works in two ways. First, it analyzes the symbol table of executable binaries to check if they contain HTTP server or gRPC server-related functions. For Go binaries, it detects symbols like net/http.(*Server).Serve and google.golang.org/grpc.(*Server).Serve. Second, it monitors listening sockets to identify processes accepting TCP connections on specific ports. By combining these two methods, services written in any language can be automatically included as instrumentation targets.

Beyla Deployment and Configuration: Kubernetes DaemonSet Setup

DaemonSet-Based Deployment

The most common way to deploy Beyla on Kubernetes is as a DaemonSet. It accesses the host PID namespace on each node to detect and instrument all processes on the node.

# beyla-daemonset.yaml
# Grafana Beyla DaemonSet deployment manifest
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: beyla
  namespace: monitoring
  labels:
    app.kubernetes.io/name: beyla
    app.kubernetes.io/component: auto-instrumentation
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: beyla
  template:
    metadata:
      labels:
        app.kubernetes.io/name: beyla
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '9090'
        prometheus.io/path: '/metrics'
    spec:
      serviceAccountName: beyla
      hostPID: true # Host PID namespace access required
      tolerations:
        - operator: Exists # Deploy to all nodes (including master)
      containers:
        - name: beyla
          image: grafana/beyla:1.8.2
          securityContext:
            privileged: true # Required for loading eBPF programs
            runAsUser: 0
          ports:
            - containerPort: 9090
              name: metrics
              protocol: TCP
          env:
            - name: BEYLA_OPEN_PORT
              value: '80,443,8080,8443,3000,5000,9090'
            - name: BEYLA_SERVICE_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: BEYLA_KUBE_METADATA_ENABLE
              value: 'autodetect'
          volumeMounts:
            - name: beyla-config
              mountPath: /config
            - name: sys-kernel-security
              mountPath: /sys/kernel/security
              readOnly: true
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
      volumes:
        - name: beyla-config
          configMap:
            name: beyla-config
        - name: sys-kernel-security
          hostPath:
            path: /sys/kernel/security
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: beyla-config
  namespace: monitoring
data:
  beyla-config.yml: |
    # Beyla main configuration file
    log_level: info

    # Service auto-discovery settings
    discovery:
      services:
        - k8s_namespace: "production|staging"
          k8s_pod_labels:
            app.kubernetes.io/part-of: ".*"
        - k8s_namespace: ".*"
          k8s_deployment_name: ".*"

    # Metrics export settings
    otel_metrics_export:
      endpoint: http://mimir-distributor.monitoring:4317
      protocol: grpc
      interval: 15s
      features:
        - application
        - application_process
        - application_service_graph
      histograms:
        - explicit:
            boundaries:
              - 0.005
              - 0.01
              - 0.025
              - 0.05
              - 0.1
              - 0.25
              - 0.5
              - 1
              - 2.5
              - 5
              - 10

    # Trace export settings
    otel_traces_export:
      endpoint: http://tempo-distributor.monitoring:4317
      protocol: grpc
      sampler:
        name: parentbased_traceidratio
        arg: "0.1"                       # 10% sampling

    # Prometheus endpoint settings
    prometheus_export:
      port: 9090
      path: /metrics
      features:
        - application
        - application_process
        - application_service_graph

    # Network metrics (experimental feature)
    network:
      enable: true
      cidrs:
        - 10.0.0.0/8
        - 172.16.0.0/12

    # Attribute settings
    attributes:
      kubernetes:
        enable: true
      host_id:
        fetch_timeout: 5s
      select:
        beyla_network_flow_bytes:
          include:
            - k8s.src.namespace
            - k8s.dst.namespace
            - direction
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: beyla
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: beyla
rules:
  - apiGroups: ['']
    resources: ['pods', 'nodes', 'services', 'replicationcontrollers']
    verbs: ['get', 'list', 'watch']
  - apiGroups: ['apps']
    resources: ['deployments', 'replicasets', 'statefulsets', 'daemonsets']
    verbs: ['get', 'list', 'watch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: beyla
subjects:
  - kind: ServiceAccount
    name: beyla
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: beyla
  apiGroup: rbac.authorization.k8s.io
# Deploy Beyla DaemonSet
kubectl apply -f beyla-daemonset.yaml

# Check deployment status
kubectl rollout status daemonset/beyla -n monitoring

# Check auto-detected services in Beyla logs
kubectl logs -n monitoring -l app.kubernetes.io/name=beyla --tail=50 | grep "instrumenting"
# Example output:
# msg="instrumenting process" pid=12345 service=frontend-deployment
# msg="instrumenting process" pid=12346 service=api-server-deployment
# msg="instrumenting process" pid=12347 service=payment-service-deployment

# Check metrics generated by Beyla
kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=beyla -o jsonpath='{.items[0].metadata.name}') \
  -- wget -qO- http://localhost:9090/metrics | head -40

# Check eBPF program load status
kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=beyla -o jsonpath='{.items[0].metadata.name}') \
  -- bpftool prog list | grep beyla

Grafana Stack Integration: Tempo, Mimir, Loki

Integrated Architecture

Integrating telemetry data generated by Cilium Hubble and Grafana Beyla with the Grafana LGTM (Loki, Grafana, Tempo, Mimir) stack creates a complete observability platform based on zero instrumentation. The data flow is as follows.

Hubble metrics are collected via Prometheus ServiceMonitor and stored long-term in Mimir. Hubble's network flow logs are sent to Loki via Fluentd or Vector. RED metrics generated by Beyla are sent directly to Mimir Distributor via OTLP protocol or stored through Prometheus Remote Write. Beyla's distributed traces are sent to Tempo Distributor via OTLP.

In Grafana dashboards, you can correlate Mimir metrics (Hubble network + Beyla APM), Tempo traces (Beyla auto-generated), and Loki logs (Hubble flow logs) in a single view. The ability to freely navigate from traces to metrics and from metrics to logs significantly reduces root cause analysis (RCA) time during incidents.

Grafana Datasource and Dashboard Configuration

# grafana-datasources.yaml
# Grafana datasource ConfigMap (Hubble + Beyla integration)
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
  namespace: monitoring
  labels:
    grafana_datasource: '1'
data:
  datasources.yaml: |
    apiVersion: 1
    datasources:
      # Mimir - Unified storage for Hubble network metrics + Beyla APM metrics
      - name: Mimir
        type: prometheus
        url: http://mimir-query-frontend.monitoring:8080/prometheus
        access: proxy
        isDefault: true
        jsonData:
          timeInterval: 15s
          exemplarTraceIdDestinations:
            - name: traceID
              datasourceUid: tempo
              urlDisplayLabel: View Trace
          httpMethod: POST
          prometheusType: Mimir

      # Tempo - Storage for Beyla auto-generated traces
      - name: Tempo
        type: tempo
        uid: tempo
        url: http://tempo-query-frontend.monitoring:3200
        access: proxy
        jsonData:
          tracesToMetrics:
            datasourceUid: mimir
            spanStartTimeShift: "-1h"
            spanEndTimeShift: "1h"
            tags:
              - key: service.name
                value: service
              - key: http.method
                value: method
            queries:
              - name: Request Rate
                query: "sum(rate(http_server_request_duration_seconds_count{service=\"${__span.tags.service.name}\"}[5m]))"
              - name: Error Rate
                query: "sum(rate(http_server_request_duration_seconds_count{service=\"${__span.tags.service.name}\",http_status_code=~\"5..\"}[5m]))"
          tracesToLogs:
            datasourceUid: loki
            spanStartTimeShift: "-5m"
            spanEndTimeShift: "5m"
            filterByTraceID: true
            filterBySpanID: false
            tags:
              - key: k8s.namespace.name
                value: namespace
              - key: k8s.pod.name
                value: pod
          serviceMap:
            datasourceUid: mimir
          nodeGraph:
            enabled: true
          search:
            hide: false
          lokiSearch:
            datasourceUid: loki

      # Loki - Hubble flow logs + application logs
      - name: Loki
        type: loki
        uid: loki
        url: http://loki-read.monitoring:3100
        access: proxy
        jsonData:
          derivedFields:
            - datasourceUid: tempo
              matcherRegex: "traceID=(\\w+)"
              name: TraceID
              url: "$${__value.raw}"

eBPF vs Sidecar vs Agent Comparison

A comparison of eBPF-based observability, sidecar proxy-based service mesh, and traditional agent-based monitoring from an operational perspective.

Comparison ItemeBPF (Hubble + Beyla)Sidecar (Istio/Envoy)Agent (SDK Instrumentation)
Code change requiredNoneNone (auto-injection)SDK integration required
Additional container per PodNone1 (Envoy)None
Memory overhead per node~300MB (Cilium+Beyla)~100MB x Pod count~50MB (agent)
P99 latency impactUnder 0.1ms2~5ms0.5~1ms
L3/L4 network visibilityVery detailedLimitedNone
L7 HTTP/gRPC visibilityRED metrics via BeylaVery detailedSDK dependent
Distributed tracingBeyla auto (context propagation limited)Auto (full propagation)Full control
DNS observationHubble built-inEnvoy DNS proxySeparate configuration needed
Kernel version dependency5.8+ recommendedNoneNone
Security permission requirementsprivileged or CAP_BPFNET_ADMINRegular user
Service mesh features (mTLS, traffic control)Partially supported via CiliumFully supportedNone
Operational complexityMediumHighLow (but SDK management cost is high)
Multi-language supportLanguage agnosticLanguage agnosticLanguage-specific SDK needed

Selection Guide

eBPF-based approaches are suitable in the following cases. When you need to apply consistent monitoring across hundreds of microservices but lack the engineering resources to integrate SDKs into each service. High-density node clusters where sidecar proxy resource overhead is difficult to absorb. Migration periods where you need to achieve monitoring without code changes, including legacy services. Infrastructure-centric operations where fine-grained network-level visibility (DNS, TCP connection state, packet drops) is important.

On the other hand, sidecar proxies may be more suitable in some cases. When you need service mesh traffic control features such as mTLS, traffic splitting, and circuit breaking. When complete distributed trace context propagation is essential. Environments that must operate nodes with kernels older than 5.8. Clusters with security policies that cannot allow privileged container permissions for eBPF.

Operational Considerations

Kernel Version and Distribution Compatibility

The most common failure cause in eBPF-based tool operations is kernel version mismatch. Cilium Hubble's basic features work on kernel 4.19 and above, but L7 protocol parsing and advanced metrics require kernel 5.4 or higher. Grafana Beyla's uprobes-based auto-instrumentation operates stably only on kernel 5.8 and above, and features utilizing BPF ring buffer specifically require 5.8 as the minimum.

For Amazon EKS, the AL2023 AMI provides kernel 6.1 by default, so there are no issues. However, the AL2 AMI uses kernel 5.10, which supports most features but may have limitations for some advanced capabilities. GKE's Container-Optimized OS (COS) provides kernel 5.15 or higher, and Ubuntu node images are also 5.15 or above. AKS uses kernel 5.15 with Ubuntu 22.04-based nodes.

Security Considerations

Beyla requires CAP_SYS_ADMIN or CAP_BPF + CAP_PERFMON privileges to load eBPF programs into the kernel. The simplest method is running as a privileged container, but in production environments, it is recommended to grant only the necessary capabilities following the principle of least privilege.

# Minimum privilege security context (instead of privileged)
securityContext:
  privileged: false
  runAsUser: 0
  capabilities:
    add:
      - BPF # eBPF program loading
      - PERFMON # perf event access
      - NET_RAW # raw socket access (network observation)
      - SYS_PTRACE # process tracing (uprobes)
      - DAC_READ_SEARCH # filesystem traversal
    drop:
      - ALL

However, some Kubernetes security policies (Pod Security Standards Restricted profile) may deny adding these capabilities altogether. In this case, you need to configure a separate exception policy for the Beyla DaemonSet. Also, since eBPF programs execute in kernel space, the source and integrity of Beyla images must be verified. Use signed images and set the image pull policy to Always to prevent tampered images from being deployed.

Resource Management and Scaling

When operating Hubble and Beyla on large-scale clusters (500+ nodes), you must closely monitor resource usage. Hubble Relay aggregates events from all nodes in the cluster, so its memory and CPU usage increases linearly with the number of nodes. Using one Relay instance per 100 nodes as a baseline, clusters with 500 or more nodes should horizontally scale Relay to 5 or more instances with load balancing configured.

Beyla's memory usage varies based on the number of instrumented processes per node. Approximately 2-5MB of additional memory is needed per process, so on nodes with high Pod density (50+ Pods), Beyla's memory limit may need to be increased to 1Gi or more. Additionally, it is advisable to limit instrumentation targets to specific namespaces or labels in the discovery settings to prevent unnecessary resource consumption.

Failure Cases and Recovery Procedures

Case 1: eBPF Program Load Failure

The most common failure type is eBPF program load failure due to kernel version mismatch. This easily occurs when only some nodes are transitioned to a new kernel during a node upgrade process.

Symptom: Beyla Pod falls into CrashLoopBackOff state, with failed to load eBPF program: invalid argument or kernel doesn't support bpf_ringbuf errors in the logs.

Diagnostic procedure: First, check the kernel version of the affected node. Query the kernel version with kubectl get node <node-name> -o jsonpath='{.status.nodeInfo.kernelVersion}' and compare it to Beyla's minimum required version (5.8+). BTF support should also be verified. Access the node and check for the existence of /sys/kernel/btf/vmlinux.

Recovery method: Use nodeSelector or nodeAffinity to prevent the Beyla DaemonSet from being scheduled on nodes with older kernels. In the long term, upgrade the OS image on those nodes.

Case 2: Hubble Relay Out of Memory (OOMKilled)

This occurs when Hubble Relay exceeds its memory limit while processing a large volume of network flows.

Symptom: Hubble Relay Pod restarts repeatedly, and OOMKilled termination reason is confirmed in kubectl describe pod. Connections intermittently drop in Hubble CLI and UI.

Diagnostic procedure: Check current memory usage with kubectl top pod -n kube-system -l k8s-app=hubble-relay. Also use the hubble_relay_received_flows_total metric to understand the per-second volume of events being processed by Relay.

Recovery method: In the short term, increase Relay's memory limit (from 512Mi to 1Gi or more). In the medium term, adjust Cilium's bpf.monitorAggregation setting to medium or high to reduce event volume. In the long term, horizontally scale Relay instances.

Case 3: Beyla Metrics Missing

Beyla is running normally but metrics for a specific service are not being collected.

Symptom: RED metrics for a specific service do not appear in Grafana dashboards. No instrumentation messages for that service appear in Beyla logs.

Diagnostic procedure: Check the binary format of the service. If a Go service was built with -ldflags="-s -w" option to strip symbols, Beyla may not be able to detect function symbols. Auto-detection can also fail if the service uses non-standard ports or a custom HTTP framework.

Recovery method: For Go binaries, retain symbols instead of using -ldflags="-s -w", or explicitly add the service's port to the BEYLA_OPEN_PORT environment variable in Beyla's configuration. For non-standard frameworks, enable Beyla's generic HTTP tracing mode to detect HTTP patterns at the socket level.

Case 4: Metric Discrepancy Between Hubble and Beyla

A case where Hubble's network-level HTTP metrics and Beyla's application-level HTTP metrics show different numbers.

Symptom: The number of HTTP requests reported by Hubble is higher or lower than what Beyla reports.

Root cause analysis: Hubble parses HTTP at the network packet level, so it captures all HTTP traffic including health checks, Kubernetes probes, and sidecar-to-sidecar communication. Beyla, on the other hand, traces function calls of application processes, so it only instruments requests actually processed by the application. This discrepancy is normal behavior, and analyzing both metrics together is useful for distinguishing infrastructure traffic from application traffic.

Operations Checklist

A list of items to verify when introducing eBPF-based observability into production.

Pre-deployment verification:

  • Confirm all node kernel versions are 5.8 or above
  • Verify BTF (BPF Type Format) support is enabled (/sys/kernel/btf/vmlinux exists)
  • Confirm node security policies (Pod Security Standards) allow privileged or CAP_BPF
  • For managed Kubernetes (EKS, GKE, AKS), verify CNI plugin replacement feasibility
  • Validate Cilium does not conflict with existing CNI (including kube-proxy replacement mode)

Post-deployment verification:

  • Confirm Cilium Agent is Ready on all nodes (cilium status)
  • Confirm Hubble Relay is connected to all node Hubble Servers
  • Check logs to verify Beyla has auto-detected expected services
  • Confirm Hubble and Beyla metrics are being collected normally in Mimir/Prometheus
  • Confirm Beyla traces are being sent normally to Tempo

Monitoring alerts:

  • Alert when Hubble Relay memory utilization exceeds 80%
  • Verify Beyla DaemonSet Ready Pod count matches node count
  • Detect eBPF program load failure events
  • Monitor Hubble metric collection delay (scrape duration)
  • Monitor Beyla's OTLP export failure rate

Regular maintenance:

  • Monthly: Verify eBPF program compatibility after kernel security patches
  • Quarterly: Review and test Cilium/Beyla version upgrades
  • Semi-annually: Plan kernel version upgrades (track LTS kernels)

References

Quiz

Q1: What is the main topic covered in "eBPF-Based Zero Instrumentation Kubernetes Observability: Cilium Hubble and Grafana Beyla Practical Guide"?

A practical operations guide for zero-code-change Kubernetes monitoring architecture using eBPF-based Cilium Hubble network observability and Grafana Beyla auto-instrumentation.

Q2: Describe the Cilium Hubble Architecture: The Core of Network Observability. Hubble Internal Structure Cilium Hubble is the observability layer of Cilium CNI, composed of three core components. First, Hubble Server runs on each node, embedded in the Cilium Agent, collecting network events from the eBPF dataplane.

Q3: What are the key steps for Hubble Installation and Configuration: Helm Chart-Based Deployment?

Cilium + Hubble Integrated Installation Hubble is deployed as part of Cilium CNI. If you are already using Cilium, you simply need to enable the Hubble feature. For new clusters, enable Hubble during Cilium installation.

Q4: What are the key aspects of Grafana Beyla Auto-Instrumentation: Zero-Code APM?

How Beyla Works Grafana Beyla is an auto-instrumentation tool using eBPF that automatically generates RED (Rate, Error, Duration) metrics and distributed traces for HTTP/HTTPS, gRPC, and SQL requests without any application code changes.

Q5: What are the key steps for Beyla Deployment and Configuration: Kubernetes DaemonSet Setup?

DaemonSet-Based Deployment The most common way to deploy Beyla on Kubernetes is as a DaemonSet. It accesses the host PID namespace on each node to detect and instrument all processes on the node.