관리 메뉴

근묵자흑

커스텀 리소스와 컨트롤러 - CR & CRD 본문

k8s

커스텀 리소스와 컨트롤러 - CR & CRD

Luuuuu 2025. 6. 8. 17:11

컨트롤러란?

쿠버네티스에서 컨트롤러는 리소스가 변경되면 변경이 감지돼 컨트롤러로 이벤트가 전달되고, 이벤트를 받은 컨트롤러는 리소스가 '원하는 상태(desired status)'가 될 때까지 Reconcile이라는 함수를 이용해 조정하는 과정을 수행합니다. 이는 쿠버네티스의 핵심 철학인 선언적 API(Declarative API)를 구현하는 핵심 메커니즘입니다.

 

controller-runtime의 reconcile 패키지에서 "Reconciler implements a Kubernetes API for a specific Resource by Creating, Updating or Deleting Kubernetes objects, or by making changes to systems external to the cluster" reconcile package - sigs.k8s.io/controller-runtime/pkg/reconcile - Go Packages라고 정의하고 있습니다.


코드: https://github.com/kubernetes-sigs/controller-runtime/blob/main/pkg/reconcile/reconcile.go
참고: https://kubebyexample.com/learning-paths/operator-framework/operator-sdk-go/controller-reconcile-function

컨트롤러의 동작 원리

컨트롤러의 동작 방식은 매우 직관적입니다. 현재 state를 모니터링하고 이를 설정한 state와 비교하여 작업하는 것이 전부입니다. 가령 ReplicaSet의 pod가 3개를 유지해야하는데, 두 개뿐이라면 한 개를 신속하게 올려준다는 방식으로 동작합니다.

원하는 상태 (Desired)           현재 상태 (Current)
┌─────────────────┐              ┌─────────────────┐
│ replicas: 3     │              │ replicas: 2     │
│ ● ● ●           │              │ ● ●             │
│ Pod Pod Pod     │              │ Pod Pod         │
└─────────────────┘              └─────────────────┘
         │                                │
         └──────────┐            ┌────────┘
                    │            │
              ┌─────▼────────────▼──────┐
              │     Controller          │
              │  상태 비교 & 조정        │
              │   Reconcile Loop        │
              └─────────────────────────┘
                          │
                          ▼
                   Pod 하나 추가 생성

실제 사례: Deployment의 동작

디플로이먼트를 생성하면 컨트롤러는 스펙 중 replicas의 값을 보고 해당 개수만큼 파드를 만듭니다. 만약 replicas값을 3으로 변경하면 파드가 세 개로 변경됩니다.

변경 전 (replicas: 2)               변경 후 (replicas: 3)
┌──────────────────┐               ┌──────────────────┐
│ Deployment       │               │ Deployment       │
│ replicas: 2      │  ──Update──>  │ replicas: 3      │
│                  │               │                  │
│ ┌──────┐┌──────┐ │               │ ┌──────┐┌──────┐ │
│ │ Pod1 ││ Pod2 │ │               │ │ Pod1 ││ Pod2 │ │
│ └──────┘└──────┘ │               │ └──────┘└──────┘ │
└──────────────────┘               │ ┌──────┐         │
                                   │ │ Pod3 │ ← 새로 생성
                                   │ └──────┘         │
                                   └──────────────────┘

이 과정을 단계별로 살펴보면:

  1. 사용자가 Deployment 매니페스트를 변경
  2. API 서버가 변경 사항을 감지
  3. Deployment Controller가 이벤트를 수신
  4. 현재 상태와 원하는 상태를 비교
  5. 차이가 있다면 조정 작업 수행 (Pod 생성/삭제)

컨트롤러 관리 구조

kubernetes의 controller들은 kube Controller Manager에 의해서 집합적으로 관리되어진다. kube Controller Manager는 controller의 또 다른 타입인데, controller의 상태를 감시하고 오류가 발생한 경우 오류 복구를 시도하거나 자동으로 복구할 수 없는 경우는 사람의 개입을 위해 오류를 보고한다.

이는 컨트롤러 자체도 관리되는 리소스라는 것을 의미하며, 쿠버네티스의 자체 치유(Self-Healing) 특성을 보여줍니다.

2. 커스텀 리소스에 대한 개념

커스텀 리소스의 정의

커스텀 리소스는 쿠버네티스의 기본 리소스(Pod, Service, Deployment 등) 외에 사용자가 정의할 수 있는 추가적인 리소스입니다. custom resource는 CustomResourceDefinition(CRD)를 따르는 API object이다. CRD는 user와 관리자가 kubernetes platform을 자신들의 resource object를 통해 확장시킬 수 있는 것이다.

커스텀 리소스의 활용 사례

예를 들어, Database라는 커스텀 리소스를 생성하고 replicas를 지정하면 replicas 개수에 맞는 데이터베이스 인스턴스가 생성되어 사용자에게 제공됩니다. 커스텀 리소스를 생성하면 커스텀 리소스를 관리하는 컨트롤러가 실제 리소스를 생성하는 것이지요.

실제 업무에서는 다음과 같은 커스텀 리소스들을 만들 수 있습니다:

  • 데이터베이스 인스턴스 (MySQL, PostgreSQL, Redis)
  • 모니터링 설정 (Prometheus, Grafana)
  • 백업 작업 정의
  • 네트워킹 정책
  • 보안 정책

커스텀 리소스 사용 방법

파드를 생성하기 위해 kubectl get pod 명령어를 실행하는 것처럼 Database를 생성하기 위해서 kubectl get database를 실행합니다. YAML 파일로 Database 스펙을 지정하고 해당 스펙에 맞게 Database가 생성되도록 kubectl apply -f database.yaml 명령어를 실행합니다.

3. 커스텀 리소스를 정의하기 위한 CRD

CustomResourceDefinition (CRD) 개요

CRD는 쿠버네티스 API에 새로운 리소스 타입을 등록하는 메커니즘입니다. 쿠버네티스 API를 블록 장난감처럼 묘사했습니다. 마치 블록을 하나 만들어 붙이는 것처럼 커스텀 리소스에 대한 API도 쿠버네티스 API에서 제공하도록 붙여 넣을 수 있기 때문입니다.

CRD 작성 예시

database-crd.yaml과 같이 커스텀리소스데피니션이라는 쿠버네티스 리소스를 매니페스트 파일로 작성하는 것입니다. 이어서 '만든 블럭을 기존 블럭에 붙이는 과정'은 apply(kubectl apply -f database-crd.yaml) 명령어를 실행해서 생성하는 것이라고 할 수 있습니다.

# database-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.example.com
spec:
  group: example.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              engine:
                type: string
                enum: ["mysql", "postgresql", "redis"]
              version:
                type: string
              replicas:
                type: integer
                minimum: 1
              storageSize:
                type: string
          status:
            type: object
            properties:
              phase:
                type: string
              ready:
                type: boolean
              endpoint:
                type: string
  scope: Namespaced
  names:
    plural: databases
    singular: database
    kind: Database

Operator SDK를 통한 CRD 생성

operator-sdk create api를 사용하면 operator와 상호작용하여 동작할 operand에 대한 CRD를 만들 수 있는데 필수적인 API를 만들어준다. operator-sdk create api에 flag를 통해 만드려는 CRD의 version과 API type을 지정할 수 있다.

$ mkdir sample-app
$ cd sample-app/
$ operator-sdk init --domain mysite.com --repo github.com/sample/simple-app
$ operator-sdk create api --group myapp --version v1alpha1 --kind WebApp --resource --controller

Operator Framework 활용

Operator SDK

Operator SDK를 통해서 이전에 정의한 API, 추상화된 common function, code generator, project scaffolding tool을 쉽게 제공해주어 operator project를 처음부터 개발할 수 있도록 해준다.

OLM (Operator Lifecycle Manager)

OLM은 kubernetes cluster 내에서 동작하는 component로 operator의 배포, 업그레이드를 편리하게 해준다. 또한, operator뿐만 아니라 operator에 있는 dependency들에 대해서도 설치와 패치를 도와준다.

OperatorHub

OperatorHub를 통해서 Operator들을 공유하고 배포할 수 있다. 이는 공개된 sw로서 operator를 부담없이 설치하고 자유롭게 사용이 가능한 것이다.


4. 커스텀 리소스와 컨트롤러

컨트롤러 작성의 필요성

커스텀리소스데피니션을 쿠버네티스 클러스터에 등록했다면 이제 컨트롤러를 작성해야 합니다. 컨트롤러의 궁극적인 목표는 리소스 스펙을 충족하는 상태로 만드는 것입니다.

컨트롤러의 핵심 로직

컨트롤러가 담당해야 하는 중요한 두 가지 로직은 아래와 같습니다. Database 리소스가 생성됐을 때 실제 데이터베이스 인스턴스 생성, Database 리소스의 상태를 확인해서 Ready 상태까지 도달했는지 검증

스텝 1: 리소스 생성 처리

처음에 Database 리소스를 생성하면 endpoint 필드가 비어있습니다. 이 필드가 비어있다면 '아직 데이터베이스 인스턴스를 생성하지 않았다'고 판단하고, 데이터베이스 생성 API를 호출한 뒤 이후 endpoint를 전달받으면 Database 리소스에 업데이트합니다.

스텝 2: 상태 검증 및 동기화

endpoint가 있다면 상태를 확인합니다. 아직 Ready가 아니라면 API를 일정 간격으로 계속 호출해서 Ready라는 결과가 나올 때까지 상태를 업데이트하는 과정을 거칩니다.

Operator Pattern

kubernetes operator는 운영 controller 개발을 사용자의 손에 맡긴다. 이는 관리자에게 kubernetes cluster 또는 사용자 정의 application의 모든 측면을 관리할 수 있는 controller를 작성할 수 있는 유연성을 제공한다.

사용자 (Cluster Administrator)
         │
         │ kubectl apply -f custom-resource.yaml
         ▼
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes API Server                                       │
│                                                             │
│ ┌─────────────────┐    ┌─────────────────────────────────┐  │
│ │ Built-in        │    │ Custom Resources (CRD)          │  │
│ │ Resources       │    │                                 │  │
│ │ - Pod           │    │ ┌─────────────────────────────┐ │  │
│ │ - Service       │    │ │ Database                    │ │  │
│ │ - Deployment    │    │ │ spec:                       │ │  │
│ └─────────────────┘    │ │   engine: mysql             │ │  │
│                        │ │   replicas: 3               │ │  │
│                        │ │ status:                     │ │  │
│                        │ │   phase: Ready              │ │  │
│                        │ └─────────────────────────────┘ │  │
│                        └─────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                                  │
                                  │ Watch Events
                                  ▼
┌─────────────────────────────────────────────────────────────┐
│ Custom Controller (Operator)                                │
│                                                             │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Reconcile Logic                                         │ │
│ │                                                         │ │
│ │ 1. Database 리소스 생성 감지                             │ │
│ │ 2. 실제 데이터베이스 인스턴스 생성                        │ │
│ │ 3. 상태 모니터링 및 업데이트                              │ │
│ │ 4. 장애 복구 및 스케일링                                 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                                  │
                                  │ 관리하는 리소스들 (Operands)
                                  ▼
┌─────────────────────────────────────────────────────────────┐
│ 실제 워크로드                                                │
│                                                             │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Database    │ │ ConfigMap   │ │ Service                 │ │
│ │ Pod #1      │ │             │ │                         │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Database    │ │ Secret      │ │ PersistentVolumeClaim   │ │
│ │ Pod #2      │ │             │ │                         │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ ┌─────────────┐                                             │
│ │ Database    │                                             │
│ │ Pod #3      │                                             │
│ └─────────────┘                                             │
└─────────────────────────────────────────────────────────────┘

Operator와 Operand의 관계

operator에 의해 관리되는 component들을 operand라고 한다. 이들은 일종의 application, workload로서 operator에 의해 조정(reconciled)되어진다. 조심해야할 것은 operator는 operand를 조정하는 것을 도와주는 것이지 operand의 일을 대신해주는 것이 아니다.

Custom Resource를 통한 상호작용

cluster관리자는 operator와 상호작용하여 그들의 application을 설정하기 위해서 Custom Resource를 통해 접근해야한다. 즉, operator는 Custom Resource를 사용하여 operator의 설정, option들을 외부로 노출하는 것이다.


심화 학습: 컨트롤러 조금 더 깊이 살펴보기

Informer와 캐시 메커니즘

컨트롤러는 API 서버의 부하를 줄이고 속도를 높이기 위해 오브젝트를 바로 가져오는 것이 아니라 Informer라는 구조체를 이용해서 가져옵니다. Informer에는 캐시 스토리지가 있는데요. 'Watch'를 통해 API 서버에서 오브젝트를 가져와서 캐시에 저장하는 것이죠.

┌─────────────┐  Watch   ┌─────────────────────────────────────────┐
│ API Server  │ ────────> │ Informer                               │
│             │          │                                         │
│ ┌─────────┐ │          │ ┌──────────────┐  ┌─────────────────┐   │
│ │ etcd    │ │          │ │ Reflector    │  │ Cache Storage   │   │
│ │         │ │          │ │              │  │                 │   │
│ └─────────┘ │          │ │ Watch Events │──│ Object Store    │   │
└─────────────┘          │ └──────────────┘  └─────────────────┘   │
                         │                                         │
                         │ ┌─────────────────┐                     │
                         │ │ Event Handler   │                     │
                         │ │ OnAdd/Update/   │                     │
                         │ │ Delete          │                     │
                         │ └─────────────────┘                     │
                         └─────────────────────────────────────────┘
                                            │
                                            ▼
                         ┌─────────────────────────────────────────┐
                         │ Controller                              │
                         │ ┌─────────────┐  ┌─────────────────┐    │
                         │ │ WorkQueue   │  │ Reconcile       │    │
                         │ │             │──│ Function        │    │
                         │ └─────────────┘  └─────────────────┘    │
                         └─────────────────────────────────────────┘

Watch 메커니즘

'Watch'란 객체의 변경 사항을 실시간으로 모니터링하는 기능을 의미합니다. 이 기능을 사용하면 클러스터 내의 리소스(파드, 서비스, 디플로이먼트 등)의 생성, 수정, 삭제 이벤트를 실시간으로 감지할 수 있습니다.

DeltaFIFO 큐 시스템

Reflector라는 컴포넌트에서 streamWatcher라는 구조체를 통해서 전달받은 이벤트 구조체를 DeltaFIFO 큐에 넣습니다. DeltaFIFO 큐는 큐 하나만 있는 게 아니라 맵과 큐로 구성돼 있는데요. 맵에는 전달받은 이벤트 구조체를 Delta 구조체로 변경해 키-값 중 값으로 넣고, 이때 키(ID)는 오브젝트의 namespacedName으로 만들고, 만약 이 키가 큐에 없다면 큐에도 집어넣습니다.

Events from API Server
         │
         ▼
┌─────────────────────────────────────────────────────────────────┐
│ DeltaFIFO                                                       │
│                                                                 │
│ ┌─────────────────────┐         ┌─────────────────────────────┐ │
│ │ Map                 │         │ Queue                       │ │
│ │                     │         │                             │ │
│ │ Key: namespace/name │         │ ┌─────────────────────────┐ │ │
│ │ Value: Delta[]      │    ──── │ │ namespace/name-1        │ │ │
│ │                     │         │ └─────────────────────────┘ │ │
│ │ example/pod-1:      │         │ ┌─────────────────────────┐ │ │
│ │   [{Type: Add,      │         │ │ example/pod-2           │ │ │
│ │     Object: Pod}]   │         │ └─────────────────────────┘ │ │
│ │                     │         │ ┌─────────────────────────┐ │ │
│ │ example/pod-2:      │         │ │ example/pod-3           │ │ │
│ │   [{Type: Update,   │         │ └─────────────────────────┘ │ │
│ │     Object: Pod}]   │         │                             │ │
│ └─────────────────────┘         └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼ Pop
                        ┌─────────────────────┐
                        │ Event Handler       │
                        │ OnAdd/Update/Delete │
                        └─────────────────────┘

 

이벤트 처리 플로우

변경된 오브젝트가 Processor로 전달되면 리스너를 통해 각 타입(updateNotification / addNotification / deleteNotification)에 따라 이벤트 핸들러의 OnUpdate / OnAdd / OnDelete를 호출합니다.

이벤트 핸들러의 OnAdd, OnUpdate, OnDelete는 모두 같은 작업을 합니다. 바로 WorkQueue라는 큐에 푸시하는 것인데요. 이때 큐에는 셋 중 무엇이든 상관없이 모두 NamespacedName만을 갖고 있는 구조체가 들어가며, 이렇게 들어간 구조체는 WorkQueue를 가져오는 goroutine을 통해 Reconcile 함수의 파라미터로 넘어가면서 실행됩니다.

API 서버와 Informer 간의 통신

Informer는 이 이벤트 구조체를 전달하기 위해 쿠버네티스 API 서버를 Watch하고, 쿠버네티스 API 서버는 watchEncoder 구조체를 이용해 이벤트 구조체를 전달합니다.

컨트롤러 전체 동작 순서

컨트롤러 작동 순서는 다음과 같습니다.

┌─────────────┐  1. Watch    ┌─────────────────────────────────────────┐
│ etcd        │ ──────────>  │ API Server                              │
│             │              │                                         │
│ Object      │              │ ┌─────────────────────────────────────┐ │
│ Changes     │              │ │ watchEncoder                        │ │
│             │              │ │ Event: {Type, Object}               │ │
└─────────────┘              │ └─────────────────────────────────────┘ │
                             └─────────────────────────────────────────┘
                                              │ 2. Event Stream
                                              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Informer                                                                 │
│                                                                          │
│ ┌───────────────┐  3. Push   ┌─────────────────┐  4. Cache Update        │
│ │ Reflector     │ ────────>  │ DeltaFIFO Queue │ ──────────────────┐     │
│ │               │            │                 │                   │     │
│ │ streamWatcher │            │ Map + Queue     │                   ▼     │
│ └───────────────┘            └─────────────────┘         ┌─────────────┐ │
│                                        │                 │ Cache       │ │
│                                        │ 5. Pop          │ Storage     │ │
│                                        ▼                 │ (Indexer)   │ │
│ ┌─────────────────────────────────────────────────────┐  └─────────────┘ │
│ │ Event Handler (Processor)                           │                  │
│ │ OnAdd / OnUpdate / OnDelete                         │                  │
│ │                        │                            │                  │
│ │                        │ 6. Push NamespacedName     │                  │
│ │                        ▼                            │                  │
│ │ ┌─────────────────────────────────────────────────┐ │                  │
│ │ │ WorkQueue                                       │ │                  │
│ │ └─────────────────────────────────────────────────┘ │                  │
│ └─────────────────────────────────────────────────────┘                  │
└──────────────────────────────────────────────────────────────────────────┘
                                        │ 7. Pop NamespacedName
                                        ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Controller                                                              │
│                                                                         │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Reconcile Function                                                  │ │
│ │                                                                     │ │
│ │ 8. Get Object from Cache  ──────────> Cache Lookup                  │ │
│ │ 9. Business Logic                                                   │ │
│ │ 10. Update Object Status  ──────────> API Server (POST/PUT)         │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
  1. API 서버는 etcd Watcher를 통해서 이벤트 구조체를 전달합니다.
  2. Reflector는 먼저 그 이벤트 구조체를 DeltaFIFO 큐에 넣어서 캐시 스토리지를 업데이트합니다.
  3. Reflector는 다음으로 이벤트 핸들러에게 알립니다(notify).
  4. 이벤트 핸들러는 전달받은 이벤트의 Name과 Namespace를 유형과 상관 없이 모두 WorkQueue에 넣습니다.
  5. Reconcile 함수가 WorkQueue에서 Name과 Namespace를 가져와 이를 키로 사용해서 캐시 스토리지에서 오브젝트를 가져옵니다.
  6. Reconcile 함수에서 일련의 작업을 진행한 뒤 상태를 업데이트하기 위해 API 서버를 호출(POST/PUT)합니다.

실제 동작 예시

컨트롤러의 작동을 Database에서 replicas를 2에서 3으로 변경하는 작업을 예시로 살펴보면 다음과 같습니다.

  1. API 서버는 replicas가 2에서 3으로 변경됐다는 이벤트를 Watch를 통해 전달합니다.
  2. DeltaFIFO 큐로 들어가서 캐시에 있는 Database 커스텀 리소스의 replicas를 3으로 업데이트합니다.
  3. Notify를 통한 onUpdate가 호출됩니다.
  4. 이벤트 핸들러는 Database의 namespacedName를 WorkQueue에 넣습니다.
  5. Reconcile 함수에서 namespacedName을 기준으로 캐시 스토리지에서 오브젝트를 가져옵니다.
  6. 인스턴스 개수를 확인하고 인스턴스를 하나 더 생성(POST)하는 작업을 진행합니다.

심화 학습: 컨트롤러 조금 더 깊이 살펴보기

Informer와 캐시 메커니즘

왜 Informer가 필요한가요?

Kubernetes 클러스터에서 수천 개의 Pod가 실행 중이라고 상상해보세요. 컨트롤러가 이 Pod들의 상태를 감시하려면 어떻게 해야 할까요?

단순한 접근법의 문제점:

// ❌ 나쁜 예시 - 지속적인 API 호출
for {
    pods, _ := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
    for _, pod := range pods.Items {
        // Pod 상태 확인 및 처리
    }
    time.Sleep(5 * time.Second)  // 5초마다 반복
}

이 방식의 문제점:

  • API 서버 과부하: 매번 전체 목록을 요청
  • 네트워크 낭비: 변경사항이 없어도 계속 통신
  • 지연된 반응: Sleep 시간 동안 변경사항을 놓침
  • 확장성 부족: Pod가 많을수록 성능 저하

Informer 패턴의 등장

Informer는 이러한 문제를 해결하기 위해 **"변경사항만 감시하는 효율적인 메커니즘"**을 제공합니다.

실생활 비유:

  • Polling 방식: 우편함을 5분마다 확인하러 나가는 것
  • Informer 방식: 우편 도착 시 알림을 받는 것

Informer의 핵심 구성 요소

┌─────────────────────────────────────────────────────────────┐
│                     Kubernetes API Server                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ LIST & WATCH
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                         Reflector                           │
│  - API 서버와 연결을 유지하며 변경사항 감시                   	   │
│  - 초기에 LIST로 전체 목록 가져오기                           	 │
│  - 이후 WATCH로 변경사항만 수신                              	  │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ 변경 이벤트
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        DeltaFIFO                            │
│  - 변경사항을 순서대로 저장하는 큐                                  │
│  - Added/Updated/Deleted 이벤트 관리                           │
│  - 중복 이벤트 처리 (같은 객체의 여러 업데이트)                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ Pop (하나씩 꺼내기)
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Controller                             │
│  - DeltaFIFO에서 이벤트를 하나씩 처리                             │
│  - 로컬 캐시(Indexer) 업데이트                                  │
│  - 이벤트 핸들러 호출                                           │
└─────────────────────────────────────────────────────────────┘
                    │                    │
                    ▼                    ▼
         ┌──────────────────┐   ┌──────────────────┐
         │  Indexer (캐시)   │   │  Event Handlers  │
         │  - 로컬 저장소      │   │  - AddFunc       │
         │  - 빠른 조회       │   │  - UpdateFunc    │
         │  - 인덱스 지원      │   │  - DeleteFunc    │
         └──────────────────┘   └──────────────────┘
                                         │
                                         ▼
                                 ┌──────────────────┐
                                 │   Work Queue     │
                                 │  - 처리할 작업      │
                                 │  - 재시도 로직      │
                                 └──────────────────┘

 

각 컴포넌트 상세 설명

1. Reflector - API 서버와의 연결 담당

Reflector는 API 서버와 지속적인 연결을 유지하며 리소스의 변경사항을 감시합니다.

// Reflector의 핵심 동작 원리
type Reflector struct {
    // 무엇을 감시할지 정의
    expectedType reflect.Type
    
    // 어떻게 감시할지 정의 (List와 Watch 기능)
    listerWatcher ListerWatcher
    
    // 변경사항을 어디에 저장할지
    store Store  // 보통 DeltaFIFO
    
    // 재시작을 위한 마지막 동기화 버전
    lastSyncResourceVersion string
}

LIST & WATCH 패턴:

// 1단계: LIST - 현재 상태 가져오기
pods, resourceVersion, err := reflector.listerWatcher.List(metav1.ListOptions{})

// 2단계: WATCH - 변경사항 감시 시작
watcher, err := reflector.listerWatcher.Watch(metav1.ListOptions{
    ResourceVersion: resourceVersion,  // 이 버전 이후의 변경만 감시
})

// 3단계: 변경 이벤트 처리
for event := range watcher.ResultChan() {
    switch event.Type {
    case watch.Added:
        reflector.store.Add(event.Object)
    case watch.Modified:
        reflector.store.Update(event.Object)
    case watch.Deleted:
        reflector.store.Delete(event.Object)
    }
}

2. DeltaFIFO - 변경사항 큐

DeltaFIFO는 "Delta(변경사항) + FIFO(선입선출)" 큐입니다.

왜 일반 큐가 아닌 DeltaFIFO인가요?

예시 상황:

시간 순서:
1. Pod A 생성 (Added)
2. Pod A 수정 (Updated) - 라벨 변경
3. Pod A 수정 (Updated) - 이미지 변경
4. Pod A 수정 (Updated) - 상태 변경

일반 큐라면 4개의 이벤트를 모두 처리해야 하지만, DeltaFIFO는 이를 압축합니다:

// DeltaFIFO 내부 구조
type DeltaFIFO struct {
    items map[string]Deltas  // key: "namespace/name", value: 변경 이력
    queue []string           // 처리 순서
}

// Pod A의 Deltas 예시
Deltas = [
    {Type: Added, Object: podA_v1},
    {Type: Updated, Object: podA_v2},
    {Type: Updated, Object: podA_v3},
    {Type: Updated, Object: podA_v4},
]

컨트롤러는 이를 한 번에 처리하여 최종 상태(podA_v4)로 직접 동기화할 수 있습니다.

3. Indexer - 로컬 캐시

Indexer는 메모리 내 캐시로, 빠른 조회를 지원합니다.

// 인덱서 사용 예시
indexer := cache.NewIndexer(
    cache.MetaNamespaceKeyFunc,  // 기본 키: namespace/name
    cache.Indexers{
        "nodeName": func(obj interface{}) ([]string, error) {
            pod := obj.(*v1.Pod)
            return []string{pod.Spec.NodeName}, nil
        },
    },
)

// 특정 노드의 모든 Pod 조회 (API 호출 없이!)
podsOnNode, _ := indexer.ByIndex("nodeName", "worker-node-1")

캐시의 이점:

  • API 서버 호출 없이 즉시 조회
  • 복잡한 필터링과 검색 가능
  • 메모리만 사용하므로 매우 빠름

SharedInformer - 리소스 공유의 마법

여러 컨트롤러가 같은 리소스를 감시한다면 어떻게 될까요?

// ❌ 비효율적 - 각 컨트롤러가 독립적인 Informer 사용
deploymentController := NewInformer(deploymentLister)  // API 연결 1
replicaSetController := NewInformer(deploymentLister)  // API 연결 2
hpaController := NewInformer(deploymentLister)         // API 연결 3

SharedInformer의 해결책:

// ✅ 효율적 - 하나의 Informer를 공유
sharedInformer := factory.Apps().V1().Deployments().Informer()

// 여러 컨트롤러가 동일한 informer에 핸들러 등록
sharedInformer.AddEventHandler(deploymentControllerHandlers)
sharedInformer.AddEventHandler(replicaSetControllerHandlers)
sharedInformer.AddEventHandler(hpaControllerHandlers)

WorkQueue - 이벤트 처리의 완충지대

Informer에서 바로 처리하지 않고 WorkQueue를 사용하는 이유:

// Informer 이벤트 핸들러
AddFunc: func(obj interface{}) {
    key, _ := cache.MetaNamespaceKeyFunc(obj)
    queue.Add(key)  // 즉시 처리하지 않고 큐에 추가
}

// 별도의 워커에서 처리
func (c *Controller) worker() {
    for c.processNextItem() {
    }
}

func (c *Controller) processNextItem() bool {
    key, quit := c.queue.Get()
    if quit {
        return false
    }
    defer c.queue.Done(key)
    
    // 실제 처리 로직 (시간이 걸릴 수 있음)
    err := c.syncHandler(key)
    
    if err != nil {
        // 실패 시 재시도 (Rate Limiting 적용)
        c.queue.AddRateLimited(key)
        return true
    }
    
    c.queue.Forget(key)  // 성공 시 재시도 기록 삭제
    return true
}

WorkQueue의 장점:

  1. 비동기 처리: Informer가 블록되지 않음
  2. 재시도 메커니즘: 실패한 작업 자동 재시도
  3. 중복 제거: 같은 객체의 여러 이벤트를 하나로 병합
  4. Rate Limiting: 과도한 재시도 방지

WorkQueue 내부 구조 상세 분석

WorkQueue는 단순한 큐가 아닌 여러 자료구조의 조합입니다:

// client-go/util/workqueue/queue.go
type Type struct {
    // queue는 처리 순서를 정의
    queue []t
    
    // dirty는 처리가 필요한 모든 아이템을 저장
    dirty set
    
    // processing은 현재 처리 중인 아이템
    processing set
    
    cond *sync.Cond
}

동작 원리:

┌─────────────────────────────────────────────────────┐
│ WorkQueue 내부 구조                                   │
│                                                     │
│  dirty set          processing set      queue       │
│ ┌─────────┐        ┌─────────────┐    ┌─────────┐  │
│ │ item1   │        │ item2       │    │ item1   │  │
│ │ item3   │        │             │    │ item3   │  │
│ │ item4   │        └─────────────┘    │ item4   │  │
│ └─────────┘                           └─────────┘  │
│                                                     │
│ Add(item) → dirty에 추가, queue에도 추가              │
│ Get() → queue에서 pop, processing으로 이동           │
│ Done(item) → processing에서 제거                     │
└─────────────────────────────────────────────────────┘

Rate Limiting 메커니즘 심화

WorkQueue의 Rate Limiting은 세 가지 주요 구현을 제공합니다:

1. ItemExponentialFailureRateLimiter

// 실패할 때마다 지연 시간이 지수적으로 증가
rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(
    5*time.Millisecond,    // 기본 지연
    1000*time.Second,      // 최대 지연
)

// 동작 예시:
// 1번째 실패: 5ms 대기
// 2번째 실패: 10ms 대기
// 3번째 실패: 20ms 대기
// 4번째 실패: 40ms 대기
// ...최대 1000초까지

2. ItemFastSlowRateLimiter

// 빠른 재시도 후 느린 재시도로 전환
rateLimiter := workqueue.NewItemFastSlowRateLimiter(
    5*time.Millisecond,    // 빠른 재시도 간격
    10*time.Second,        // 느린 재시도 간격
    3,                     // 빠른 재시도 횟수
)

// 동작 예시:
// 1-3번째 실패: 5ms 대기
// 4번째 이후: 10초 대기

3. BucketRateLimiter

// Token Bucket 알고리즘 사용
import "golang.org/x/time/rate"

rateLimiter := &workqueue.BucketRateLimiter{
    Limiter: rate.NewLimiter(
        rate.Limit(10),  // 초당 10개 처리
        100,             // 버스트 100개 허용
    ),
}

실제 사용 예시:

// 여러 Rate Limiter 조합
queue := workqueue.NewRateLimitingQueue(
    workqueue.NewMaxOfRateLimiter(
        // 지수적 백오프
        workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),
        // 전역 rate limit (초당 10개, 버스트 100)
        &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(10, 100)},
    ),
)

// 재시도 로직
func (c *Controller) handleErr(err error, key interface{}) {
    if err == nil {
        c.queue.Forget(key)
        return
    }
    
    // 5번까지 재시도
    if c.queue.NumRequeues(key) < 5 {
        c.queue.AddRateLimited(key)
        return
    }
    
    // 재시도 횟수 초과
    c.queue.Forget(key)
    utilruntime.HandleError(err)
}

전체 흐름 예시: Deployment 업데이트

사용자가 Deployment의 이미지를 변경했을 때의 전체 흐름:

# kubectl apply -f deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.20  # 1.19에서 1.20으로 변경

처리 과정:

  1. API Server: 변경사항 저장 및 이벤트 발생
  2. Reflector: WATCH를 통해 Update 이벤트 수신
  3. DeltaFIFO: Update 이벤트를 큐에 추가
  4. Controller: DeltaFIFO에서 이벤트 Pop
  5. Indexer: 로컬 캐시 업데이트 (새 버전으로)
  6. Event Handler: UpdateFunc 호출
  7. WorkQueue: "default/nginx-deployment" 키 추가
  8. Worker: 큐에서 키를 가져와 처리
    • 현재 상태 확인 (3개 Pod 중 이미지가 다른 것들)
    • 원하는 상태로 조정 (새 ReplicaSet 생성)
    • 점진적 업데이트 시작

실전 예제: 간단한 Pod 감시 컨트롤러

package main

import (
    "fmt"
    "time"
    
    v1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/util/runtime"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/client-go/informers"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/cache"
    "k8s.io/client-go/util/workqueue"
)

type Controller struct {
    indexer  cache.Indexer
    queue    workqueue.RateLimitingInterface
    informer cache.Controller
}

func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller {
    return &Controller{
        informer: informer,
        indexer:  indexer,
        queue:    queue,
    }
}

func (c *Controller) processNextItem() bool {
    key, quit := c.queue.Get()
    if quit {
        return false
    }
    defer c.queue.Done(key)
    
    // 캐시에서 객체 가져오기
    obj, exists, err := c.indexer.GetByKey(key.(string))
    if err != nil {
        fmt.Printf("Fetching object with key %s from store failed: %v\n", key, err)
        return true
    }
    
    if !exists {
        fmt.Printf("Pod %s does not exist anymore\n", key)
    } else {
        pod := obj.(*v1.Pod)
        fmt.Printf("Sync/Add/Update for Pod %s in namespace %s\n", pod.Name, pod.Namespace)
        
        // 여기서 실제 비즈니스 로직 수행
        // 예: Pod 상태 확인, 외부 시스템 업데이트 등
    }
    
    c.queue.Forget(key)
    return true
}

func (c *Controller) Run(workers int, stopCh chan struct{}) {
    defer runtime.HandleCrash()
    defer c.queue.ShutDown()
    
    fmt.Println("Starting Pod controller")
    
    go c.informer.Run(stopCh)
    
    // 캐시 동기화 대기
    if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
        runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
        return
    }
    
    fmt.Println("Pod controller synced and ready")
    
    // 워커 시작
    for i := 0; i < workers; i++ {
        go wait.Until(c.runWorker, time.Second, stopCh)
    }
    
    <-stopCh
}

func (c *Controller) runWorker() {
    for c.processNextItem() {
    }
}

func main() {
    // 클라이언트 설정 (kubeconfig 사용)
    config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    clientset, err := kubernetes.NewForConfig(config)
    
    // SharedInformerFactory 생성
    factory := informers.NewSharedInformerFactory(clientset, time.Second*30)
    podInformer := factory.Core().V1().Pods()
    
    // WorkQueue 생성
    queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
    
    // 이벤트 핸들러 등록
    podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            key, err := cache.MetaNamespaceKeyFunc(obj)
            if err == nil {
                queue.Add(key)
            }
        },
        UpdateFunc: func(old, new interface{}) {
            key, err := cache.MetaNamespaceKeyFunc(new)
            if err == nil {
                queue.Add(key)
            }
        },
        DeleteFunc: func(obj interface{}) {
            key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
            if err == nil {
                queue.Add(key)
            }
        },
    })
    
    controller := NewController(queue, podInformer.Informer().GetIndexer(), podInformer.Informer())
    
    // Informer 시작
    stopCh := make(chan struct{})
    defer close(stopCh)
    
    factory.Start(stopCh)
    controller.Run(2, stopCh)
}

client-go vs controller-runtime 비교

이제 우리는 Informer의 기본을 이해했으니, 두 가지 접근 방식의 차이를 살펴보겠습니다:

client-go 방식 (저수준, 세밀한 제어)

// 모든 컴포넌트를 직접 설정
indexer := cache.NewIndexer(...)
fifo := cache.NewDeltaFIFO(...)
config := &cache.Config{
    Queue:         fifo,
    ListerWatcher: listWatcher,
    Process: func(obj interface{}) error {
        // Delta 처리 로직
    },
}
controller := cache.New(config)

장점:

  • 완전한 제어권
  • 커스텀 로직 구현 가능
  • Kubernetes 내부 동작 이해에 도움

단점:

  • 복잡한 설정
  • 보일러플레이트 코드 많음
  • 실수하기 쉬움

controller-runtime 방식 (고수준, 단순화)

// Manager가 모든 것을 처리
mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{})

// Reconciler만 구현하면 됨
type PodReconciler struct {
    client.Client
}

func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var pod v1.Pod
    if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }
    
    // 비즈니스 로직
    return ctrl.Result{}, nil
}

// 컨트롤러 등록
ctrl.NewControllerManagedBy(mgr).
    For(&v1.Pod{}).
    Complete(&PodReconciler{})

장점:

  • 간단한 사용법
  • 자동화된 설정
  • 베스트 프랙티스 내장

단점:

  • 커스터마이징 제한
  • 내부 동작 숨겨짐
  • 특수한 요구사항 구현 어려움

핵심 차이점: DeltaFIFO 사용 여부

client-go의 DeltaFIFO 직접 사용:

// client-go는 DeltaFIFO를 명시적으로 사용
fifo := cache.NewDeltaFIFO(
    cache.MetaNamespaceKeyFunc,
    indexer,
)

// Process 함수에서 Delta 타입을 직접 처리
Process: func(obj interface{}) error {
    // obj는 Deltas 타입
    for _, d := range obj.(cache.Deltas) {
        switch d.Type {
        case cache.Added:
            // 추가 처리
        case cache.Updated:
            // 업데이트 처리
        case cache.Deleted:
            // 삭제 처리
        }
    }
    return nil
}

controller-runtime의 추상화된 접근:

// controller-runtime은 내부적으로 SharedIndexInformer 사용
// DeltaFIFO는 숨겨져 있고, 개발자는 최종 상태만 받음

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // Delta 타입이 아닌 최종 객체 상태를 받음
    // Added/Updated/Deleted 구분 없이 현재 상태만 처리
    
    obj := &v1.Pod{}
    if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
        if errors.IsNotFound(err) {
            // 객체가 없으면 삭제된 것으로 간주
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }
    
    // 현재 상태 기반으로 처리
    return ctrl.Result{}, nil
}

왜 이런 차이가 존재하는가?

  1. controller-runtime의 철학: "개발자는 비즈니스 로직에만 집중"
    • Delta 타입 처리는 대부분 동일한 패턴
    • 최종 상태만 중요한 경우가 많음
  2. client-go의 유연성: "모든 것을 제어하고 싶을 때"
    • 이벤트 타입별로 다른 최적화 가능
    • 특수한 캐싱 전략 구현 가능

성능 최적화 팁

  1. 적절한 Resync Period 설정
  2. // 너무 짧으면 부하 증가, 너무 길면 일관성 문제 factory := informers.NewSharedInformerFactory(clientset, 10*time.Minute)
  3. 선택적 Watch 사용
  4. // 레이블 셀렉터로 필요한 객체만 감시 listOptions := metav1.ListOptions{ LabelSelector: "app=myapp", }
  5. 적절한 Worker 수 설정
  6. // CPU 코어 수와 처리 복잡도 고려 controller.Run(runtime.NumCPU(), stopCh)

Resync 메커니즘 심화 이해

Resync는 캐시와 실제 상태 간의 일관성을 보장하는 중요한 메커니즘입니다:

// Resync 동작 원리
type sharedIndexInformer struct {
    resyncCheckPeriod time.Duration  // resync 주기
    defaultEventHandlerResyncPeriod time.Duration
    processor *sharedProcessor
    // ...
}

Resync가 필요한 이유:

  1. 네트워크 이슈: Watch 연결 중 일부 이벤트 누락 가능성
  2. 버그 방지: 컨트롤러 로직 오류로 인한 상태 불일치
  3. 외부 변경: API 서버를 통하지 않은 직접적인 etcd 수정

Resync 동작 과정:

┌─────────────────────────────────────────────────────────┐
│ Resync Timer (예: 10분마다)                               │
│                                                         │
│ 1. 캐시의 모든 객체를 순회                                 │
│    ↓                                                    │
│ 2. 각 객체에 대해 Sync 이벤트 생성                         │
│    ↓                                                    │
│ 3. 이벤트 핸들러 호출 (UpdateFunc)                        │
│    ↓                                                    │
│ 4. 컨트롤러가 현재 상태 재확인                             │
└─────────────────────────────────────────────────────────┘

Resync 최적화 전략:

// 리소스별로 다른 Resync 주기 설정
podInformer := factory.Core().V1().Pods().Informer()
podInformer.AddEventHandlerWithResyncPeriod(
    cache.ResourceEventHandlerFuncs{...},
    5*time.Minute,  // Pod는 자주 변경되므로 짧게
)

deploymentInformer := factory.Apps().V1().Deployments().Informer()
deploymentInformer.AddEventHandlerWithResyncPeriod(
    cache.ResourceEventHandlerFuncs{...},
    30*time.Minute,  // Deployment는 덜 자주 변경되므로 길게
)

프로덕션 환경의 일반적인 문제와 해결책

1. 메모리 누수 문제

문제 상황:

// ❌ 잘못된 예시 - 이벤트 핸들러에서 객체 참조 유지
var globalPodCache = make(map[string]*v1.Pod)

AddFunc: func(obj interface{}) {
    pod := obj.(*v1.Pod)
    globalPodCache[pod.Name] = pod  // 메모리 누수!
}

해결책:

// ✅ 올바른 예시 - 필요한 정보만 저장
type PodInfo struct {
    Name      string
    Namespace string
    Phase     v1.PodPhase
}

var podInfoCache = make(map[string]PodInfo)

AddFunc: func(obj interface{}) {
    pod := obj.(*v1.Pod)
    podInfoCache[pod.Name] = PodInfo{
        Name:      pod.Name,
        Namespace: pod.Namespace,
        Phase:     pod.Status.Phase,
    }
}

2. Watch 연결 끊김 처리

문제 감지 및 모니터링:

// Informer 상태 모니터링
func (c *Controller) monitorInformer() {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        if !c.informer.HasSynced() {
            metrics.InformerNotSynced.Inc()
            klog.Error("Informer lost sync")
        }
        
        // Watch 연결 상태 확인
        lastSyncTime := c.informer.LastSyncResourceVersion()
        if time.Since(lastSyncTime) > 5*time.Minute {
            klog.Warning("No updates received for 5 minutes")
        }
    }
}

3. 대규모 클러스터에서의 성능 이슈

문제: 수만 개의 리소스 처리 시 지연

해결책 1 - 샤딩:

// 네임스페이스별로 컨트롤러 샤딩
namespaces := []string{"ns-1", "ns-2", "ns-3"}
for _, ns := range namespaces {
    go runControllerForNamespace(ns)
}

func runControllerForNamespace(namespace string) {
    factory := informers.NewSharedInformerFactoryWithOptions(
        clientset,
        10*time.Minute,
        informers.WithNamespace(namespace),  // 특정 네임스페이스만
    )
    // ...
}

해결책 2 - 선택적 필드 감시:

// 필요한 필드만 감시하여 네트워크 대역폭 절약
listOptions := metav1.ListOptions{
    FieldSelector: "status.phase=Running",
    LabelSelector: "app=critical",
}

4. 이벤트 폭주 (Event Storm) 대응

문제: 대량의 이벤트가 동시에 발생

해결책:

// 적응형 Rate Limiting
type AdaptiveRateLimiter struct {
    baseDelay    time.Duration
    maxDelay     time.Duration
    failureCount map[string]int
    mu           sync.Mutex
}

func (r *AdaptiveRateLimiter) When(item interface{}) time.Duration {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    key := item.(string)
    failures := r.failureCount[key]
    
    // 큐 깊이에 따라 동적으로 조정
    queueDepth := queue.Len()
    if queueDepth > 1000 {
        // 큐가 가득 차면 더 긴 지연
        return r.maxDelay
    }
    
    delay := r.baseDelay * time.Duration(math.Pow(2, float64(failures)))
    if delay > r.maxDelay {
        return r.maxDelay
    }
    return delay
}

디버깅과 트러블슈팅 가이드

1. Informer 이벤트 추적:

// 디버그용 이벤트 로깅
AddFunc: func(obj interface{}) {
    if klog.V(4).Enabled() {
        klog.Infof("Add event: %s", format.Object(obj))
    }
    queue.Add(key)
}

2. 메트릭 수집:

var (
    reconcileTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "controller_reconcile_total",
            Help: "Total number of reconciliations",
        },
        []string{"controller", "result"},
    )
    
    queueDepth = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "controller_queue_depth",
            Help: "Current depth of workqueue",
        },
        []string{"controller"},
    )
)

정리

Informer 패턴은 Kubernetes의 핵심이며, 효율적인 리소스 감시를 가능하게 합니다. 이해해야 할 핵심 포인트:

  1. LIST & WATCH: 초기 상태를 가져오고 변경사항만 감시
  2. 로컬 캐시: API 서버 부하 감소와 빠른 조회
  3. 이벤트 기반: 변경 시에만 반응하는 효율적 구조
  4. WorkQueue: 안정적인 비동기 처리와 재시도

이러한 개념을 이해하면 Kubernetes 컨트롤러 개발의 든든한 기초를 갖추게 됩니다!

마무리

커스텀 리소스와 컨트롤러는 쿠버네티스의 확장성을 보여주는 핵심 기능입니다. operator framework는 이러한 kubernetes 관리를 위한 수동 업무들을 자동으로 해주는 것이 전부이다. 또한, 딱히 새로운 개념이 아닌 것이 kubernetes cluster를 이미 자동화하는 많은 핵심 구성 요소와 기능적으로 크게 다르지 않다.

전체 아키텍처 요약

사용자 ──── CRD 정의 ──── kubectl apply ──── Kubernetes API
 │                                              │
 │                                              │
 └── Custom Resource 생성 ──────────────────────┘
                                                │
                                                │ Watch Events
                                                ▼
                                        ┌───────────────────┐
                                        │ Custom Controller │
                                        │                   │
                                        │ Reconcile Loop:   │
                                        │ 1. 현재 상태 확인  │
                                        │ 2. 원하는 상태 비교│
                                        │ 3. 차이점 조정     │
                                        └───────────────────┘
                                                │
                                                │ 관리
                                                ▼
                                        ┌───────────────────┐
                                        │ Operands          │
                                        │ (실제 워크로드)    │
                                        │ - Pods            │
                                        │ - Services        │
                                        │ - ConfigMaps      │
                                        │ - etc...          │
                                        └───────────────────┘

실제 운영 환경에서 복잡한 애플리케이션을 관리할 때, 커스텀 리소스와 컨트롤러를 통해 도메인 특화적인 관리 로직을 구현할 수 있으며, 이를 통해 운영 부담을 크게 줄일 수 있습니다.


참고 자료

주요 참고 사이트

  1. Kubernetes Operator를 만들어보자 1일차 - Operator란
  2. 쿠버네티스 커스텀 리소스 정의하고 관리하기(feat.컨트롤러)

공식 문서

  1. Kubernetes 공식 문서 - Custom Resources
  2. Kubernetes 공식 문서 - Controllers
  3. CustomResourceDefinition 공식 문서
  4. Kubernetes 아키텍처 공식 문서
  5. etcd 공식 문서

개발 도구 및 프레임워크

  1. Operator Framework
  2. Operator SDK
  3. Kubebuilder