🐛

Keploy — eBPF로 트래픽을 가로채서 API 테스트를 자동 생성하는 구조

Go + eBPF + 투명 프록시로 코드 수정 없이 테스트를 만드는 CNCF 프로젝트

Go로 작성된 CNCF 프로젝트. 17k+ 스타. keploy record 한 번이면 앱의 API 호출, DB 쿼리, 외부 요청을 전부 녹화해서 YAML 기반 테스트+Mock으로 뽑아낸다.

핵심은 eBPF다. 앱에 SDK를 심거나 코드를 고칠 필요가 없다. 모든 언어가 결국 커널 syscall(socket, connect, bind, sendmsg)을 호출하니까, 거기를 잡으면 언어에 상관없이 동작한다.

아키텍처 3층 구조

Agent 레이어 — eBPF 훅 + 투명 프록시. 커널에 붙어서 트래픽을 가로챈다.

Service 레이어 — record, replay, mock, coverage 오케스트레이션. 녹화/재생/비교의 핵심 로직.

Platform 레이어 — YAML 스토리지, 언어별 커버리지 수집, Docker/Telemetry.

eBPF가 트래픽을 잡는 방법

pkg/agent/hooks/linux/에 미리 컴파일된 eBPF 오브젝트(bpf_x86_bpfel.o, bpf_arm64_bpfel.o)가 있다. cilium/ebpf Go 라이브러리로 로드한다. 런타임 컴파일이 필요 없다.

핵심 훅들:

sys_enter_socket tracepoint — 소켓 생성 syscall을 잡아서 새 연결을 추적한다.

SockOps (cgroup 부착) — cgroupv2 경로에 부착. 컨테이너 안의 모든 소켓 연산(connect, accept 등)을 감시하는 핵심 훅이다.

connect4/connect6 — IPv4/IPv6 아웃바운드 연결을 가로챈다.

cgBind4/cgBind6 — bind() 호출을 감시해서 앱이 어떤 포트에서 리슨하는지 파악한다.

BindEvents 링 버퍼 — eBPF 프로그램이 이벤트를 ring buffer에 쓰고, Go 유저스페이스가 ringbuf.NewReader로 읽는다.

핵심 트릭: 목적지 리다이렉트

가장 영리한 부분이다. eBPF 공유 맵 3개가 핵심이다:

clientRegistrationMap — 대상 앱을 eBPF에 등록.
agentRegistrationMap — Keploy 에이전트를 등록.
redirectProxyMap — 소스 포트 → 원래 목적지(DestInfo 구조체: IPv4/IPv6 + 포트) 매핑.

eBPF 프로그램이 나가는 연결의 목적지를 Keploy의 로컬 프록시(127.0.0.1:proxyPort)로 덮어쓴다. 원래 목적지는 redirectProxyMap에 저장해둔다. 프록시는 GetDestinationInfo(srcPort)로 원래 어디로 가려 했는지 알아낸다.

앱 입장에서는 평소처럼 DB에 연결하는 것 같지만, 실제로는 Keploy 프록시를 거치는 거다. 앱 코드는 아무것도 모른다.

투명 프록시의 프로토콜 파싱

pkg/agent/proxy/의 투명 프록시가 리다이렉트된 트래픽을 받는다:

  1. eBPF가 리다이렉트한 연결을 accept
  2. eBPF 맵에서 원래 목적지를 조회
  3. 첫 바이트로 프로토콜 판별 (등록된 파서들이 MatchType() 호출)
  4. 해당 프로토콜 핸들러로 라우팅

지원하는 프로토콜이 상당하다. HTTP, HTTP2, gRPC, MySQL(와이어 프로토콜 전체 파싱 — 연결 페이즈, 쿼리, prepared statements), PostgreSQL, MongoDB, Redis, Kafka. 그리고 매칭 안 되면 Generic(바이너리 캡처) 폴백.

TLS도 pkg/agent/proxy/tls/에서 자동 생성 CA 인증서로 투명하게 처리한다.

녹화(Record) 흐름

pkg/service/record/record.go가 오케스트레이션한다:

  1. Instrumentation.Setup() — eBPF 훅 로드, 프록시 시작
  2. Instrumentation.Run() — 대상 앱을 실행
  3. 3개의 동시 채널이 열린다:
  • GetIncoming() — 인바운드 API 요청/응답 → TestCase 객체

  • GetOutgoing() — 아웃바운드 호출(DB, 외부 API) → Mock 객체

  • GetMappings() — 어떤 Mock이 어떤 테스트 케이스에 속하는지 연결

    1. YAML로 저장. testdb/에 테스트 케이스, mockdb/에 Mock.

Go의 goroutine과 channel이 이 동시성 파이프라인에 딱 맞는 구조다.

재생(Replay) 흐름

pkg/service/replay/가 테스트를 실행한다:

  1. YAML에서 테스트 케이스+Mock 로드
  2. Mock을 프록시의 인메모리 DB(MockMemDb)에 적재
  3. 녹화된 HTTP/gRPC 요청을 앱에 전송
  4. 앱의 아웃바운드 호출이 프록시에 도달 → 녹화된 Mock 응답
  5. 실제 응답 vs 기대 응답 비교
  6. 디노이징 — 타임스탬프, 랜덤 ID 같은 노이즈 필드를 자동 식별해서 비교 제외
  7. 타임 프리징 — 재생 시 시스템 시간을 고정

커버리지 계산

pkg/platform/coverage/언어별 구현이 있다. Go, Java, Python, JavaScript, C# 각각. 각 언어의 네이티브 커버리지 도구(Go cover, Istanbul, coverage.py, JaCoCo 등)에 훅을 걸어서 statement/branch 커버리지를 수집한다.

AI 통합 (utgen)

pkg/service/utgen/에서 LLM 기반 테스트 확장. 기존 녹화 데이터 + Swagger/OpenAPI 스키마를 읽어서 누락 필드, 경계값, 타입 오류 테스트를 생성한다. prompt.go에서 프롬프트 엔지니어링, coverage.go에서 미커버 경로 식별, injector.go에서 생성된 테스트를 코드베이스에 주입.

Rails를 감싸면 무슨 일이 벌어지는가

keploy record -c "bundle exec rails s" — 이렇게 실행하면 Rails가 네트워크로 하는 모든 일이 녹화된다.

캡처되는 것:

  • 들어오는 HTTP 요청/응답 (API 엔드포인트) → 테스트 케이스가 된다

  • PostgreSQL/MySQL 쿼리 → Mock이 된다. ActiveRecord가 내부적으로 쓰는 SQL이 전부 와이어 프로토콜 레벨에서 잡힌다

  • Redis 호출 (캐시, Sidekiq) → Mock

  • 외부 API 호출 (Faraday, Net::HTTP 등) → Mock

  • Kafka/RabbitMQ 메시지 → Mock

캡처 안 되는 것:

  • SQLite — 네트워크 소켓을 안 쓴다. 파일 I/O라서 eBPF 네트워크 훅에 안 걸린다. 이 프로젝트(rails_start_template)처럼 SQLite 쓰는 앱에서는 DB 쿼리 Mock이 생성되지 않는다

  • 파일 읽기/쓰기

  • 인메모리 연산

  • localhost 내부 프로세스 간 통신 (Unix 도메인 소켓은 일부 지원)

핵심: eBPF는 네트워크 소켓 syscall만 잡는다. TCP/UDP로 통신하는 건 전부 잡히고, 그 외는 안 잡힌다. Rails에서 PostgreSQL을 쓴다면 ActiveRecord가 날리는 SELECT/INSERT/UPDATE가 전부 PostgreSQL 와이어 프로토콜로 캡처돼서, 재생 시 실제 DB 없이 Mock으로 응답한다.

Linux 전용 — macOS/Windows 개발자의 현실

eBPF는 Linux 커널 기능이다. macOS에 없다. Windows에도 없다 (Keploy가 Windows용 별도 훅을 시도하고 있지만 아직 실험적이다).

macOS에서 개발한다면 Docker가 필수다:

keploy record -c "docker compose up" --containerName "app"

Docker Desktop의 Linux VM 안에서 eBPF가 동작한다. 로컬 네이티브 개발 → Docker로 감싸야 하는 부담이 생긴다. CI/CD에서 Linux 컨테이너로 돌리는 건 문제없다.

eBPF 트래픽 캡처 → 테스트 생성 플로우

# 1. eBPF 훅이 커널에 부착
sys_enter_socket, SockOps(cgroup), connect4/6, cgBind4/6
# 2. 아웃바운드 연결 목적지를 프록시로 리다이렉트
App → connect(postgres:5432)
↓ eBPF가 목적지를 127.0.0.1:proxyPort로 덮어씀
↓ 원래 목적지(postgres:5432)는 redirectProxyMap에 저장
App → connect(127.0.0.1:proxyPort) [앱은 모름]
# 3. 투명 프록시가 프로토콜 파싱
첫 바이트 → MatchType() → PostgreSQL 파서 선택
요청/응답 쌍을 Mock 객체로 캡처
# 4. 동시 수집 (3 goroutine channels)
GetIncoming() ──→ TestCase (API 요청/응답)
GetOutgoing() ──→ Mock (DB/외부 API)
GetMappings() ──→ TestCase ↔ Mock 연결
# 5. YAML 저장
testdb/test-1.yaml + mockdb/mock-1.yaml

소스 코드 구조

디렉토리역할
pkg/agent/hooks/linux/eBPF 프로그램, 맵, 커널 훅
pkg/agent/proxy/투명 프록시 + DNS 서버
pkg/agent/proxy/integrations/프로토콜 파서 (HTTP, MySQL, Postgres, Mongo, Redis, Kafka, gRPC)
pkg/service/record/녹화 오케스트레이션
pkg/service/replay/테스트 실행 + 응답 비교
pkg/service/utgen/AI 기반 테스트 생성
pkg/matcher/응답 비교 (HTTP, gRPC, 스키마)
pkg/platform/coverage/언어별 커버리지 수집 (Go/Java/Python/JS/C#)

eBPF 공유 맵 — 리다이렉트의 핵심

맵 이름KeyValue용도
clientRegistrationMapapp PID등록 정보대상 앱 식별
agentRegistrationMapagent PID프록시 포트Keploy 에이전트 식별
redirectProxyMap소스 포트DestInfo (IP+포트)원래 목적지 저장

Go 관점에서 본 설계 포인트

cilium/ebpf 라이브러리로 eBPF 프로그램을 Go에서 직접 로드/관리. goroutine 3개가 채널로 동시에 인바운드/아웃바운드/매핑을 수집하는 파이프라인은 Go 동시성 패턴의 교과서적 사용. 인터페이스로 프로토콜 파서를 추상화해서 새 프로토콜 추가가 RecordOutgoing() + MockOutgoing() + MatchType() 구현만으로 가능.

Ruby에서 Go로

1

eBPF가 커널 syscall(socket/connect/bind)을 가로채서 앱의 아웃바운드 연결을 Keploy 프록시로 리다이렉트

2

투명 프록시가 첫 바이트로 프로토콜 판별 → HTTP/MySQL/Postgres/MongoDB/Redis/Kafka 각각의 파서로 라우팅

3

인바운드(TestCase) + 아웃바운드(Mock)를 3개 goroutine 채널로 동시 수집 → YAML 저장

4

재생 시 Mock을 인메모리 DB에 적재, 녹화된 요청을 앱에 전송, 디노이징으로 타임스탬프 등 노이즈 제거 후 비교

장점

  • 코드 수정 제로 — eBPF가 커널에서 동작하므로 SDK나 라이브러리 설치 불필요
  • 언어 무관 — Go, Java, Python, Node, Rust 등 syscall을 쓰는 모든 언어에서 동작
  • 프로토콜 커버리지가 넓다 — HTTP/gRPC/MySQL/Postgres/MongoDB/Redis/Kafka + TLS 투명 처리

단점

  • Linux 전용 — eBPF는 커널 5.15+ 필요. macOS/Windows에서는 반드시 Docker 안에서 돌려야 한다. 로컬 네이티브 개발 워크플로우가 깨진다
  • SQLite 등 임베디드 DB는 캡처 불가 — 네트워크 소켓을 안 쓰는 통신은 eBPF에 안 걸린다. PostgreSQL/MySQL만 지원
  • 녹화 품질이 실제 트래픽에 의존 — 트래픽이 커버하지 않는 경로는 테스트도 없다. 에러 케이스, 엣지 케이스는 직접 만들어야 한다
  • 디노이징이 완벽하지 않다 — 타임스탬프, 랜덤 ID 등 동적 필드 식별이 실패하면 테스트가 flaky해진다
  • 통합 테스트만 생성 — 비즈니스 로직의 단위 테스트는 만들어주지 않는다. "이 메서드가 올바른 값을 반환하는가" 같은 검증은 여전히 직접 작성해야 한다
  • DB 스키마 변경 시 녹화 데이터가 무효화된다 — 컬럼 추가/삭제하면 Mock의 SQL 응답이 실제와 달라져서 테스트가 깨진다. 재녹화 필요

사용 사례

기존 앱에 테스트가 없는 상태에서 실제 트래픽 기반으로 회귀 테스트를 빠르게 확보하고 싶을 때 DB, 외부 API, 메시지 큐 등 외부 의존성을 Mock으로 분리해서 오프라인 테스트 환경을 만들고 싶을 때