LabHub

KCNA — 쿠버네티스·클라우드 네이티브 입문 · 쿠버네티스 아키텍처 · 이론

컨트롤 플레인은 왜 이렇게 쪼개졌나

LabHub 에서 이어서 보기

한 줄 요약

쿠버네티스는 '명령을 실행하는 시스템'이 아니라 **'원하는 상태를 적어 두면 컨트롤러들이 현실을 거기에
맞춰 가는 시스템'** 입니다. 컨트롤 플레인이 여러 조각인 이유는 이 루프를 도는 주체를 역할별로 나눴기 때문입니다.

왜 이게 필요했나

명령형 시스템은 "파드를 3개 띄워라"를 실행하고 끝납니다. 그런데 노드가 하나 죽으면? 아무도 신경 쓰지
않습니다. 명령은 이미 끝났으니까요.

선언형 시스템은 "파드가 3개인 상태를 원한다"를 저장합니다. 그리고 누군가가 계속 현재 상태를 읽어
원하는 상태와 비교하고, 다르면 좁힙니다. 이 루프를 reconciliation loop 라 부르고, 이게 쿠버네티스의
전부입니다. 나머지는 이 루프를 안전하고 확장 가능하게 만들기 위한 장치들입니다.

이렇게 보면 컴포넌트 분리가 자연스럽게 읽힙니다. **상태를 어디에 적을 것인가(etcd), 누가 그 상태를
읽고 쓰게 할 것인가(kube-apiserver), 누가 루프를 돌 것인가(kube-controller-manager), 파드를 어느
노드에 놓을지 결정하는 특별한 루프는 누가 돌 것인가(kube-scheduler), 노드 위에서 실제로 컨테이너를
띄우는 건 누구인가(kubelet).**

어떻게 동작하나

컨트롤 플레인

| 컴포넌트 | 하는 일 | 없어지면 |
| --- | --- | --- |
| etcd | 클러스터의 모든 상태를 담는 유일한 저장소 | 클러스터의 기억이 사라진다 |
| kube-apiserver | etcd 로 가는 유일한 문. 인증·인가·어드미션·검증 | 아무도 아무것도 읽고 쓸 수 없다 |
| kube-controller-manager | Deployment/ReplicaSet/Node/Endpoint 등 수십 개 컨트롤러 루프 | 선언은 저장되지만 아무 일도 안 일어난다 |
| kube-scheduler | Pending 파드에 노드를 배정 | 파드가 만들어지되 영원히 Pending |

어떤 컴포넌트도 etcd 를 직접 건드리지 않습니다. 스케줄러도, 컨트롤러 매니저도, kubelet 도 전부
apiserver 를 통합니다. 그래야 인증·인가·어드미션·감사 로그가 한 곳에서 걸립니다. 이것이 KCSA 로
이어지는 보안 설계의 출발점입니다.

etcd 안의 키 구조도 알아 두면 좋습니다. 모든 리소스는 /registry/ 아래에 살고,
네임스페이스 리소스는 /registry/<종류>/<네임스페이스>/<이름>, 클러스터 리소스는
/registry/<종류>/<이름> 형태입니다. 값은 기본적으로 protobuf 로 직렬화됩니다.

노드

API 는 리소스로 되어 있다

쿠버네티스와 대화하는 방법은 하나뿐입니다 — REST 리소스에 CRUD. Deployment 를 만드는 것도,
파드를 지우는 것도 전부 같은 문법입니다. kubectl api-resources 로 이 클러스터가 아는 리소스의
전체 목록을, kubectl explain 으로 각 필드의 뜻을 볼 수 있습니다. **문서 사이트를 열지 않고
클러스터에게 직접 물어보는 습관**이 시험장에서도 현장에서도 가장 빠릅니다.

라벨 셀렉터 — 느슨한 연결의 접착제

쿠버네티스에는 "이 서비스는 저 파드 3개를 가리킨다" 같은 직접 참조가 거의 없습니다. 대신 라벨을 붙이고
셀렉터로 고릅니다. Service 도, ReplicaSet 도, NetworkPolicy 도 전부 셀렉터로 대상을 찾습니다.
그래서 셀렉터 오타 하나가 조용한 장애를 만듭니다 — 오브젝트는 정상 생성되고, 다만 아무것도 안 잡힙니다.

현장에서 만나는 모습

저자의 홈랩에서 실제로 있었던 일입니다. 어느 날 클러스터가 응답하지 않았고,
dial tcp 10.0.0.111:6443: connect: no route to host 가 떴습니다. 원인은 DHCP 였습니다 —
컨트롤 플레인 노드의 IP 가 .111 에서 .120 으로 갱신돼 버린 것입니다.

여기서 컴포넌트 분리가 눈에 보이는 형태로 드러났습니다. **etcd 와 kube-apiserver 는 없는 주소
.111 에 바인드하려다 실패해 CrashLoopBackOff 에 빠졌지만, kube-scheduler 와 controller-manager 는
127.0.0.1 에 바인드하므로 프로세스는 멀쩡히 살아 있었습니다.** 살아 있되 아무 일도 못 하는 상태였죠.
apiserver 가 없으면 컨트롤러는 읽을 것도 쓸 것도 없기 때문입니다.

결정적 증거는 인증서에 있었습니다. apiserver.crt 의 Subject Alternative Name 에 .120 이 없었습니다.
그래서 IP 만 바꿔서는 복구가 안 됩니다 — SAN 에 없는 주소로 접속하면 TLS 검증이 실패하니까요.

같은 클러스터를 나중에 7 노드(컨트롤 플레인 3 + GPU 워커 4)로 확장하면서 더 뼈아픈 교훈이 나왔습니다.
etcd 멤버를 3 개로 만들어 쿼럼을 갖췄는데도, controlPlaneEndpoint 가 VIP 나 DNS 가 아니라
cp-1 의 물리 IP(10.0.0.120:6443) 로 박혀 있었습니다. cp-1 이 죽으면 etcd 쿼럼은 2/3 로 멀쩡하고
다른 두 노드의 apiserver 프로세스도 정상인데, kubectl 과 7 대의 kubelet 이 전부 접속 불가가 됩니다.
"컨트롤 플레인은 살아 있는데 아무도 문을 못 찾는" 상태입니다.
데이터 가용성과 접근 가용성은 완전히 별개의 문제라는 것이 이 사고의 요약입니다.

다음 실습에서 할 것

바로 다음 실습에서 kcna-arch 네임스페이스를 만들고, 노드 목록을 뽑고, kubectl api-resources
kubectl explain 으로 API 를 클러스터에게 직접 물어봅니다. 라벨 셀렉터로 파드를 골라내고,
마지막에는 같은 디플로이먼트를 명령형과 선언형 두 방식으로 만들어 무엇이 다른지 확인합니다.