tech

Kubernetes Operator 패턴 구현체를 빠르게 탐색해보자

Kubernetes Operator는 사용자 정의 리소스인 CRD(Custom Resource Definition)를 감시하고, 변경 사항이 감지되면 지정된 로직을 실행하여 Kubernetes 오브젝트를 원하는 상태로 유지하는 역할을 한다. Operator는 Kubernetes API를 활용하여 지속적으로 리소스 상태를 모니터링하고, 필요한 동작을 수행하는 사용자 정의 컨트롤러로, 종료되지 않은 루프를 실행하며 동작한다. Kubernetes에서 운영되는 많은 오픈 소스는 이 operator 패턴을 구현하고 있으며 대표적으로 argocd, agones, keda 등이 있다.

한편, 직접 operator 패턴을 탐색해야 하는 경우도 있다. 예를 들면 다음과 같다.

Kubernetes 기반 오픈 소스의 동작 원리를 분석하는 경우
운영 중인 operator의 버그를 해결해야 하는 경우
직접 operator를 개발해야 하는 경우

이번 기사에서는 오픈 소스에서 Operator 패턴을 쉽게 탐색할 수 있는 지점을 소개하고 분석하는 것을 목표로 한다.

Kubernetes API는 클러스터 내 모든 리소스를 관리하는 핵심 인터페이스이다. 이를 활용하면 클러스터 내부 또는 외부에서 리소스를 생성, 삭제, 업데이트할 수 있다. 예를 들어, Go 언어에서 Kubernetes API를 쉽게 호출할 수 있도록 도와주는 kubernetes/client-go 패키지를 활용하여 모든 Pod를 조회하는 코드는 다음과 같다.

해당 코드를 살펴보면, 먼저 클러스터 접근 권한을 획득한 후 특정 네임스페이스인 kube-system 내의 모든 파드를 조회하는 것을 확인할 수 있다. 이와 같은 방식으로 Kubernetes의 다양한 리소스 변화를 감지할 수 있다.

Kubernetes API 중에서도 특히 중요한 두 가지 API는 바로 List와 Watch이다.

List: 특정 리소스를 한 번에 모두 조회하는
API Watch: 특정 리소스의 변화를 지속적으로 감지하는 API

이 두 API는 Kubernetes 내 리소스의 상태를 지속적으로 파악하는 데 중요한 역할을 한다. 예를 들어, 특정 CRD를 감시하는 Operator가 있다고 가정해 보겠다. 이 Operator는 처음 실행될 때 기존에 존재하는 리소스 목록을 가져와야 하며, 이후에는 변경 사항을 지속적으로 감지해야 한다. 이때 List API는 Operator가 처음 실행될 때 모든 리소스를 가져오는 역할을 하고, Watch API는 이후 지속적으로 리소스 변화를 감지하는 역할을 수행한다. 이를 통해 Kubernetes 클러스터 내에서 원하는 리소스를 효과적으로 관리할 수 있다.

하지만 Kubernetes API를 직접 활용하여 리소스의 변화를 추적하기는 불편하다. 관련된 단점은 다음과 같다.

  • 성능 및 부하: List API는 리소스 목록 전체를 가져오는 반면, Watch API는 연결이 끊어진 경우 다시 List 요청을 통해 재시작해야 한다.
  • 코드 복잡성: Watch API를 사용하면 재연결 및 이벤트 처리 로직을 직접 관리해야 하므로 코드의 복잡성이 증가한다.
  • 데이터 정합성: List API는 특정 시점의 스냅샷을 제공하지만, 이후 변경 사항을 즉시 반영하지 못한다. Watch API는 장애 발생 시 수동으로 복구해야 한다.

이와 같은 이유로 List 및 Watch API를 직접 활용하려면 성능 최적화, 데이터 정합성, 코드 유지보수성을 직접 관리해야 하는데 이를 직접 처리하기는 쉽지 않다. 지속적인 감시가 필요한 경우, informer를 활용하는 것이 좋다. informer는 내부 캐싱을 통해 API 서버의 부하를 줄이고 성능을 최적화하며, 데이터 정합성을 구현할 필요 없이 이벤트 핸들러를 통해 간단하게 리소스 변경을 감지하고 처리한다.

다음은 kubernetes/client-go 패키지를 활용하여 informer를 사용하는 예시 코드이다.

사용자는 informer의 AddEventHandler메서드를 호출하여 이벤트를 등록할 수 있다. 일반적인 오픈 소스 프로젝트의 경우, informer AddEventHandler 내부의 UpdateFunc이 어떻게 구현되어 있는지 살펴보는 게 중요하다. 이번에는 예시를 확인해 보자.

argocd 사례

다음은 argocd-2.14.4 코드의 일부분이다. 이 코드에서 informer는 argocd의 custom resource인 application에 대한 이벤트가 발생했을 때 처리 로직을 구현하고 있다.

agones 사례

다음은 agones-1.47.0 코드의 일부분이다. 이 코드에서 informer는 agones의 custom resource인 game server에 대한 이벤트가 발생했을 때 처리 로직을 구현하고 있다.

따라서 Kubernetes 기반으로 동작하는 operator에서 어떤 오브젝트가 변화를 감지하기 위해 Watch API나 informer를 활용해야 한다. 대부분의 오픈소스 operator는 informer를 활용한다. 그러므로 informer의 UpdateFunc을 추적하면 오브젝트가 operator 내부로 진입한 이후의 동작을 추적할 수 있다.

workqueue

informer에서 이벤트로 전달된 데이터는 자체적으로 queue를 구현해서 사용하는 경우도 있지만, 일반적인 경우 kubernetes/client-go에서 제공하는 workqueue를 사용하여 비동기로 처리를 수행한다. 이때 workqueue에 이벤트를 발행하는 주요 메서드는 다음과 같다.

  • Add
  • AddAfter
  • AddReteLimited

가장 많이 사용되는 메서드는 AddRateLimited이고 이 메서드는 workqueue중 TypedRateLimitingInterface가 제공한다. 위 argocd 코드 예시에서 AddRateLimited를 이용하여 queue에 원소를 발행하는 것을 확인할 수 있다. agones의 경우에는 queue를 자체적으로 구현해서 사용한다. 한편 queue에서 원소를 구독하여 가져오는 메서드도 제공하는데 이 역할은 Get이 수행한다.

argocd 사례

argocd의 application controller의 경우 for 루프 내에서 계속 queue에서 원소를 가져와 로직을 호출하는 방식으로 이벤트를 처리하도록 구현하고 있다.


지금까지 Kubernetes API를 활용하여 Kubernetes 내부의 변화를 추적하고 operator 내부에서 이벤트를 처리하는 로직에 전달되는 과정을 살펴봤다.

informer 또는 Watch API를 활용하여 리소스 변화 감시
리소스 변화 이벤트를 queue에 발행
goroutine을 활용하여 비동기로 queue에 있는 원소를 꺼내 operator 로직 실행

이 흐름을 코드 상에서 빠르게 확인하려면, 다음 키워드를 프로젝트 내에서 검색한다.

  • informer 관련: UpdateFunc:, informer(, WatchFunc
  • Watch API 관련: .Watch(
  • queue 관련: workqueue., queue.Get(), AddRateLimited(

queue가 실행되는 부분을 찾았다면 해당 부분부터 operator 내부 로직이 실행될 가능성이 높다.

informer와 workqueue를 활용하면 Kubernetes에서 발생하는 이벤트를 효율적으로 감지하고 처리할 수 있다. 특히 오픈 소스 프로젝트에서는 이러한 패턴이 널리 사용되며, 이를 이해하면 Kubernetes 기반 애플리케이션을 설계하거나 디버깅할 때 많은 도움이 된다. 추가적으로, 더 깊이 있는 분석이 필요하다면 각 프로젝트의 queue 처리 흐름을 따라가며 operator 내부 로직이 어떻게 작동하는지 확인하는 것도 좋은 방법이다.

김영준 기자

이 글이 kubernetes operator를 분석하는 분들이 처음에 프로젝트를 쉽게 탐색할 수 있도록 도움을 줄 수 있다면 좋겠습니다.


TOP