안녕하세요! 길었던 KubeRay 시리즈의 마지막 편입니다.
- 1부에서는 LLM 서빙에 왜 Ray가 적합한지 고민했고,
- 2부에서는 RayCluster로 단단한 기반을 다졌으며,
- 3부에서는 RayService와 vLLM을 활용해 실제 LLM 서빙 API를 구축했습니다.
지금까지의 과정은 kubectl apply -f 명령어를 통해 수동으로 YAML 파일을 관리하는 방식이었습니다. 하지만 여러 모델을 관리하고, 여러 사용자가 함께 사용하는 환경에서는 이런 수동 방식은 명확한 한계를 가집니다.
이번 4부에서는 이 모든 과정을 자동화하고 사용자 친화적으로 만들기 위해, 어떻게 Ray Managed API를 설계하고 구현했는지 그 여정을 공유하며 시리즈를 마무리하고자 합니다.
1. 왜 API가 필요했을까?: 복잡성 추상화
수십 줄에 달하는 RayCluster와 RayService YAML 파일을 매번 작성하고 관리하는 것은 번거롭고 실수의 여지가 많습니다. 특히 쿠버네티스나 KubeRay에 익숙하지 않은 사용자에게는 큰 진입장벽이 됩니다.
제가 원했던 것은 사용자가 KubeRay의 복잡한 CRD 구조를 몰라도, 다음과 같이 간단하게 자신의 요구사항을 표현할 수 있는 환경이었습니다.
- "저는 A100 GPU 1개를 사용하는 Ray 클러스터가 필요해요."
- "Hugging Face의
gpt2모델을 vLLM 엔진을 사용해서 배포하고 싶어요."
이러한 'Managed Service' 경험을 제공하기 위해, 모든 복잡성을 뒤로 숨겨주는 API 서버를 구축하기로 결정했습니다.
2. Ray Managed API 설계
핵심 API 엔드포인트를 다음과 같이 설계했습니다.
Note: 실제 구현에서는 네임스페이스와 클러스터 이름을 경로에 포함하여 (
/api/v1/clusters/{cluster_name}/namespaces/{namespace}/services) 리소스를 더 명확하게 구분했지만, 이 글에서는 핵심 아이디어를 보여주기 위해 경로를 간소화했습니다.
-
클러스터 관리 (
/clusters): RayCluster 생성, 조회, 삭제 -
서비스(모델) 관리 (
/services): RayService 배포, 조회, 중단 -
Job 관리 (
/jobs): RayJob 제출, 조회
API 설계의 교훈: CRD 위에서 똑똑하게 일하기
단순히 CRD를 감싸는 것을 넘어, 안정적이고 예측 가능하게 동작하는 API를 만들기 위해서는 몇 가지 중요한 원칙이 필요했습니다.
1. 제약의 미학: 필수 필드에 집중하고 API를 단순하게 유지하기
KubeRay CRD는 매우 많은 속성을 가지고 있습니다. 모든 속성을 API 파라미터로 노출하는 것은 유연해 보이지만, 오히려 API의 복잡도를 높이고 사용자를 혼란스럽게 만듭니다. 저는 유연성보다는 명확성과 안정성을 선택했습니다.
가장 먼저 CRD가 동작하기 위한 필수 필드가 무엇인지 파악했습니다. 이는 kubectl get crd rayclusters.ray.io -o yaml과 같은 명령어로 CRD의 스키마 정의(openAPIV3Schema) 내 required 항목을 통해 확인할 수 있습니다.
API 설계 시 이 필수 필드들은 반드시 포함시키거나 안정적인 기본값을 설정해주고, 그 외에는 사용자들이 가장 빈번하게 사용할 것이라 예상되는 핵심적인 옵션들만 선별적으로 노출했습니다. 이는 의도적인 제약을 통해 오히려 더 나은 사용자 경험을 제공하는 중요한 설계 원칙이었습니다.
2. 수정은 Patch 대신 Put으로: 복잡성을 피하는 전략
리소스 수정 시 부분 업데이트를 지원하는 PATCH 메서드는 매우 유용하지만, CRD와 같이 깊고 복잡한 구조를 다룰 때는 오히려 독이 될 수 있습니다. 예를 들어 workerGroupSpecs 배열에서 특정 워커 그룹의 복제본 수를 변경하거나, 새로운 워커 그룹을 추가하거나, 기존 그룹을 삭제하는 로직을 PATCH로 구현하는 것은 매우 복잡합니다.
그래서 저는 수정 로직을 PUT 메서드 기반으로 설계했습니다. 사용자가 수정을 요청하면, API 서버는 기존 상태를 가져와 요청된 변경사항을 적용한 완전한 새 템플릿(YAML)을 생성한 뒤, 이를 쿠버네티스에 제출하여 리소스 전체를 **교체(replace)**하도록 했습니다. 이 방식은 구현의 복잡도를 크게 낮추고, 예측 불가능한 상태 변경을 방지하여 안정성을 높여주었습니다.
3. 오퍼레이터와의 상호작용: 보이지 않는 규칙 이해하기
Managed API는 결국 KubeRay 오퍼레이터의 클라이언트입니다. 따라서 오퍼레이터가 어떻게 동작하는지 이해하는 것이 매우 중요했습니다.
예를 들어, RayService를 수정할 때 파드 템플릿(template)의 변경사항이 없다면 KubeRay는 RayCluster를 재시작(rolling update)하지 않습니다. 만약 API가 이 동작을 인지하지 못하고 단순하게 CR만 수정한 뒤 '성공'을 반환한다면, 사용자는 변경사항이 즉시 적용될 것이라 기대했지만 실제로는 아무 일도 일어나지 않는 상황이 발생할 수 있습니다.
이처럼 API는 리소스를 수정하기 전에 현재 상태를 확인하고, 오퍼레이터의 동작 규칙을 고려하여 사용자의 기대와 실제 동작이 일치하도록 설계되어야 합니다.
3. 프로젝트 회고
가장 큰 배움: 사용자가 아닌 '운영자'의 관점을 얻다
이번 프로젝트를 통해 얻은 가장 큰 수확은 단순히 'Ray를 어떻게 사용하는가'를 넘어, **'Ray를 어떻게 안정적으로 운영할 것인가'**에 대한 시각을 갖게 된 것입니다. 사용자의 입장에서 기능을 바라보는 것을 넘어, 수많은 사용자들이 이 시스템을 문제없이 사용하게 하려면 어떤 점들을 고려해야 하는지, 즉 Ops의 관점에서 고민하게 된 것이 가장 큰 성장 동력이었습니다.
아쉬웠던 점 (Lessons Learned)
- 동적 설정 관리의 복잡성: 사용자의 간단한 요청("GPU A100 1개")을 완전한 KubeRay YAML로 변환하는 로직은 생각보다 복잡했습니다. 다양한 옵션 조합을 처리하고, 유효성을 검사하며, 안정적인 템플릿을 동적으로 생성하는 부분을 견고하게 만드는 데 많은 노력이 필요했습니다.
- 상태 동기화 및 에러 핸들링: API가 쿠버네티스에 리소스 생성을 요청하는 것은 비동기적으로 처리됩니다. RayCluster가 생성되는 도중 문제가 발생했을 때, 그 실패 상태를 사용자에게 명확하게 전달하고 비정상적인 리소스를 정리하는 등, API의 상태와 실제 클러스터의 상태를 일관성 있게 유지하는 로직을 구현하는 것이 까다로운 과제였습니다.
4. 앞으로의 과제 (Future Work)
1. 모니터링 시스템 고도화 및 플랫폼 통합
현재 구축된 모니터링 시스템만으로는 운영에 한계가 있었습니다. 특히 기존 클러스터 관리 플랫폼에 Ray의 상태를 효과적으로 표현하는 것이 어려웠습니다. 앞으로 Ray의 상태를 더 깊이 있게 수집하고, 이를 운영자가 직관적으로 이해할 수 있도록 플랫폼에 통합하는 작업을 진행할 것입니다.
2. 사용자 경험(UX) 개선을 위한 심층적 추상화
솔직하게 고백하자면, 저는 Ray의 모든 것을 완벽하게 이해하고 이 프로젝트를 시작하지 않았습니다. 요구사항이 발생하면 분석, 설계, 구현을 빠르게 반복하는 애자일한 방식으로 시스템을 발전시켰습니다. 이 접근법은 빠르게 결과물을 만들어내는 데 효과적이었지만, 때로는 API가 Ray의 개념을 너무 직접적으로 노출하여 사용자 친화성이 부족한 결과로 이어지기도 했습니다.
앞으로는 그동안 축적된 운영 경험과 Ray에 대한 깊어진 이해를 바탕으로, API를 사용하는 '사용자'의 입장에서 시스템을 다시 한번 점검하고 개선해나갈 계획입니다.
시리즈를 마치며
지금까지 KubeRay를 활용하여 LLM 서빙 인프라를 구축하고, 이를 관리하기 위한 API를 설계하는 여정을 함께해주셔서 감사합니다. 이 시리즈가 쿠버네티스 환경에서 Ray를 도입하고자 하는 분들께 작은 도움이 되었기를 바랍니다.
Top comments (0)