컨테이너 하나를 실행하는 것은 Docker로 충분하다. 그런데 수십 개의 서비스를 안정적으로 운영하면서, 트래픽에 따라 자동으로 늘리고 줄이고, 장애가 나면 스스로 회복하게 하려면 그 위의 플랫폼이 필요하다. Kubernetes(이하 k8s)는 컨테이너 오케스트레이션 플랫폼이다. 오케스트라 지휘자처럼 수많은 컨테이너가 어디서 실행될지, 몇 개가 실행될지, 장애 시 어떻게 대처할지를 조율한다.

선언적 모델 — k8s를 관통하는 핵심 개념

k8s를 처음 접할 때 가장 먼저 이해해야 할 것이 선언적(declarative) 모델이다. 이것을 이해하면 나머지 개념들이 훨씬 자연스럽게 따라온다.

명령적(imperative) 방식은 “파드를 3개 실행해라"처럼 행동을 직접 지시한다. 선언적 방식은 “파드는 항상 3개인 상태여야 한다"처럼 원하는 상태를 선언한다. k8s는 이 선언을 끊임없이 감시하면서 현실을 거기에 맞춘다. 파드가 하나 죽으면 k8s가 감지해 새로 만들고, 노드가 하나 꺼지면 그 위의 파드들을 다른 노드로 옮긴다. 운영자가 명령을 내리는 게 아니라 원하는 상태를 YAML로 선언하면 k8s가 알아서 그 상태를 유지한다.

이 원하는 상태를 desired state, 실제 상태를 actual state라 한다. k8s의 모든 동작은 이 둘의 차이를 좁히려는 루프다.

desired state (YAML로 선언)
        ↕  끊임없이 비교
actual state  (지금 실제로 돌아가는 것)
        ↓  차이가 생기면
k8s가 자동으로 바로잡음

클러스터 구조

k8s는 여러 서버(노드)를 묶어 클러스터(cluster) 로 다룬다. 노드는 역할에 따라 두 종류로 나뉜다.

마스터 노드(control plane)

클러스터의 두뇌다. 직접 컨테이너를 실행하지 않고, 전체 상태를 관리하고 워커들에게 지시한다. 네 가지 핵심 컴포넌트로 이뤄진다.

kube-apiserver는 클러스터의 모든 명령이 통과하는 관문이다. kubectl로 내리는 명령, 컨트롤러들의 상태 갱신, 워커 노드의 보고 — 전부 이 API Server를 거친다. REST API로 동작하므로 kubectl 없이 HTTP로 직접 호출해도 된다. k8s에서 “API를 호출한다"는 것은 곧 이 API Server에 요청을 보내는 것이다.

etcd는 클러스터의 모든 상태를 저장하는 분산 키-값 저장소다. 어떤 파드가 몇 개 있어야 하는지, 지금 실제로 어느 노드에 몇 개가 있는지, 어떤 서비스가 어떤 파드를 가리키는지 — 모든 것이 여기 있다. etcd가 날아가면 클러스터 전체 상태가 사라지므로, 프로덕션에서는 etcd 백업이 가장 중요한 운영 작업 중 하나다. etcd 자체도 분산 합의 알고리즘(Raft)으로 여러 노드에 복제해 고가용성을 확보한다.

kube-scheduler는 새 파드를 어느 워커 노드에 배치할지 결정한다. CPU·메모리 여유, 노드 레이블, 파드의 affinity/anti-affinity 규칙, 테인트(taint)와 톨러레이션(toleration) 등을 종합해 가장 적합한 노드를 고른다. 파드를 직접 실행하지는 않고 “이 파드는 노드 B에 올려라"고 기록할 뿐이다. 실제 실행은 그 노드의 kubelet이 맡는다.

kube-controller-manager는 desired state와 actual state의 차이를 감지해 바로잡는 여러 컨트롤러들의 묶음이다. 디플로이먼트 컨트롤러, 레플리카셋 컨트롤러, 노드 컨트롤러 등이 각자 담당하는 오브젝트를 감시한다. “파드가 3개여야 하는데 2개다 → 하나 더 만든다”, “노드가 응답이 없다 → 그 노드의 파드들을 재스케줄한다” 같은 일을 끊임없이 한다.

워커 노드(worker node)

실제로 컨테이너가 실행되는 서버다. 각 워커에는 세 가지 컴포넌트가 있다.

kubelet은 마스터의 지시를 받아 이 노드에서 컨테이너를 실행하고 상태를 API Server에 보고한다. API Server에 주기적으로 연락해 이 노드에서 실행해야 할 파드 목록을 받아오고, 컨테이너가 지시대로 살아있는지 감시한다. 노드의 현장 반장이다.

kube-proxy는 파드로 들어오는 네트워크 트래픽을 올바른 파드로 라우팅한다. Service 오브젝트가 정의하는 가상 IP로 들어온 요청을 실제 파드 IP로 연결한다.

컨테이너 런타임은 실제로 컨테이너를 시작하고 멈추는 엔진이다. 예전에는 Docker가 쓰였지만 지금은 containerd가 표준이다. kubelet이 “이 이미지로 컨테이너를 실행해라"고 지시하면 런타임이 이를 수행한다.

전체 흐름

[사용자] kubectl apply -f deployment.yaml
[API Server]  → etcd에 desired state 저장
[Controller Manager]  desired ≠ actual 감지
[Scheduler]  어느 노드에 올릴지 결정
[kubelet (워커 노드)]  컨테이너 실행
[컨테이너 런타임]  실제 컨테이너 시작

k8s API — 모든 것은 오브젝트다

k8s의 모든 구성 요소 — 파드, 디플로이먼트, 서비스, ConfigMap, 심지어 노드 자체까지 — 는 API 오브젝트다. YAML로 정의해 API Server에 제출하면 etcd에 저장되고, 컨트롤러가 그 선언을 현실로 만든다.

모든 오브젝트는 공통 구조를 갖는다.

apiVersion: apps/v1       # 어떤 API 그룹의 어떤 버전인가
kind: Deployment          # 오브젝트 종류
metadata:
  name: my-app            # 이름 (같은 네임스페이스 안에서 유일해야 함)
  namespace: production   # 논리적 격리 단위
  labels:                 # 키-값 태그 (셀렉터로 오브젝트를 고를 때 쓴다)
    app: my-app
    version: "1.0"
spec:                     # 원하는 상태 정의 (오브젝트마다 내용이 다르다)
  ...
status:                   # k8s가 채우는 실제 상태 (직접 수정하지 않는다)
  ...

metadata.labels는 k8s에서 오브젝트를 선택하는 기본 수단이다. Service가 어떤 파드로 트래픽을 보낼지, HPA가 어떤 Deployment를 스케일할지 모두 레이블 셀렉터로 결정한다.

네임스페이스(namespace) 는 클러스터 안의 논리적 격리 단위다. 같은 클러스터를 개발/스테이징/프로덕션 환경으로 나누거나, 팀별로 분리할 때 쓴다. 네임스페이스 간에는 기본적으로 네트워크 통신은 가능하지만 RBAC으로 접근을 제한할 수 있다.

이점과 트레이드오프

k8s가 주는 것은 분명하다. 선언적 배포, 자동 확장, 셀프힐링, 풍부한 관측성 생태계 — 규모가 일정 이상이면 이 기능들 없이는 안정적 운영이 어렵다.

감수하는 것은 복잡성이다. 컨테이너 하나를 실행하기 위해 파드, 디플로이먼트, 서비스 등을 이해하고 YAML로 선언해야 한다. 클러스터 자체를 운영하는 비용(etcd 백업, 버전 업그레이드, 노드 관리)도 만만치 않다. 서비스가 몇 개 안 되거나 팀이 작다면 k8s는 과한 선택이다. k8s가 값어치를 하기 시작하는 시점은 여러 서비스가 독립적으로 스케일링돼야 하거나, 장애 격리와 자동 복구가 운영의 필수 조건이 될 때다.