컨트롤 플레인 (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으로 돌기 때문이다 — 뒤에서 다룬다.
flowchart LR
U["kubectl / 클라이언트"] --> API
subgraph CP["컨트롤 플레인"]
API["kube-apiserver<br/>유일한 관문"]
ETCD[("etcd<br/>유일한 저장소")]
SCH["kube-scheduler"]
CM["kube-controller-manager"]
API <--> ETCD
SCH --> API
CM --> API
end
subgraph N1["워커 노드"]
KL["kubelet"]
KP["kube-proxy"]
CR["containerd"]
KL --> CR
end
KL --> API
KP --> API
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef store fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class API key
class ETCD store
class SCH,CM,KL,KP,CR,U mute
화살표 방향이 핵심이다. 모든 컴포넌트가 API 서버를 향한다. API 서버가 다른 컴포넌트를 호출하지 않는다.
kubectl은 그 위의 얇은 클라이언트일 뿐etcd만 백업하면 클러스터 전체를 복구할 수 있다 (PV 안의 데이터는 제외)# etcd 안의 키 구조 — 오브젝트 경로가 그대로 키다/registry/pods/default/nginx/registry/deployments/kube-system/coredns/registry/secrets/default/my-secretflowchart LR
W["spec.nodeName 이 비어 있는 Pod"] --> F["Filtering<br/>못 놓는 노드를 거른다"]
F --> S["Scoring<br/>남은 노드에 점수"]
S --> B["spec.nodeName 을 채운다<br/>★ 여기까지가 전부"]
B -.->|"실행은 kubelet 의 몫"| KL["kubelet"]
F -.->|"리소스 부족 · taint<br/>nodeSelector 불일치 …"| X["전부 걸러지면 Pending"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class F,S key
class X bad
class W,B,KL mute
spec.nodeName이 비어 있는 Pod을 찾는다spec.nodeName을 채운다. 그게 전부다두 단계로 고른다
flowchart LR
CM["kube-controller-manager<br/>프로세스 하나"]
CM --> L1["Deployment 컨트롤러"]
CM --> L2["ReplicaSet 컨트롤러"]
CM --> L3["Node 컨트롤러"]
CM --> L4["Job / CronJob 컨트롤러"]
CM --> L5["EndpointSlice 컨트롤러"]
CM --> L6["ServiceAccount 컨트롤러"]
CM --> L7["PV / PVC 컨트롤러"]
L1 --> LOOP["각 루프: 원하는 상태 관찰<br/>→ 실제 상태 관찰<br/>→ 차이만큼 API 호출"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class CM,LOOP key
class L1,L2,L3,L4,L5,L6,L7 mute
| 컨트롤러 | 하는 일 |
|---|---|
| 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이면 스케줄러다.
NotReady)flowchart LR
API["kube-apiserver"] -->|"Service · EndpointSlice watch"| KP["kube-proxy<br/>DaemonSet"]
KP -->|"규칙만 심고 빠진다"| K["노드 커널<br/>iptables · ipvs · nftables"]
C["클라이언트 Pod"] -->|"패킷"| K
K -->|"DNAT"| P["대상 Pod"]
KP -.->|"트래픽은 여기를 통과하지 않는다"| K
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef kern fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class KP key
class K kern
class API,C,P mute
iptables(기본) / ipvs / nftablescrictl 을 쓴다# 노드 안에서 (kubectl이 안 될 때의 생명줄)sudo crictl ps # 실행 중 컨테이너sudo crictl ps -a # 죽은 것 포함sudo crictl logs <container-id>sudo crictl pods # Pod 샌드박스 목록kubeadm으로 만든 클러스터에서, 컨트롤 플레인 컴포넌트는 Pod이다. 그런데 특별한 Pod이다.
flowchart LR
F["/etc/kubernetes/manifests/*.yaml"] -->|"kubelet 이 직접 읽는다"| KL["kubelet"]
KL --> CR["containerd"] --> POD["컨트롤 플레인 Pod"]
KL -.->|"읽기 전용 미러 Pod 등록"| API["kube-apiserver"]
NOTE["API 서버도 스케줄러도 거치지 않는다<br/>→ 순환 의존을 피한다"] -.- F
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class F key
class NOTE warn
class KL,CR,POD,API mute
/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를 가져도 컨트롤 플레인 컴포넌트가 뜨는 이유다.
kubectl apply -f pod.yaml 을 쳤을 때 API 서버 안에서 벌어지는 일.
flowchart LR
R["요청"] --> A["Authentication<br/>너는 누구인가"]
A -->|"실패 401"| X1["거부"]
A --> Z["Authorization<br/>해도 되는가 · RBAC"]
Z -->|"실패 403"| X2["거부"]
Z --> M["Mutating Admission<br/>요청을 고친다"]
M --> V["Validating Admission<br/>규칙 위반인가"]
V -->|"위반"| X3["거부"]
V --> S["스키마 검증 → etcd 저장"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class S ok
class X1,X2,X3 bad
class A,Z key
class R,M,V mute
이 순서를 알면 에러 코드로 원인을 짚을 수 있다.
401 = 인증, 403 = RBAC, 그 외 거부 = admission (14장에서 자세히).
sequenceDiagram
participant K as kubectl
participant A as apiserver
participant D as Deployment 컨트롤러
participant R as ReplicaSet 컨트롤러
participant S as scheduler
participant L as kubelet
K->>A: Deployment 생성
A->>D: watch 이벤트
D->>A: ReplicaSet 생성
A->>R: watch 이벤트
R->>A: Pod 생성 (nodeName 비어 있음)
A->>S: watch 이벤트
S->>A: nodeName 채움 (binding)
A->>L: 내 노드의 Pod
L->>L: 이미지 pull → 컨테이너 실행
L->>A: 상태 보고 (Running)
아무도 서로를 직접 호출하지 않는다. 전부 API 서버를 통한 watch다. 느슨하게 결합되어 있어서 컴포넌트 하나가 죽어도 나머지는 계속 돈다.
모든 Kubernetes 오브젝트는 같은 뼈대를 가진다.
apiVersion: apps/v1 # 어느 API 그룹의 어느 버전인가kind: Deployment # 무엇인가metadata: # 이름·네임스페이스·라벨·애노테이션 name: web namespace: default labels: app: webspec: # 내가 원하는 상태 ← 사람이 쓴다 replicas: 3status: # 실제 상태 ← 컨트롤러가 쓴다 readyReplicas: 3flowchart LR
H["사람"] -->|"쓴다"| SPEC["spec<br/>원하는 상태"]
SPEC --> CTRL["컨트롤러"]
CTRL -->|"현실을 맞춘다"| REAL["실제 클러스터"]
REAL --> STATUS["status<br/>실제 상태"]
CTRL -->|"쓴다"| STATUS
H -.->|"직접 편집하지 않는다 ❌"| STATUS
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SPEC key
class STATUS bad
class H,CTRL,REAL mute
spec은 사람이, status는 시스템이 쓴다. status를 직접 편집하지 않는다apiVersion이 v1이면 core 그룹(그룹 이름이 없다), apps/v1이면 apps 그룹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 # 하위 전부 펼치기flowchart LR
subgraph NS["네임스페이스 안 — 이름의 유효 범위"]
P["Pod"]
S["Service"]
C["ConfigMap · Secret"]
RB["Role · RoleBinding"]
end
subgraph CL["클러스터 스코프 — 네임스페이스가 없다"]
N["Node"]
PV["PersistentVolume"]
SC["StorageClass"]
CR["ClusterRole"]
end
NET["네트워크 격리가 아니다 ⚠️<br/>다른 네임스페이스와 통신은 그대로 된다<br/>막으려면 NetworkPolicy · 12장"] -.- NS
classDef ns fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef cl fill:#fef3c7,stroke:#d97706,color:#78350f
classDef warn fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class P,S,C,RB ns
class N,PV,SC,CR cl
class NET warn
kubectl get nskubectl create ns devkubectl get pods -A # 전체 네임스페이스kubectl config set-context --current --namespace=dev # 기본 네임스페이스 변경flowchart LR
L["Pod 라벨<br/>app=web"] --> SVC["Service selector"]
L --> RS["ReplicaSet selector"]
L --> NP["NetworkPolicy podSelector"]
L --> PDB["PodDisruptionBudget selector"]
A["애노테이션<br/>선택 대상이 아니다"] -.->|"도구가 읽는 메타데이터"| TOOL["kubectl · 컨트롤러 · 외부 도구"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class L key
class SVC,RS,NP,PDB,A,TOOL mute
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=WarningownerReferences — 오브젝트가 누구에게서 만들어졌는지 기록한다.
flowchart LR
D["Deployment web"] -->|ownerReference| RS["ReplicaSet web-5d4f"]
RS -->|ownerReference| P["Pod web-5d4f-x7k2p"]
DEL["부모를 지우면"] -->|"가비지 컬렉터가 자식을 지운다 · cascade"| P
ORPH["--cascade=orphan"] -.->|"Pod 을 남긴다"| P
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class D key
class ORPH warn
class RS,P,DEL mute
kubectl get pod web-abc-123 -o jsonpath='{.metadata.ownerReferences[0].kind}'# ReplicaSetkubectl delete deploy web --cascade=orphan 을 쓰면 Pod을 남길 수 있다kubectl get nodes -o widekubectl describe node node01kubectl get node node01 -o yamldescribe node 에서 반드시 볼 곳:
| 항목 | 의미 |
|---|---|
| Conditions | Ready, MemoryPressure, DiskPressure, PIDPressure |
| Taints | 이 노드가 밀어내는 조건 (7장) |
| Capacity / Allocatable | 전체 자원 / 실제 배정 가능한 자원 |
| Allocated resources | 현재 요청(request) 합계 — 사용량이 아니다 |
| Non-terminated Pods | 이 노드의 Pod 목록 |
nodeName만 채운다. 실행은 kubeletkubectl로 못 고친다/etc/kubernetes/manifests/의 스태틱 Pod — 파일을 고치면 즉시 반영kubectl explain과 kubectl api-resources 는 오프라인 문서다