콘텐츠로 이동

출시 안내: 새로운 Rust 기반 클러스터 에이전트


tl;dr 클러스터 에이전트를 Go에서 Rust로 마이그레이션했으며, 이제 더 작고 더 적은 메모리를 사용합니다. 새 클러스터 에이전트를 사용하려면 최신 릴리스(cli/v0.8.2, helm/v0.15.2)로 업그레이드하세요. 여기에서 직접 체험해볼 수도 있습니다.


최근 클러스터 에이전트를 Go에서 Rust로 마이그레이션하기로 결정했습니다. 이제 재작성이 완료되어 기쁘게 발표할 수 있습니다. 결과물은 CPU 사용량을 최소로 유지(~0.1%)하면서 이미지 크기가 57% 감소(10MB)하고 메모리 사용량이 70% 감소(~3MB)한 클러스터 에이전트입니다.

Kubetail의 첫 번째 버전은 클러스터 내에서 실행되어 웹 브라우저를 통해 사용자에게 로그를 제공하도록 설계되었습니다. 이 버전에서 백엔드의 주된 역할은 Kubernetes API에 요청을 보내고 응답을 실시간으로 프론트엔드에 전달하는 것이었습니다. Python, JavaScript 등 몇 가지 옵션을 검토한 끝에 Go를 선택했습니다. Kubernetes API를 잘 지원하고, 멀티스레딩 지원이 뛰어나며, 빠른 실행 파일과 작은 Docker 이미지를 생성하기 때문입니다.

다음 버전의 Kubetail에서는 웹 대시보드를 로컬에서 실행할 수 있는 kubetail CLI 도구가 추가되었습니다. CLI 도구 구현에도 Go를 다시 선택했습니다. 훌륭한 CLI 인터랙션 라이브러리(spf13 감사합니다!)가 있고, 크로스 플랫폼 지원이 뛰어나며, 무엇보다 클러스터 내 대시보드에서 사용하던 Go 기반 웹 앱을 재사용할 수 있었기 때문입니다.

그때까지 Kubetail은 Kubernetes API를 통해서만 로그를 가져왔습니다. 하지만 로그 파일 크기, 마지막 이벤트 타임스탬프 등 Kubernetes API에서 노출되지 않는 데이터를 활용한 새 기능을 추가하고 싶어지면서, 각 노드의 원시 로그 파일에 직접 접근할 수 있는 에이전트가 필요하다는 것을 깨달았습니다. 다른 언어를 사용할 수도 있었지만, 가장 잘 알고 있고 지금까지 잘 활용해 온 Go를 다시 선택했습니다. 다행히 Go는 에이전트 인터페이스로 자연스러운 선택인 gRPC도 잘 지원했습니다.

당시 앱의 기능 범위를 고려했을 때, 데스크톱과 클러스터 모두에서 잘 작동한 Go를 처음 선택한 것에 매우 만족했습니다. 그러다 가장 많이 요청된 기능인 로그 검색을 어떻게 구현할지 고민하기 시작했습니다.

로그 검색을 고민하면서, 전문 검색 인덱스 대신 grep을 사용하고 싶었습니다. 대부분의 사용 사례에 충분하고, 사용자에게 전문 검색 인덱스 유지에 따른 리소스 부담을 주고 싶지 않았기 때문입니다. 동시에 개인적으로 rg를 사용해 로그를 grep해온 터라 그 속도에 깊은 인상을 받았고, grep 솔루션을 찾으면서 이것을 활용할 수 있을지 궁금해졌습니다. 그때 rg가 라이브러리 형태로도 사용 가능하다는 것을 알게 되었는데, 한 가지 조건이 있었습니다. Rust로 작성되었다는 것이었습니다.

커스텀 코드를 작성하기 전에 exec.Command를 사용해 stdin/stdout으로 rg를 외부 실행 파일로 활용하는 방법을 먼저 검토했습니다. 기본적인 사용 사례에서는 잘 동작했지만, 시간 필터, ANSI 이스케이프 시퀀스 처리, JSON 형식 줄 지원 등 커스텀 기능을 추가하면서 관리하기 어려워졌습니다. 결국 커스텀 로그 파일 grepper를 직접 작성하기로 결심했습니다. Go로 작성하는 것도 잠시 검토했지만, 성능과 견고성을 위해 rg 뒤에 있는 라이브러리인 ripgrep을 사용하고 싶었고, 그러려면 Rust로 작성해야 했습니다.

당시에는 클러스터 에이전트 전체를 Rust로 재작성하고 싶지 않았기 때문에, Go에서 Rust를 호출하는 방법(예: rustgo)을 찾아보다가 커스텀 Rust 코드를 별도의 실행 파일로 유지하고 exec.Command를 통해 Go에서 호출하는 방식을 택했습니다. 코드를 최대한 단순하게 유지하기 위해 공유 protocol buffers 스키마를 사용하고 stdin/stdout 인터페이스에서 직렬화/역직렬화를 구현했습니다.

검색 기능을 출시한 후 커뮤니티가 성장하면서 저보다 훨씬 많은 Rust 경험을 가진 두 명의 개발자를 만났습니다. Christopher Valerio(freexploit)와 Giannis Karagiannis(gikaragia)입니다. 처음에는 두 사람이 Rust 코드를 개선하기 시작했고, 코드베이스에 익숙해지면서 클러스터 에이전트에서 Go와 Rust 사이의 임피던스 불일치를 없애는 방법에 대해 논의하게 되었습니다. 검색 기능과는 별개로, 클러스터 에이전트는 클러스터의 모든 노드에서 실행되기 때문에 가능한 한 성능이 좋고 가벼워야 했는데, 이는 정확히 Rust가 빛을 발하는 사용 사례였습니다. 이러한 아이디어들이 떠오르는 가운데, 커뮤니티 회의에서 전체 에이전트를 Rust로 마이그레이션하는 아이디어를 논의했습니다. 두 사람 모두 열정적으로 참여하겠다고 해서 바로 진행하기로 했습니다!

결정을 내린 후 Christopher와 Giannis가 작업에 착수했습니다. Christopher는 프로젝트의 초기 고수준 아키텍처를 정의하고 GitHub에 초기 이슈들을 만들었습니다. 그 다음 Giannis가 참여하여 기능 구현, 테스트 작성, 다른 기여자들의 도움을 받을 수 있도록 추가 이슈들을 만들었습니다. Giannis는 단 몇 주 만에 Go 기반 클러스터 에이전트와 동등한 기능을 구현했고, 약 일주일간의 추가 테스트 후 코드를 main에 머지할 준비가 되었다고 판단했습니다.

저는 최근에야 Rust를 배우기 시작했기 때문에 Giannis의 풀 리퀘스트를 검토하는 데 Claude Code와 Codex CLI가 큰 도움이 되었습니다. Giannis도 자신의 작업에 챗봇을 활용하고 있었기에, 이는 GitHub 풀 리퀘스트를 통해 이루어진 진정한 인간-봇 협업이었습니다. 핵심적인 이점 중 하나는 에이전트가 잘 정의된 gRPC 인터페이스를 사용하기 때문에 protocol buffers 스키마를 재사용하고, Rust 기반 에이전트가 Go 기반 버전과 기능적으로 동등해지는 시점에 스위치를 전환하는 방식을 취할 수 있었다는 점입니다. Rust 기반 gRPC 서버를 구축하는 데는 tonic을 사용했는데, 직관적이었으며 Go 기반 gRPC 서버와 비교해 사소한 차이점만 있었습니다.

최종 결과물은 CPU 사용량을 최소로 유지(~0.1%)하면서 이미지 크기가 57% 감소(10MB)하고 메모리 사용량이 70% 감소(~3MB)한 클러스터 에이전트입니다. 게다가 이제 코드 전체가 같은 언어로 작성되어 훨씬 다루기 쉬워졌습니다.

저희의 미션은 단순하고 가벼운 패키지로 강력한 로깅 도구를 사용자에게 제공하는 것입니다. 하지만 Kubernetes API는 로깅 기능이 제한적이어서 더 고급 기능을 활성화하려면 각 노드의 원시 로그 파일에 직접 접근이 필요합니다. 바로 그 역할을 클러스터 에이전트가 담당합니다. 에이전트는 앞으로 구현할 모든 기능의 토대입니다.

물론 사용자들이 클러스터에 에이전트를 설치하는 것에 신중한 것은 충분히 이해합니다. 유용성 외에도 에이전트는 작고, 빠르고, 안전해야 합니다. Rust 마이그레이션은 이러한 요구 사항에 대한 저희의 답변입니다. 이미지 크기를 절반 이상 줄이고 메모리 사용량을 70% 감소시킴으로써 Kubetail 에이전트는 리소스가 가장 제한된 환경에서도 배포할 수 있을 만큼 작아졌습니다.

하지만 이것은 시작에 불과합니다. Rust를 통해 CPU와 메모리를 최소한으로 사용하면서 클러스터 내에서 디스크의 파일을 직접 실시간으로 처리하는 가능성의 한계를 계속 넓혀나갈 것입니다. 지금은 로그에 집중하고 있지만, 같은 접근 방식은 메트릭, 알림, 그 외 다양한 유형의 옵저버빌리티 데이터에도 적용됩니다.

앞으로의 계획에 기대가 크며, 여러분도 함께해 주시길 바랍니다. 저희가 하는 일에 공감하고 코드 기여나 사용자로서 피드백을 공유하고 싶다면 Discord에서 함께해 주세요.