[☸️K8s] 선언형 모델과 Reconciliation Loop

쿠버네티스가 선언형 모델을 구현하는 방식, spec/status 분리, Reconciliation Loop, Watch와 Informer가 어떻게 함께 동작하는지 정리합니다.

[☸️K8s] 선언형 모델과 Reconciliation Loop

쿠버네티스의 선언형 모델은 단순히 YAML을 쓰는 방식이 아니다. 사용자가 spec에 목표 상태를 선언하면, 컨트롤러가 status와의 차이를 계속 비교하고, Watch와 Informer가 변경을 효율적으로 전달하며, Reconciliation Loop가 클러스터를 원하는 상태로 수렴시킨다. 이번 글에서는 이 흐름을 아키텍처 관점에서 정리한다.


What this post covers

  • 쿠버네티스가 선언형 모델을 사용하는 이유
  • 선언형 모델을 구현하기 위한 Control Plane 구조
  • specstatus가 분리되어 있는 이유
  • Reconciliation Loop의 Observe → Diff → Act 흐름
  • Watch, resourceVersion, Informer가 필요한 이유
  • Watch 이벤트와 level-triggered reconcile의 관계

1. 왜 선언형 모델인가?

1.1 명령형 모델의 한계

쿠버네티스 이전의 인프라 운영 방식은 대부분 명령형(Imperative)이었다.

명령형 모델에서 운영자는 시스템이 어떻게 변해야 하는지, 즉 절차(procedure)를 직접 기술한다.

1
2
3
4
# 명령형: "지금 이것을 실행하라"
docker run -d nginx
docker stop old_container
docker rm old_container

이 방식은 단일 서버에서는 직관적으로 작동한다.

하지만 수백 개의 노드와 수천 개의 컨테이너를 운영하는 분산 시스템에서는 구조적인 한계에 부딪힌다. 명령을 전달하는 도중 네트워크 오류, 노드 장애, 프로세스 크래시가 발생할 수 있기 때문이다.

“100개 중 47번째 명령에서 실패했다면 어디서부터 재시작해야 하는가?” 이 질문에 답하려면 운영자가 현재 시스템 상태를 직접 파악해야 한다. 그런데 그 상태 조회 자체도 실패할 수 있다.

1.2 선언형 모델이 해결하는 것

선언형(Declarative) 모델에서 운영자는 절차가 아니라 목표 상태(desired state)를 기술한다.

1
2
3
# 선언형: "이 상태가 되어야 한다"
replicas: 3
image: nginx:1.25

시스템은 현재 상태를 스스로 파악하고, 목표 상태와의 차이를 계산한 뒤, 그 차이를 좁히는 행동을 직접 결정한다.

이 구조에서 세 가지 중요한 성질이 생긴다.

성질의미
멱등성(Idempotency)같은 선언을 여러 번 적용해도 결과가 같다. replicas: 3을 두 번 적용해도 Pod가 6개로 늘어나지 않는다.
자기 치유(Self-healing)노드 장애, OOM Kill, 수동 삭제 등으로 상태가 변하면 시스템이 자동으로 복구한다.
드리프트 감지(Drift Detection)실제 상태가 선언된 상태와 달라지는 순간 차이를 감지할 수 있다.

선언형 모델의 핵심은 “명령을 잘 실행했는가?”가 아니라 “지금 시스템이 선언한 상태와 같은가?”를 계속 묻는 데 있다.


2. 선언형 모델이 규정하는 쿠버네티스 아키텍처

선언형 모델을 구현하려면 특정한 구조가 반드시 필요하다.

“목표 상태를 저장하고, 현재 상태를 파악하고, 차이를 좁히는 행동을 취한다”는 루프를 지속적으로 실행할 수 있어야 한다. 이 요구사항이 쿠버네티스 컴포넌트 구조를 직접 결정한다.

Control Plane 구조

2.1 etcd: 목표 상태의 유일한 저장소

쿠버네티스에서 사용자가 선언한 목표 상태는 etcd에 저장된다.

etcd는 분산 key-value 저장소이며, Raft 합의 알고리즘을 사용해 클러스터 안에서 강한 일관성(strong consistency)을 보장한다.

모든 컴포넌트는 현재 상태나 목표 상태를 직접 들고 있지 않고, API Server를 통해 etcd의 상태를 조회하거나 갱신한다. 어느 컴포넌트가 죽어도 etcd에 목표 상태가 남아 있으면 복구가 가능하다.

2.2 kube-apiserver: 유일한 진입점

etcd에 직접 접근하는 컴포넌트는 kube-apiserver뿐이다.

kubectl, kubelet, controller-manager, scheduler 등 모든 컴포넌트는 반드시 API Server를 통해서만 상태를 읽고 쓴다.

이 설계는 두 가지를 달성한다.

  • 인증(Authentication), 인가(Authorization), 어드미션 컨트롤(Admission Control)을 단일 지점에서 처리한다.
  • Watch 메커니즘을 API Server 수준에서 제공해 etcd의 부하를 줄인다.

2.3 kube-controller-manager: 선언형 루프의 실체

controller-manager는 여러 컨트롤러를 하나의 프로세스로 묶어 실행한다.

각 컨트롤러는 특정 리소스 타입에 대해 “목표 상태와 현재 상태의 차이를 감지하고 좁히는 루프”를 실행한다. ReplicaSet Controller, Deployment Controller, Node Controller 등이 여기에 속한다.

이 컨트롤러들이 선언형 모델의 실질적인 구현체다.

2.4 kube-scheduler: 배치 결정자

scheduler는 어떤 Pod를 어느 Node에 배치할지 결정하는 컴포넌트다.

Pod가 생성됐지만 아직 Node가 배정되지 않은 상태, 즉 spec.nodeName이 비어 있는 상태를 감지하고, Node 리소스, affinity, taint/toleration 등을 계산해 spec.nodeName 필드를 채운다.

scheduler는 Pod를 직접 실행하지 않는다. 어디서 실행될지만 기록하며, 실제 컨테이너 실행은 해당 Node의 kubelet이 담당한다.

2.5 kubelet: Node에서의 선언형 루프

kubelet은 각 Node에서 실행되는 에이전트다.

자신이 실행 중인 Node에 스케줄링된 Pod들의 목표 상태를 API Server에서 가져오고, 컨테이너 런타임(containerd, CRI-O)을 통해 실제 컨테이너를 생성하거나 삭제한다.

kubelet도 일종의 선언형 루프를 실행한다. Control Plane과 일시적으로 연결이 끊겨도 이미 알고 있는 목표 상태를 기준으로 계속 작동할 수 있다.


3. Spec과 Status: 선언형 모델의 데이터 구조

쿠버네티스의 모든 리소스 오브젝트는 보통 세 개의 최상위 필드로 구성된다.

필드역할
metadata이름, Namespace, label, annotation 등 식별 정보
spec사용자가 선언한 목표 상태
status시스템이 관찰한 현재 상태

이 중 specstatus는 선언형 모델의 핵심 개념을 직접 구현한다.

3.1 Spec: 사용자가 선언하는 목표 상태

spec은 “이 리소스가 어떤 상태여야 하는가”를 기술하는 필드다.

사용자 또는 상위 컨트롤러가 작성하며, API Server를 통해 etcd에 저장된다. 쿠버네티스 공식 문서에서는 이를 desired state라고 부른다.

1
2
3
4
5
6
7
8
9
10
spec:
  replicas: 3          # "Pod가 3개 실행 중이어야 한다"
  selector:
    matchLabels:
      app: nginx
  template:
    spec:
      containers:
        - name: nginx
          image: nginx:1.25

중요한 점은 spec을 작성한 시점에 Pod가 즉시 3개 실행되는 것은 아니라는 점이다.

이 선언을 받은 시스템이 그 상태를 만들기 위한 작업을 시작할 뿐이다.

3.2 Status: 시스템이 기록하는 현재 상태

status는 “이 리소스가 현재 어떤 상태인가”를 기술하는 필드다.

컨트롤러와 kubelet이 실제 상태를 관찰한 뒤 API Server를 통해 갱신한다. 사용자가 직접 status를 수정하는 것은 일반적으로 의미가 없다. 보통 API Server가 별도의 status subresource로 처리한다.

1
2
3
4
5
6
7
8
status:
  replicas: 3            # 현재 존재하는 Pod 수
  readyReplicas: 2       # 트래픽을 받을 준비가 된 Pod 수
  conditions:
    - type: Available
      status: "True"
    - type: Progressing
      status: "True"

spec.replicas: 3이지만 status.readyReplicas: 2라면, 하나의 Pod가 아직 시작 중이거나 문제가 있는 상태다. 컨트롤러는 이 차이를 감지하고 해소하기 위해 계속 동작한다.

3.3 Spec과 Status를 분리하는 이유

Spec과 Status의 분리는 단순한 구조 설계가 아니다.

목표 상태(spec)와 현재 상태(status)를 명확히 분리함으로써, 시스템의 어느 부분이 어떤 데이터에 권한을 갖는지가 분명해진다.

주체읽는 것쓰는 것
사용자statusspec
컨트롤러spec, statusstatus, 필요한 하위 리소스
kubeletPod specPod status

사용자는 spec에 목표를 쓰고, 시스템은 status에 관찰 결과를 쓴다. 컨트롤러는 두 필드의 차이를 계산해 다음 행동을 결정한다.


4. Reconciliation Loop: 선언형의 구현 메커니즘

Reconciliation(조정)은 specstatus 사이의 차이를 좁히는 과정이다.

각 컨트롤러는 이 과정을 루프로 실행한다. 쿠버네티스 공식 문서와 소스코드에서는 이를 보통 reconcile() 함수로 표현한다.

4.1 Observe → Diff → Act

reconcile 함수는 세 단계로 단순화할 수 있다.

단계역할예시
Observe현재 상태를 관찰한다ReplicaSet의 spec.replicas와 실제 Pod 목록 조회
Diff목표 상태와 현재 상태를 비교한다Pod 3개가 필요한데 현재 2개만 있음
Act차이를 줄이는 행동을 한다Pod 1개 생성 API 호출
1
2
3
4
5
loop:
    desired = read(spec)
    actual  = observe(cluster)
    diff    = desired - actual
    act(diff)

이 루프는 차이가 0이 될 때까지, 즉 spec과 관찰된 상태가 일치할 때까지 반복된다.

차이가 없으면 아무것도 하지 않는다(no-op). 이것이 멱등성의 근거다.

4.2 Level-Triggered vs Edge-Triggered

소프트웨어 아키텍처에서 이벤트를 처리하는 방식은 크게 두 가지로 나눌 수 있다.

방식의미한계 또는 장점
Edge-triggered상태가 변하는 순간의 이벤트에 반응이벤트를 놓치면 변화가 처리되지 않을 수 있음
Level-triggered현재 상태 자체를 반복해서 검사이벤트를 놓쳐도 다음 reconcile에서 현재 상태를 다시 읽어 복구 가능

쿠버네티스는 의도적으로 level-triggered 방식을 채택한다.

컨트롤러는 특정 이벤트가 발생했다는 사실을 기억해서 처리하는 것이 아니라, 항상 전체 현재 상태를 다시 읽고 판단해야 한다. 이 설계 덕분에 컨트롤러가 재시작되거나 일부 이벤트를 놓쳐도 시스템이 올바른 상태로 수렴할 수 있다.


5. Watch: 효율적인 상태 감시 메커니즘

Reconciliation Loop가 “어떻게 차이를 좁히는가”라면, Watch는 “언제 reconcile을 시작해야 하는가”를 알려주는 메커니즘이다.

5.1 Polling의 문제

가장 단순한 구현은 일정 주기마다 API Server를 폴링(polling)하는 것이다.

하지만 수천 개의 리소스를 여러 컨트롤러가 계속 폴링하면 API Server와 etcd에 큰 부하가 걸린다. 변경 감지 지연도 폴링 주기만큼 생긴다.

5.2 Watch API의 동작 방식

쿠버네티스 API Server는 HTTP long-polling 기반의 Watch API를 제공한다.

클라이언트가 다음과 같은 요청을 보내면, 서버는 응답 연결을 닫지 않고 유지한다.

1
GET /apis/apps/v1/replicasets?watch=true

이후 해당 리소스에 변경이 생길 때마다 변경 이벤트를 스트리밍 방식으로 클라이언트에 전달한다.

이벤트 타입은 대표적으로 세 가지다.

이벤트의미
ADDED새 리소스 생성
MODIFIED기존 리소스 변경
DELETED리소스 삭제
1
2
{"type":"ADDED", "object": {"kind":"ReplicaSet", "metadata": {}, "spec": {}, "status": {}}}
{"type":"MODIFIED", "object": {"kind":"ReplicaSet", "metadata": {}, "spec": {}, "status": {}}}

5.3 ResourceVersion과 Watch의 연속성

모든 etcd 객체는 metadata.resourceVersion이라는 단조 증가하는 값을 가진다.

클라이언트는 처음 리소스를 List로 조회할 때의 resourceVersion을 기록해두고, 이후 다음 형태로 Watch를 요청한다.

1
GET /apis/apps/v1/replicasets?watch=true&resourceVersion=<N>

이렇게 하면 N 이후에 발생한 변경 이벤트만 받을 수 있다. 연결이 끊겼다가 재연결해도, 마지막으로 처리한 resourceVersion 이후부터 이어서 이벤트를 받을 수 있다.

5.4 Informer: Watch의 실제 구현체

컨트롤러가 Watch API를 직접 사용하면 연결 관리, 재연결, 캐시, 중복 제거를 모두 직접 구현해야 한다.

쿠버네티스의 client-go 라이브러리는 이를 Informer 패턴으로 추상화한다.

구성 요소역할
ReflectorAPI Server에 Watch 연결을 유지하고 이벤트를 수신한다. 연결이 끊기면 재연결하고 resourceVersion을 관리한다.
Store(Local Cache)수신한 이벤트를 바탕으로 리소스의 최신 상태를 메모리에 캐시한다. 컨트롤러는 매번 API Server를 조회하지 않고 캐시를 읽는다.
WorkQueue변경된 리소스의 키(namespace/name)를 큐에 넣는다. 중복을 제거해 짧은 시간에 여러 이벤트가 생겨도 reconcile은 필요한 만큼만 실행된다.

Informer는 Watch를 “컨트롤러가 안정적으로 사용할 수 있는 로컬 캐시와 작업 큐”로 바꿔주는 계층이다.

5.5 Act 이후의 연쇄 반응

컨트롤러가 Act 단계에서 API Server에 변경을 가하면, 그 변경은 etcd에 저장되고 resourceVersion이 증가한다.

다른 컴포넌트들이 해당 리소스를 Watch하고 있다면 이 변경이 이벤트로 전달된다.

예를 들어 ReplicaSet Controller가 Pod를 생성하면 다음 흐름이 이어진다.

1
2
3
4
5
6
ReplicaSet Controller
  → Pod 생성
  → Scheduler가 새 Pod를 Watch
  → Scheduler가 nodeName 지정
  → kubelet이 배정된 Pod를 Watch
  → kubelet이 컨테이너 실행

이 연쇄가 쿠버네티스 전체 워크플로우의 기본 구조다.

5.6 Watch와 level-triggered reconcile의 관계

Watch 이벤트는 edge-triggered처럼 보인다.

하지만 컨트롤러는 이벤트에 담긴 내용을 그대로 처리하지 않는다. 보통 해당 리소스의 키만 WorkQueue에 넣고, 실제 reconcile 시점에는 캐시에서 최신 상태를 다시 읽는다.

따라서 이벤트가 여러 번 중복으로 큐에 들어오거나 일부 이벤트가 유실되어도, reconcile은 항상 “지금 현재 상태”를 기준으로 동작한다.

Watch는 reconcile을 깨우는 신호이고, reconcile은 최신 상태를 다시 읽는 level-triggered 루프다. 이 조합이 쿠버네티스 컨트롤러의 신뢰성을 만든다.


핵심 정리

  • 선언형 모델은 절차가 아니라 목표 상태를 선언하는 방식이다.
  • 쿠버네티스는 목표 상태를 spec에 저장하고, 관찰된 현재 상태를 status에 기록한다.
  • 컨트롤러는 spec과 현재 상태의 차이를 계산해 Reconciliation Loop를 실행한다.
  • Reconciliation Loop는 Observe → Diff → Act 구조로 동작한다.
  • Watch는 변경을 효율적으로 감지하고, Informer는 Watch를 캐시와 작업 큐로 추상화한다.
  • Watch 이벤트는 reconcile을 깨우는 신호일 뿐이며, 실제 판단은 최신 상태를 다시 읽는 level-triggered 방식으로 이루어진다.

Reading flow

  • Previous: [☸️K8s] 쿠버네티스를 공부하는 이유와 아키텍처 한눈에 보기_posts/k8s/2026-05-15-K8s(1).md
  • Next: [☸️K8s] 쿠버네티스 리소스와 GVK 체계_posts/k8s/2026-05-17-K8s(3).md
  • Series: /series/