Controller in Kubernetes

1. 컨트롤러가 왜 필요한가?

쿠버네티스는 선언적(Declarative) 시스템이다. 사용자는 “nginx 3개를 띄워줘”라고 **원하는 상태(Desired State)**만 선언하고, 어떻게 할지는 말하지 않는다.

API Server는 이 Desired State를 etcd에 저장하지만, 실제로 Current State를 Desired State에 맞추는 일은 하지 않는다.

💡 컨트롤러 = Desired State와 Current State 사이의 간극(gap)을 메우는 주체

왜 이런 구조인가? (인과적 추론)

  • 원인: 분산 시스템에서는 노드가 죽고, 네트워크가 끊기고, Pod가 갑자기 사라질 수 있다. 현재 상태는 항상 불안정하다.
  • 결과: “한 번 명령 → 끝”이 아니라, 지속적으로 감시하면서 차이가 생길 때마다 자동으로 복원하는 구조가 필요하다.
  • 비유: 에어컨 온도 조절기(thermostat). “25도”로 설정하면, 현재 온도를 계속 측정하고 목표와 차이가 나면 냉방/난방을 켜는 것과 같다.

2. Reconciliation Loop (Control Loop)

컨트롤러의 핵심 동작 원리:

1. 현재 상태(Current State) 관찰 → "지금 nginx Pod가 2개 떠 있네" 2. 원하는 상태(Desired State) 확인 → "etcd에는 3개라고 적혀 있네" 3. 차이(Diff) 계산 → "1개가 부족하네" 4. 조정(Reconcile) 실행 → "Pod 1개를 더 생성하자"

이 루프를 Reconciliation Loop 또는 Control Loop이라 부른다.


3. 변화 감지 방식: Watch와 Informer

image.png

3-1. Polling vs Event-driven

컨트롤러가 “현재 상태가 변했는지”를 알아내는 방법은 두 가지:

방식설명문제점
Polling주기적으로 API Server에 “지금 상태 알려줘” 요청컨트롤러 수 × 리소스 수 = API Server 과부하
Watch (Event-driven)API Server와 long-lived 연결을 맺고, 변경 시 이벤트를 push 받음연결 끊기면 이벤트 유실
  • 쿠버네티스의 선택: Watch (Event-driven)
  • 이유: 변경이 없으면 트래픽도 없음. API Server 부하가 리소스 수가 아닌 변경 빈도에 비례하므로 대규모 클러스터에서도 안정적.

3-2. Informer = List-Watch + 로컬 캐시

Watch만으로는 부족한 문제가 두 가지 있다:

문제해결 방법
Watch 연결 끊김 → 이벤트 유실List-Watch 패턴: 처음에 List로 전체 스냅샷 가져오고, 이후 Watch. 끊기면 다시 List부터 재동기화
여러 컨트롤러가 API Server에 동시 조회 → 부하로컬 캐시: Informer가 데이터를 로컬에 저장. 컨트롤러는 API Server가 아닌 캐시에서 읽음

💡 Informer = List-Watch + 로컬 캐시를 묶어놓은 구조

3-3. Work Queue (Informer와 별도 구조)

Informer 밖에 붙는 별도 구조. 이벤트 처리의 정합성 문제를 해결한다.

Work Queue가 필요한 이유 (예시): 1초 안에 이벤트 3개 수신: “Pod-A 삭제됨” → “Pod-B 삭제됨” → “Pod-A 생성됨”

  • 이벤트마다 바로 Reconcile을 호출하면 → 삭제 이벤트에 반응해서 Pod를 추가로 만들었는데, 직후에 자동 재시작으로 돌아옴 → 의도보다 Pod가 많아지는 정합성 문제 발생.

Work Queue의 해결 방식:

  • 이벤트 자체가 아니라 오브젝트 키(예: default/Pod-A)를 큐에 넣음
  • 같은 키가 이미 있으면 중복 제거(dedup)
  • Reconcile은 큐에서 키를 꺼내서, 그 시점의 최신 로컬 캐시를 보고 판단

SQS와의 차이:

  • SQS는 메시지가 영속적(persistent). Consumer가 죽어도 큐에 남음.
  • K8s Work Queue는 인메모리. 컨트롤러가 죽으면 큐도 날아감.
  • 컨트롤러 재시작 시 안정성은 Informer의 List가 보장 (전체 재동기화).

3-4. 컴포넌트별 역할 요약

컴포넌트해결하는 문제핵심 메커니즘
List-Watch (Informer 내부)Watch 끊김 시 이벤트 유실끊기면 List로 전체 재동기화
로컬 캐시 (Informer 내부)API Server 조회 부하캐시에서 읽기
Work Queue (Informer 외부)이벤트 중복, 정합성키 기반 중복 제거 + 최신 캐시 기준 판단
Reconcile 함수실제 gap 해소desired vs current 비교 → 액션 실행

4. 전체 흐름

kubectl apply (desired state)
        │
        ▼
   API Server ──── etcd (desired state 저장)
        │
        │  List (최초 전체 동기화)
        │  Watch (이후 변경 이벤트 스트림)
        ▼
    Informer
        │
        ├──→ Local Cache (현재 상태 스냅샷, 컨트롤러가 여기서 조회)
        │
        └──→ Event Handler ──→ Work Queue (키 기반 중복 제거)
                                     │
                                     │  키 dequeue
                                     ▼
                              Reconcile 함수
                         (캐시에서 current 조회 →
                          desired와 비교 → 액션 실행)

5. Informer는 어디에 떠있는가?

Informer는 별도 프로세스나 Pod가 아니다. 각 컨트롤러 코드 안에 내장된 라이브러리(client-go의 일부)이다.

[컨트롤 플레인 노드]
  └─ kube-controller-manager (하나의 프로세스)
       ├─ Deployment Controller
       │    └─ Informer (Deployment 리소스 Watch)
       ├─ ReplicaSet Controller
       │    └─ Informer (ReplicaSet 리소스 Watch)
       ├─ Node Controller
       │    └─ Informer (Node 리소스 Watch)
       └─ ...

kube-controller-manager 안에서 여러 컨트롤러가 **고루틴(goroutine)**으로 돌아가고, 각 컨트롤러마다 전용 Informer를 가진다.


6. Deployment Controller와 ReplicaSet Controller의 분리

6-1. 오브젝트 체인

Deployment → ReplicaSet → Pod

Deployment가 Pod를 직접 만들지 않고, 중간에 ReplicaSet을 둔다.

6-2. 왜 분리하는가? (인과적 추론)

만약 Deployment Controller가 직접 Pod를 관리했다면? 롤링 업데이트 시 “이 Pod는 v1인가 v2인가?”, “v1을 몇 개까지 줄여도 되나?”, “롤백하면 어떤 Pod를 살려야 하나?” — 이 모든 로직이 한 곳에 뒤섞인다.

분리하면?

컨트롤러역할특징
Deployment Controller (전략가)ReplicaSet을 만들고 replicas 숫자를 조정Pod를 직접 안 건드림. 롤링/롤백 전략을 알고 있음
ReplicaSet Controller (실행자)자기한테 주어진 replicas 수에 맞게 Pod 생성/삭제롤링 업데이트가 뭔지 모름. “3개 유지해”하면 3개 맞춤

💡 이것이 쿠버네티스의 단일 책임(Single Responsibility) 원칙 — 계층적 컨트롤러 패턴

Comments

💬 Sign in with GitHub to leave a comment. Comments are public and stored in GitHub Discussions.