TT Lab
시작하기
배우기 러닝패스 코스

CKA — 쿠버네티스 관리자

컨트롤 플레인은 왜 네 개로 쪼개졌나

TT Lab 에서 이어서 보기

한 줄 요약

쿠버네티스 컨트롤 플레인은 etcd, kube-apiserver, kube-controller-manager, kube-scheduler 네 프로세스로 나뉘어 있고, 그 중 etcd 와 말하는 것은 apiserver 하나뿐입니다. 이 한 줄이 CKA 트러블슈팅의 절반을 설명합니다.

왜 이게 필요했나

상상해 봅시다. 스케줄러가 etcd 에 직접 붙어서 파드 오브젝트를 읽고 nodeName 을 써 넣는다고요. 그러면 이런 일이 벌어집니다.

그래서 쿠버네티스는 apiserver 를 유일한 관문으로 두었습니다. apiserver 는 상태를 만들지 않습니다. 받고, 검증하고, 인가하고, 저장하고, 지켜보는 사람들에게 알려줄 뿐입니다. 실제 판단은 전부 바깥의 컨트롤러들이 합니다.

스케줄러가 etcd 에 직접 붙는다면

가정해 보는 그림이다. 실제 쿠버네티스에서 etcd 와 말하는 것은 kube-apiserver 하나뿐이다. 직접 붙는 길이 있다면 무엇을 잃는지 견준다.

  • etcd 에 직접 붙는 컴포넌트 (가정)etcd 의 저장 형식과 키 구조에 결합된다. 인가, 검증, 어드미션 웹훅, 감사 로그가 모두 우회되고 기본값 채우기와 버전 변환도 컴포넌트마다 따로 구현해야 한다.
  • kube-apiserver 를 거치는 컴포넌트apiserver 가 받고, 검증하고, 인가하고, 저장하고, 지켜보는 쪽에 알려 준다. apiserver 는 상태를 만들지 않고 실제 판단은 바깥의 컨트롤러들이 한다.

여기서 구분할 것 apiserver 는 관문이지 두뇌가 아니다. 파드를 몇 개 둘지, 어느 노드에 놓을지는 컨트롤러와 스케줄러가 정하고 그 결과를 apiserver 에 적는다.

잠깐, 예측해 보세요 누군가 etcd 에 파드 키를 직접 써 넣었다. 감사 로그에 그 변경자가 남을까?

설명 확인 · 채점 없는 자가 점검

남지 않는다. apiserver 를 거치지 않았으니 인가와 검증, 어드미션, 감사가 모두 건너뛰어진다. 누가 무엇을 바꿨는지 추적할 수 없다는 것이 모든 컴포넌트가 apiserver 만 거치게 한 이유 가운데 하나다.

근거 문서

어떻게 동작하나

컨트롤러의 reconcile 루프는 이렇게 생겼습니다.

  1. watch 로 선언된 상태(spec)를 읽는다
  2. 실제 상태(status)를 관측한다
  3. 차이를 계산해 그만큼만 API 를 호출한다
  4. 다시 1번으로

명령이 아니라 차이 줄이기라는 점이 중요합니다. 그래서 이벤트를 하나 놓쳐도 다음 리싱크에서 수렴하고, 컨트롤러를 재시작해도 처음부터 다시 맞춰 갑니다. Deployment 컨트롤러는 ReplicaSet 을 만들고, ReplicaSet 컨트롤러는 파드를 만듭니다. 각자 자기 층만 봅니다.

스케줄러는 두 단계로 동작합니다.

단계 하는 일 결과
filter (predicate) 리소스 부족, 테인트 미허용, nodeSelector 불일치, 볼륨 존 불일치 노드를 제거 배치 가능한 노드 목록
score (priority) 남은 노드에 점수를 매긴다 (자원 균형, 이미지 지역성, 토폴로지 분산) 최고점 노드 하나

0/3 nodes are available: 3 Insufficient cpu 같은 메시지는 filter 단계의 탈락 사유 집계표입니다. 이 문장을 읽을 줄 알면 Pending 파드의 90%는 그 자리에서 끝납니다.

현장에서 만나는 모습

사례 1 — DHCP 가 클러스터를 죽였다. 홈랩 컨트롤 플레인의 IP 가 10.0.0.111 에서 10.0.0.120 으로 바뀐 적이 있습니다. DHCP 임대 갱신 때문이었습니다. 증상은 dial tcp 10.0.0.111:6443: connect: no route to host 였고, 진짜 원인은 인증서에 있었습니다. apiserver 인증서의 SAN 에 IP Address:10.0.0.111 만 있고 .120 이 없었던 것입니다. 여기서 재미있는 것은 컴포넌트별 반응 차이였습니다. etcd 와 kube-apiserver 는 존재하지 않는 .111 에 바인드하려다 CrashLoopBackOff 에 빠졌고, kube-scheduler 와 controller-manager 는 127.0.0.1 에 바인드하므로 프로세스는 살아 있되 아무 일도 못 하는 상태였습니다. 네 프로세스가 각자 다른 주소에 붙는다는 설계를 알아야 이 그림이 읽힙니다.

사례 2 — etcd 쿼럼과 API 가용성은 별개다. 같은 홈랩을 3노드에서 7노드로 늘리면서 컨트롤 플레인을 3대로 만들었습니다. etcdctl member list 에 멤버 3개가 정확히 찍혔고, 각 노드에 apiserver, scheduler, controller-manager 가 하나씩 돌았습니다. HA 가 완성된 것처럼 보였습니다. 아닙니다.

controlPlaneEndpoint: 10.0.0.120:6443     # cp-1 의 물리 IP

이 값이 VIP 나 DNS 가 아니라 첫 노드의 실제 IP 였습니다. 그래서 cp-1 이 죽으면 etcd 쿼럼은 2/3 로 멀쩡하고 cp-2, cp-3 의 apiserver 도 정상 동작하는데, kubectl 과 7개 노드의 kubelet 이 전부 접속 불가가 됩니다. 게다가 인증서 SAN 에 다른 컨트롤 플레인의 IP 가 없어 cp-2 로 직접 붙어도 TLS 검증이 실패합니다. 데이터 가용성과 접근 가용성은 다른 문제입니다.

덧붙여, 컨트롤 플레인을 2대까지만 늘리는 것은 1대보다 더 위험합니다. 멤버 2의 과반은 2이므로 아무 한 대만 죽어도 쿼럼을 잃습니다. 홀수(1, 3, 5)가 권장되는 이유이고, 그래서 이 확장도 반드시 3까지 갔습니다.

주소가 바뀐 날 네 프로세스의 서로 다른 반응

본문의 홈랩 사례다. DHCP 임대 갱신으로 컨트롤 플레인 IP 가 10.0.0.111 에서 10.0.0.120 으로 바뀌었다. 주소는 설명용 예시다.

  • etcd 와 kube-apiserver존재하지 않게 된 옛 주소 .111 에 바인드하려다 CrashLoopBackOff 에 빠진다.
  • kube-scheduler 와 controller-manager127.0.0.1 에 바인드하므로 프로세스는 살아 있다. 그러나 아무 일도 하지 못한다.

여기서 구분할 것 증상 문구는 no route to host 라서 네트워크를 가리켰지만, 본문이 밝힌 진짜 원인은 apiserver 인증서의 SAN 에 새 주소가 없었던 것이다. 네 프로세스가 각자 다른 주소에 붙는다는 설계를 알아야 이 그림이 읽힌다.

잠깐, 예측해 보세요 노드 위에서 보니 kube-scheduler 와 controller-manager 컨테이너는 Running 이다. 컨트롤 플레인이 정상이라고 판단해도 될까?

설명 확인 · 채점 없는 자가 점검

안 된다. 이 둘은 상태를 읽고 쓰려면 apiserver 에 닿아야 하는데, apiserver 가 떠 있지 못하면 살아 있는 프로세스도 일을 못 한다. 프로세스가 떠 있다는 것과 컨트롤 플레인이 기능한다는 것은 다른 명제다.

근거 문서

다음 실습에서 할 것

첫 실습에서는 클러스터를 조사하고, 네임스페이스·라벨·애너테이션을 다루고, kubeconfig 컨텍스트를 직접 만들어 전환하고, kubectl explain 과 --dry-run=client -o yaml 로 매니페스트를 뽑아 냅니다. 두 번째 실습에서는 CustomResourceDefinition 을 직접 써서 API 를 넓혀 봅니다. 스키마 검증이 실제로 요청을 거부하는 순간을 눈으로 확인하는 것이 목표입니다.