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/의 투명 프록시가 리다이렉트된 트래픽을 받는다:
- eBPF가 리다이렉트한 연결을 accept
- eBPF 맵에서 원래 목적지를 조회
- 첫 바이트로 프로토콜 판별 (등록된 파서들이
MatchType()호출) - 해당 프로토콜 핸들러로 라우팅
지원하는 프로토콜이 상당하다. HTTP, HTTP2, gRPC, MySQL(와이어 프로토콜 전체 파싱 — 연결 페이즈, 쿼리, prepared statements), PostgreSQL, MongoDB, Redis, Kafka. 그리고 매칭 안 되면 Generic(바이너리 캡처) 폴백.
TLS도 pkg/agent/proxy/tls/에서 자동 생성 CA 인증서로 투명하게 처리한다.
녹화(Record) 흐름
pkg/service/record/record.go가 오케스트레이션한다:
Instrumentation.Setup()— eBPF 훅 로드, 프록시 시작Instrumentation.Run()— 대상 앱을 실행- 3개의 동시 채널이 열린다:
GetIncoming()— 인바운드 API 요청/응답 →TestCase객체GetOutgoing()— 아웃바운드 호출(DB, 외부 API) →Mock객체GetMappings()— 어떤 Mock이 어떤 테스트 케이스에 속하는지 연결- YAML로 저장.
testdb/에 테스트 케이스,mockdb/에 Mock.
- YAML로 저장.
Go의 goroutine과 channel이 이 동시성 파이프라인에 딱 맞는 구조다.
재생(Replay) 흐름
pkg/service/replay/가 테스트를 실행한다:
- YAML에서 테스트 케이스+Mock 로드
- Mock을 프록시의 인메모리 DB(
MockMemDb)에 적재 - 녹화된 HTTP/gRPC 요청을 앱에 전송
- 앱의 아웃바운드 호출이 프록시에 도달 → 녹화된 Mock 응답
- 실제 응답 vs 기대 응답 비교
- 디노이징 — 타임스탬프, 랜덤 ID 같은 노이즈 필드를 자동 식별해서 비교 제외
- 타임 프리징 — 재생 시 시스템 시간을 고정
커버리지 계산
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 트래픽 캡처 → 테스트 생성 플로우
소스 코드 구조
| 디렉토리 | 역할 |
|---|---|
| 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 공유 맵 — 리다이렉트의 핵심
| 맵 이름 | Key | Value | 용도 |
|---|---|---|---|
| clientRegistrationMap | app PID | 등록 정보 | 대상 앱 식별 |
| agentRegistrationMap | agent PID | 프록시 포트 | Keploy 에이전트 식별 |
| redirectProxyMap | 소스 포트 | DestInfo (IP+포트) | 원래 목적지 저장 |
Go 관점에서 본 설계 포인트
cilium/ebpf 라이브러리로 eBPF 프로그램을 Go에서 직접 로드/관리. goroutine 3개가 채널로 동시에 인바운드/아웃바운드/매핑을 수집하는 파이프라인은 Go 동시성 패턴의 교과서적 사용. 인터페이스로 프로토콜 파서를 추상화해서 새 프로토콜 추가가 RecordOutgoing() + MockOutgoing() + MatchType() 구현만으로 가능.
Ruby에서 Go로
eBPF가 커널 syscall(socket/connect/bind)을 가로채서 앱의 아웃바운드 연결을 Keploy 프록시로 리다이렉트
투명 프록시가 첫 바이트로 프로토콜 판별 → HTTP/MySQL/Postgres/MongoDB/Redis/Kafka 각각의 파서로 라우팅
인바운드(TestCase) + 아웃바운드(Mock)를 3개 goroutine 채널로 동시 수집 → YAML 저장
재생 시 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 응답이 실제와 달라져서 테스트가 깨진다. 재녹화 필요