컨트롤 플레인 (Control Plane)
클러스터의 뇌. “무엇이 있어야 하는가”를 결정한다.
kube-apiserver · etcd · kube-scheduler ·
kube-controller-manager · cloud-controller-manager(클라우드일 때)
무엇이 어디서 돌고 있는가
컨트롤 플레인 (Control Plane)
클러스터의 뇌. “무엇이 있어야 하는가”를 결정한다.
kube-apiserver · etcd · kube-scheduler ·
kube-controller-manager · cloud-controller-manager(클라우드일 때)
노드 (Node) / 데이터 플레인
실제로 컨테이너를 돌리는 곳.
kubelet · kube-proxy · 컨테이너 런타임(containerd 등)
컨트롤 플레인 노드에도 kubelet과 kube-proxy는 있다. 컨트롤 플레인 컴포넌트 자체가 Pod으로 돌기 때문이다 — 뒤에서 다룬다.
화살표 방향이 핵심이다. 모든 컴포넌트가 API 서버를 향한다. API 서버가 다른 컴포넌트를 호출하지 않는다.
컴포넌트들이 서로 직접 통신하고 각자 상태를 들고 있으면, “지금 클러스터가 어떤 상태인가”에 대한 답이 여러 개가 된다. 그래서 모든 읽기·쓰기를 문 하나로 모은다 — 그 문이 API 서버다.
kubectl은 그 위의 얇은 클라이언트일 뿐시작하기 전에의 루프는 “선언된 상태(spec)“가 어딘가에 남아 있어야 돌 수 있다. API 서버가 stateless일 수 있는 것도, 그 상태를 전부 맡아 주는 저장소가 따로 있기 때문이다 — 그게 etcd다.
etcd만 백업하면 클러스터 전체를 복구할 수 있다 (PV(PersistentVolume — 스토리지) 안의 데이터는 제외)Raft는 여러 노드가 같은 기록을 갖도록 합의하는 알고리즘이다. 짝수(2, 4)로 구성하면 반반으로 갈렸을 때 과반이 안 나와 쓰기가 멈추기 때문에 홀수로 둔다. 알고리즘 내부까지 팔 필요는 없다 — “과반이 살아야 쓴다”는 결과만 기억하면 된다.
# etcd 안의 키 구조 — 오브젝트 경로가 그대로 키다/registry/pods/default/nginx/registry/deployments/kube-system/coredns/registry/secrets/default/my-secretspec이 etcd에 저장되는 것만으로는 아무 일도 일어나지 않는다. Pod을 어느 노드에서 돌릴지 누군가 정해야 하는데, 그 결정만 전담하는 것이 스케줄러다.
spec.nodeName이 비어 있는 Pod을 찾는다spec.nodeName을 채운다. 그게 전부다두 단계로 고른다
시작하기 전에의 축 — “선언된 상태와 실제 상태의 차이를 줄이는 루프” — 가 실제로 도는 곳이 여기다. 리소스 종류마다 전담 루프(컨트롤러)가 있고, 그 루프들을 한 프로세스에 담은 것이 컨트롤러 매니저다.
| 컨트롤러 | 하는 일 |
|---|---|
| Deployment | ReplicaSet을 만들고 롤아웃을 조율 |
| ReplicaSet | Pod 개수를 맞춘다 |
| Node | 노드가 응답 없으면 NotReady 표시, 이후 Pod 축출 |
| Job / CronJob | Pod을 만들고 완료를 추적 |
| Endpoint(Slice) | Service 셀렉터에 맞는 Pod IP 목록을 유지 |
| ServiceAccount | 네임스페이스마다 default SA를 만든다 |
| PV / PVC | 바인딩과 회수(reclaim)를 처리 |
“컨트롤러가 죽었다”의 증상은 각기 다르다. Deployment를 만들어도 Pod이 안 생기면 컨트롤러 매니저, Pod은 생겼는데 Pending이면 스케줄러다.
장 첫머리 카드에 있던 cloud-controller-manager는 이 루프들 중 클라우드 제공자 API를 불러야 하는 것들(LoadBalancer 프로비저닝, 클라우드 쪽 노드 수명 관리)만 떼어낸 프로세스다. 시험 환경(kubeadm 클러스터)에는 보통 없다 — 이름과 역할만 알아두면 된다.
스케줄러가 nodeName을 채워도 그건 아직 etcd 안의 기록일 뿐이다.
그 기록을 노드 위의 진짜 컨테이너로 만드는 실행자가 노드마다 있어야 한다 — 그게 kubelet이다.
NotReady)Service의 ClusterIP는 어느 인터페이스에도 붙어 있지 않은 가상 IP다 (Service 자체는 Service). 이 주소로 온 패킷을 실제 Pod IP로 바꿔 줄 무언가가 노드마다 필요하다 — 그 번역 규칙을 심는 것이 kube-proxy다.
iptables(기본) / ipvs / nftables모드 셋의 내부 차이까지 팔 필요는 없다 — 기본이 iptables라는 것과,
셋 다 “커널에 규칙을 심는 방식”의 변형이라는 것만 알면 된다.
컨테이너를 실제로 만들고 지우는 것은 kubelet이 아니라 컨테이너 런타임이다. kubelet이 특정 런타임에 종속되지 않도록 둘 사이를 표준 인터페이스로 끊어 둔 것이 CRI(Container Runtime Interface)다 — Docker 제거(v1.24)가 가능했던 것도 이 경계 덕분이다.
crictl 을 쓴다# 노드 안에서 (kubectl이 안 될 때의 생명줄)sudo crictl ps # 실행 중 컨테이너sudo crictl ps -a # 죽은 것 포함sudo crictl logs <container-id>sudo crictl pods # Pod 샌드박스 목록crictl logs는 Pod 이름이나 컨테이너 이름이 아니라 crictl ps -a의 첫 번째 열인
컨테이너 ID를 받는다. 예를 들어 kubeadm 스태틱 Pod에서 etcd-controlplane은 Pod 이름,
etcd는 컨테이너 이름이다. 둘 중 하나를 crictl logs 뒤에 직접 쓰면 안 되고,
조회한 ID를 넘겨야 한다.
sudo crictl ps -a --name etcd# CONTAINER 열의 ID를 복사sudo crictl logs <etcd-container-id>Pod 샌드박스만 보이면 crictl pods -a의 Pod ID로 그 안의 컨테이너를 다시 찾는다.
sudo crictl ps -a --pod <pod-id>sudo crictl logs <container-id>kubeadm으로 만든 클러스터에서, 컨트롤 플레인 컴포넌트는 Pod이다. 그런데 특별한 Pod이다.
/etc/kubernetes/manifests/ls /etc/kubernetes/manifests/# etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yamlkube-apiserver-controlplanekubectl delete pod 해도 되살아난다. kubelet이 파일을 다시 읽기 때문staticPodPath 로 경로가 정해진다# kubelet이 어느 디렉터리를 보는지 확인sudo grep staticPodPath /var/lib/kubelet/config.yaml스태틱 Pod은 스케줄러를 거치지 않으므로 taint·affinity의 영향을 받지 않는다.
컨트롤 플레인 노드가 NoSchedule taint를 가져도 컨트롤 플레인 컴포넌트가 뜨는 이유다.
모든 요청이 API 서버 하나로 모인다는 것은, 검문도 거기 한 곳에 모을 수 있다는 뜻이다.
그래서 인증·인가·정책 검사가 전부 이 안에서 정해진 순서로 일어난다.
kubectl apply -f pod.yaml 을 쳤을 때 API 서버 안에서 벌어지는 일.
인증·인가는 “누가 요청했나”까지만 본다. 요청의 내용이 규칙에 맞는지 — 특권 컨테이너 금지, 빠진 기본값 채우기 같은 — 를 검사할 단계가 따로 필요한데, 그게 admission이다. 요청을 고쳐 주는 Mutating과 검사만 하는 Validating 둘로 나뉜다.
이 순서를 알면 에러 코드로 원인을 짚을 수 있다.
401 = 인증, 403 = RBAC, 그 외 거부 = admission (Admission에서 자세히).
아무도 서로를 직접 호출하지 않는다. 전부 API 서버를 통한 watch다. watch는 반복 조회(폴링)가 아니라 변경 스트림 구독이다 — 오브젝트가 바뀌는 순간 API 서버가 이벤트를 밀어 주기 때문에, 그림의 릴레이가 지연 없이 이어진다. 느슨하게 결합되어 있어서 컴포넌트 하나가 죽어도 나머지는 계속 돈다.
앞 절까지가 “누가 무엇을 하는가”였다면, 이 절은 그들이 주고받는 물건의 모양이다. 루프가 돌려면 “원하는 상태”와 “실제 상태”가 한 오브젝트 안에 나란히 있어야 한다 — 그래서 모든 Kubernetes 오브젝트는 같은 뼈대를 가진다.
apiVersion: apps/v1 # 어느 API 그룹의 어느 버전인가kind: Deployment # 무엇인가metadata: # 이름·네임스페이스·라벨·애노테이션 name: web namespace: default labels: app: webspec: # 내가 원하는 상태 ← 사람이 쓴다 replicas: 3status: # 실제 상태 ← 컨트롤러가 쓴다 readyReplicas: 3spec은 사람이, status는 시스템이 쓴다. status를 직접 편집하지 않는다apiVersion이 v1이면 core 그룹(그룹 이름이 없다), apps/v1이면 apps 그룹리소스가 수백 종류인데 전부 한 묶음이면 버전을 따로 올릴 수가 없다.
그래서 관련된 리소스끼리 API 그룹으로 나누고 그룹마다 버전을 매긴다 —
apps/v1의 apps가 그룹, v1이 그 그룹의 버전이다.
Pod·Service처럼 초기부터 있던 것은 그룹 이름이 없는 core 그룹(apiVersion: v1)에 남아 있다.
kind와 apiVersion을 짝지어 못 쓰면 apply가 그대로 실패하니, 아래 두 명령으로 확인한다.
kubectl api-resources # 전체 리소스 목록 (약칭·그룹·네임스페이스 여부)kubectl api-resources --namespaced=false # 클러스터 스코프만 (Node, PV, ClusterRole …)kubectl api-versions # 사용 가능한 group/versionkubectl explain pod.spec.containers.resources # 필드 설명kubectl explain deployment.spec.strategy --recursive # 하위 전부 펼치기오브젝트를 어떻게 격리하고(네임스페이스), 어떻게 골라내는가(라벨). 이 둘이 이후 모든 장의 전제다.
kubectl get nskubectl create ns devkubectl get pods -A # 전체 네임스페이스kubectl config set-context --current --namespace=dev # 기본 네임스페이스 변경kubectl label pod nginx tier=frontendkubectl label pod nginx tier=backend --overwritekubectl label pod nginx tier- # 삭제 (뒤에 하이픈)
kubectl get pods -l tier=frontendkubectl get pods -l 'tier in (frontend,backend)'kubectl get pods -l '!tier' # 라벨이 없는 것kubectl get pods --show-labelskubectl get pods --field-selector status.phase=Runningkubectl get pods --field-selector spec.nodeName=node01kubectl get events --field-selector type=Warning필드 셀렉터는 라벨 셀렉터만큼 자주 쓰이진 않는다 — status.phase · spec.nodeName,
그리고 이벤트 필터(type=Warning) 정도가 실전 레퍼토리의 거의 전부다.
ownerReferences — 오브젝트가 누구에게서 만들어졌는지 기록한다. 지운 Pod이 자꾸 되살아날 때 “누가 만들었나”를 추적하는 근거가 이 필드다.
kubectl get pod web-abc-123 -o jsonpath='{.metadata.ownerReferences[0].kind}'# ReplicaSetkubectl delete deploy web --cascade=orphan 을 쓰면 Pod을 남길 수 있다kubectl이 보여주는 노드의 상태와, kubectl이 안 될 때 직접 뒤져야 하는
노드 위의 파일들. 트러블슈팅(트러블슈팅)의 기초 체력이다.
kubectl get nodes -o widekubectl describe node node01kubectl get node node01 -o yamldescribe node 에서 반드시 볼 곳:
| 항목 | 의미 |
|---|---|
| Conditions | Ready, MemoryPressure, DiskPressure, PIDPressure |
| Taints | 이 노드가 밀어내는 조건 (스케줄링) |
| Capacity / Allocatable | 전체 자원 / 실제 배정 가능한 자원 |
| Allocated resources | 현재 요청(request) 합계 — 사용량이 아니다 |
| Non-terminated Pods | 이 노드의 Pod 목록 |
nodeName만 채운다. 실행은 kubeletkubectl로 못 고친다/etc/kubernetes/manifests/의 스태틱 Pod — 파일을 고치면 즉시 반영kubectl explain과 kubectl api-resources 는 오프라인 문서다