본문으로 건너뛰기

3.1 오브젝트 모델

Kubernetes에서 Pod, Deployment, Service, Namespace는 전부 오브젝트다. 오브젝트는 클러스터에 무엇이 어떤 모습으로 존재해야 하는지를 담은 영속 레코드이고, 공식 문서는 이를 “record of intent”, 곧 의도의 기록이라고 부른다. 오브젝트를 생성하는 행위는 “이 워크로드가 이렇게 돌기를 원한다"는 선언을 저장소에 기록하는 일이며, 그 선언을 현실로 만드는 책임은 사람이 아니라 시스템에 있다. 이 장은 오브젝트가 어떤 필드로 구성되는지, API 안에서 어떻게 식별되는지, 그리고 선언형이라는 설계가 실제로 무엇을 바꾸는지를 따라간다.

spec과 status: 두 상태의 수렴

거의 모든 오브젝트는 spec과 status라는 두 개의 중첩 필드를 갖는다. spec은 사용자가 적는 원하는 상태(desired state)다. Deployment라면 “replica 3개, 이미지는 nginx:1.29"가 spec에 들어간다. status는 시스템이 적는 현재 상태(observed state)다. 지금 실제로 준비된 replica가 몇 개인지, 어떤 condition에 걸려 있는지가 status에 기록된다. 사용자는 spec을 쓰고 status를 읽는다. 방향이 반대인 경우는 없다.

둘 사이의 간격을 좁히는 주체가 controller다. controller는 자신이 담당하는 종류의 오브젝트를 watch로 감시하다가 spec과 status가 어긋나는 순간을 감지하고, 차이를 없애는 조치를 실행한다. replica가 3개여야 하는데 2개뿐이면 하나를 더 만들고, 4개면 하나를 지운다. 이 반복을 reconciliation이라 부른다. controller가 어디에서 어떤 구조로 실행되는지는 Part II. 클러스터 아키텍처에서 다뤘다.

    flowchart TD
  U["사용자"] -->|spec 제출| A["API server"]
  A --> E[("etcd")]
  C["controller"] -->|watch| A
  C --> D{"spec과 status 일치?"}
  D -->|일치| W["대기"]
  D -->|불일치| X["조치 실행"]
  X --> S["status 갱신"]
  S --> C
  W --> C
  

이 수렴은 일회성 이벤트가 아니라 상시 동작이다. 오브젝트가 존재하는 한 controller는 계속 감시하고 계속 조치한다. 노드가 죽어 Pod가 사라져도, 누가 실수로 Pod를 지워도, spec이 남아 있는 한 시스템은 다시 그 상태로 돌아간다.

apiVersion, kind, metadata

종류와 무관하게 모든 오브젝트가 반드시 갖는 필드가 셋 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: default
  labels:
    app: web
spec:
  # 종류별 내용
  • apiVersion: 이 오브젝트가 따르는 API 그룹과 버전
  • kind: 오브젝트의 종류
  • metadata: 이름, namespace, label, annotation처럼 오브젝트를 식별하고 부가 정보를 담는 데이터

여기에 대부분의 오브젝트가 spec을 더한다. status는 매니페스트에 적지 않는다. 시스템이 채우는 영역이라 적어서 제출해도 무시된다.

GVK와 리소스

API 안에서 오브젝트 종류를 식별하는 좌표는 Group, Version, Kind 세 값의 조합이고, 줄여서 GVK라 부른다. apiVersion 필드가 그중 둘을 담는다. apps/v1은 group apps의 version v1이라는 뜻이다. 가장 오래된 core group은 이름이 빈 문자열이어서 group 없이 apiVersion: v1로 적는다. Pod, Service, ConfigMap, Namespace가 여기에 속한다.

Kind와 자주 섞여 쓰이지만 구분해야 하는 개념이 리소스(resource)다. Kind가 오브젝트 스키마의 이름이라면, 리소스는 그 오브젝트들이 노출되는 HTTP 경로의 이름이다. 소문자 복수형을 쓴다. Kind Deployment에 대응하는 리소스는 deployments이고, API server의 실제 경로는 다음과 같다.

GET /apis/apps/v1/namespaces/default/deployments/web
GET /api/v1/namespaces/default/pods

core group만 /api/v1이고 나머지는 /apis/<group>/<version> 아래에 있다. 클러스터가 제공하는 리소스 전체는 kubectl api-resources로 조회한다.

$ kubectl api-resources | head -5
NAME          SHORTNAMES   APIVERSION   NAMESPACED   KIND
bindings                   v1           true         Binding
configmaps    cm           v1           true         ConfigMap
endpoints     ep           v1           true         Endpoints
events        ev           v1           true         Event

같은 Kind가 여러 version으로 동시에 제공되는 기간도 있다. 새 API는 보통 alpha와 beta를 거쳐 stable(v1)로 승격되며, 이 기간에는 어떤 version으로 읽고 쓰든 저장된 실체는 하나다. version은 스키마 표현의 세대이지 오브젝트의 복사본이 아니다.

uid와 resourceVersion

metadata에는 사용자가 적지 않아도 시스템이 채우는 필드가 있다. 그중 둘은 동작 이해에 필요하다.

uid는 오브젝트 개체의 전 생애에 걸쳐 유일한 식별자다. name은 같은 namespace, 같은 리소스 안에서만 유일하며 오브젝트를 지우면 같은 name을 재사용할 수 있다. 반면 같은 name으로 다시 만들어도 uid는 새로 발급된다. 그래서 “이름이 같은 다른 개체"와 “같은 개체"를 구분하는 근거는 name이 아니라 uid다. garbage collection이 ownerReferences를 따라갈 때도 uid로 대상을 확인한다.

resourceVersion은 오브젝트가 바뀔 때마다 갱신되는 불투명한 문자열로, 낙관적 동시성 제어(optimistic concurrency control)에 쓰인다. 클라이언트 A와 B가 같은 오브젝트를 읽고 각자 수정본을 제출하면, 늦게 도착한 쪽은 자신이 읽었던 resourceVersion이 이미 지난 값이므로 409 Conflict로 거부된다. 잠금 없이 lost update를 막는 방식이고, 거부당한 클라이언트는 최신본을 다시 읽어 재시도한다. kubectl apply를 연달아 실행할 때 가끔 보이는 “the object has been modified” 에러가 바로 이 충돌이다.

$ kubectl get deployment web -o jsonpath='{.metadata.uid}{"\n"}{.metadata.resourceVersion}{"\n"}'
5f1c2a9e-8f0f-4c2a-9b1d-2c3e4a5b6c7d
48231

선언형 API가 바꾸는 것

명령형 시스템에서는 절차를 지시한다. “컨테이너 3개를 시작하라"는 명령은 실행이 끝나는 순간 소멸하고, 이후 컨테이너 하나가 죽어도 시스템은 아무것도 하지 않는다. 명령은 이미 완료됐기 때문이다. 선언형 시스템에서는 결과를 선언한다. “컨테이너 3개가 떠 있는 상태"라는 선언은 오브젝트로 남아 있고, 시스템은 이탈을 감지할 때마다 그 상태로 되돌린다.

이 차이가 실무에서 갖는 의미는 두 가지다. 첫째는 멱등성이다. 같은 매니페스트를 몇 번 적용해도 결과가 같으므로, 적용이 중간에 실패했을 때 그냥 다시 실행하면 된다. 매니페스트를 git에 두고 반복 적용하는 GitOps 방식이 이 성질 위에 서 있다. 둘째는 자가 복구다. 장애로 현재 상태가 무너져도 선언이 남아 있는 한 controller가 수렴을 계속 시도하므로, 복구 절차의 상당 부분이 사람의 손을 떠난다.

kubectl에도 run, scale, expose 같은 명령형 명령이 있지만, 이들 역시 내부적으로는 오브젝트를 만들거나 spec을 수정하는 편의 도구다. API 계층에는 “절차를 실행하라"는 개념 자체가 없다. 어떤 경로로 들어오든 결국 남는 것은 오브젝트, 곧 의도의 기록이다.

정리

  • 오브젝트는 spec(원하는 상태)과 status(현재 상태)를 갖고, controller가 reconciliation으로 둘을 수렴시킨다. 수렴은 일회성이 아니라 오브젝트가 사는 동안 계속된다.
  • 종류는 GVK(Group/Version/Kind)로 식별하고, 리소스는 같은 종류의 HTTP 경로 이름(소문자 복수형)이다.
  • name은 재사용되지만 uid는 개체마다 다르고, resourceVersion이 동시 수정의 충돌을 걸러낸다.