https://github.com/kubernetes/kubernetes
GitHub - kubernetes/kubernetes: Production-Grade Container Scheduling and Management
Production-Grade Container Scheduling and Management - kubernetes/kubernetes
github.com
쿠버네티스의 프로세스
쿠버네티스 클러스터는 프로세스 7개가 구성하고 있다.
컨트롤 플레인은 이전엔 마스터 노드라고 불렸으며 클러스터를 관리한다. 워커 노드에서는 실제 사용자 컨테이너 실행을 담당한다.
사용자 컨테이너가 폭주해서 노드 자원을 다 먹어도, 컨트롤 플레인이 죽지 않는다.
컨트롤 플레인 (마스터 노드에서 도는 것)
- kube-apiserver
모든 통신의 중심. REST API 게이트웨이. 인증/인가/스토리지 게이트키퍼.
- kube-scheduler
Pending 상태인 Pod을 어느 Node에 둘지 결정.
- kube-controller-manager
Deployment, ReplicaSet, Job 등 약 50개 컨트롤러를 한 프로세스에 묶음. "현재 상태 → 원하는 상태"로 수렴.
워커 노드에서 도는 것 (모든 노드에)
- kubelet
노드의 에이전트. apiserver에서 Pod 명세를 받아 컨테이너 런타임(containerd 등)에게 CRI로 "이거 띄워" 시킴.
- kube-proxy
Service의 가상 IP를 실제 Pod IP로 라우팅하는 iptables/IPVS/nftables 규칙 관리.
사용자가 손으로 쓰는 도구
- kubectl
kubectl get pods 같은 명령어. apiserver에 HTTP 요청 보내는 클라이언트.
- kubeadm
클러스터 부트스트랩 도구. 위 컴포넌트들을 처음 설치/조인할 때 사용.
GPT와의 질의응답 내용
1. scheduler랑 kubelet은 어떻게 대화하지? 컨트롤러끼리는 어떻게 협력하지?
답은 "하지 않는다". 모든 통신은 apiserver를 거친다 (hub-and-spoke)
apiserver (6443)
/ | \ \
↓ ↓ ↓ ↓
scheduler KCM kubelet (각자 WATCH)
apiserver만 진짜 API(6443)를 listen하고, 나머지 컴포넌트는 그저 자기가
apiserver에 watch를 걸어둔다. scheduler가 여는 10259 같은 포트는
healthz/metrics용 관제 채널일 뿐, 명령을 받는 API가 아니다.
이 구조의 의미:
- 컴포넌트 간 직접 통신이 없으니 장애가 전파되지 않는다. scheduler
죽어도 kubelet은 멀쩡.
- 누구든 watch만 걸면 협력자가 될 수 있다. 그래서 외부 오퍼레이터가 쉽게 만들어진다
2. 모든 컨트롤러는 같은 모양이다 (reconcile loop)
50개 컨트롤러가 다 다른 일을 하는데, 함수 모양이 거의 똑같다.
// pkg/controller/deployment/deployment_controller.go
func (dc *DeploymentController) syncDeployment(ctx, key) error {
d := dc.dLister.Get(key) // 1. 원하는 상태 읽기
actual := dc.getRelated(d) // 2. 현재 상태 읽기
// 3. diff만큼 apiserver에 POST/PATCH/DELETE
}
// pkg/controller/replicaset/replica_set.go
func (rsc *ReplicaSetController) syncReplicaSet(ctx, key) error {
rs := rsc.rsLister.Get(key)
pods := rsc.podLister.List(rs.Selector)
diff := len(pods) - rs.Spec.Replicas
if diff < 0 { rsc.manageReplicas(...) } // 부족하면 만듦
}
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
func (m *kubeGenericRuntimeManager) SyncPod(...) {
// 현재 컨테이너 상태 vs Pod.spec → diff → CRI 호출로 좁힘
}
전부 같은 구조다:
1. 원하는 상태(spec)를 읽고
2. 현재 상태(status, 또는 외부 시스템 상태)를 읽고
3. 둘의 차이를 좁힌다
K8s는 사실 이 패턴 하나의 변주로 거의 모든 일을 한다. Service의
endpoint 동기화, PVC binding, HPA 스케일, Job 재시도
부수 효과: 이 패턴은 idempotent하다. 시퀀스 중간에 뭐가 실패해도, 다음
사이클이 다시 시도해서 결국 수렴한다. 그래서 K8s가 견고하다.
3. 책임은 한 레벨씩만 위임된다
kubectl apply -f deployment.yaml을 쳤을 때 한 컴포넌트가 끝까지 처리하지 않는다.
Deployment 만들어짐
→ DeploymentController가 보고 → ReplicaSet 생성
→ ReplicaSetController가 보고 → Pod 생성 (단, nodeName 비어있음)
→ Scheduler가 보고 → Pod.spec.nodeName 채움
→ 해당 노드의 kubelet이 보고 → CRI로 containerd에게 → runc →
컨테이너 실행
각 컨트롤러는 자기 바로 아래 레벨만 책임진다. DeploymentController는
Pod이 뭔지 모른다, ReplicaSet만 안다. Scheduler는 Deployment가 뭔지
모른다, Pod에 노드 이름 박는 게 전부다.
Scheduler의 출력은 "Pod.spec.nodeName에 노드 이름 한 줄 박기"가 전부다.
컨테이너를 직접 띄우지 않는다. 이 한 줄을 박는 순간 해당 노드의 kubelet이 watch로 받아서 그때부터 자기
일을 시작한다.
이게 가능한 이유는 단계 2(hub-and-spoke) 덕분. 누구든 apiserver에
watch만 걸면 다음 단계 트리거가 된다.
4. 선언적 API와 etcd의 역할
Pod.spec = 원하는 상태, Pod.status = 현재 상태. 이 둘이 etcd에 같이
저장된다.
- 사용자/컨트롤러는 spec만 쓴다
- kubelet은 status를 업데이트한다
- 다른 컨트롤러는 spec과 status를 보고 자기 일을 결정한다
apiserver를 제외하고 etcd에 직접 접근하는 컴포넌트는 없다. scheduler도, kubelet도, controller-manager도 전부
apiserver를 통해서만 데이터에 접근한다.
이유는 미들웨어를 한 곳에 모으기 위해서, apiserver의 9단계 필터(인증, 인가, admission, validation, ...)를 거치지 않으면 etcd에 못 쓴다. 보안과 정합성이 단 한 곳에서 관리된다.
staging/src/k8s.io/apiserver/pkg/server/filters/
↓ Authentication
↓ Authorization (RBAC)
↓ Mutating admission ← 사이드카 자동 주입 같은 거
↓ Validating admission ← PodSecurity 검사
↓ Conversion ← versioned API → internal 타입
↓
etcd 쓰기
이슈 살펴보기 (kubelet panic)
https://github.com/kubernetes/kubernetes/issues/137993
kubelet panic when cgroupsPerQOS set false · Issue #137993 · kubernetes/kubernetes
What happened? When start kubelet with cgroupsPerQOS set to false, kubelet will panic. E0319 17:08:49.016993 4073428 panic.go:262] "Observed a panic" panic="runtime error: invalid memory address or...
github.com
cgroupsPerQOS=false 설정 + 누군가 Pod resize 시도 → kubelet이 nil pointer dereference로 panic하면서 죽는 이슈
// pkg/kubelet/kuberuntime/kuberuntime_manager.go:842
currentPodMemoryConfig, err := pcm.GetPodCgroupConfig(pod,
v1.ResourceMemory)
if err != nil {
// ... 에러 처리 ...
}
// ↑ err만 체크. currentPodMemoryConfig가 nil인지는 안 봄.
// pkg/kubelet/kuberuntime/kuberuntime_manager.go:1017
if errResize := resizeContainers(
v1.ResourceMemory,
int64(*currentPodMemoryConfig.Memory), // ← 💥 PANIC
*podResources.Memory, 0, 0,
); errResize != nil { ... }
currentPodMemoryConfig가 nil인 상태에서 *currentPodMemoryConfig.Memory를 dereference → nil pointer panic. pcm.GetPodCgroupConfig의 pcm은 인터페이스이고 구현체가 두 개 있다.
cgroupsPerQOS=false일 때 kubelet은 B를 사용한다.
구현체 A — 실제 cgroup 관리하는 놈
// pkg/kubelet/cm/pod_container_manager_linux.go:143
func (m *podContainerManagerImpl) GetPodCgroupConfig(pod *v1.Pod,
resource v1.ResourceName) (*ResourceConfig, error) {
// 진짜로 cgroup 읽어서 ResourceConfig 채워서 반환
}
구현체 B — 아무것도 안 하는 stub (no-op)
// pkg/kubelet/cm/pod_container_manager_linux.go:363
func (m *podContainerManagerNoop) GetPodCgroupConfig(_ *v1.Pod, _
v1.ResourceName) (*ResourceConfig, error) {
return nil, nil // ← 여기. nil 반환 + err도 nil
}
cgroupsPerQOS=true(기본값)는 "QoS 클래스별로 cgroup 계층 만들어서 자원 격리해라"는 모드. false로 끄면 per-Pod cgroup 관리를 안 하니까 stub 구현체를 갖다 끼우는 것. Go의 인터페이스는 같은 메서드 시그니처로 다른 구현을 갈아끼울 수 있다.
문제는 호출하는 쪽(kuberuntime_manager.go)이 "노옵 구현체가 nil을 반환할 수 있다"는 사실을 모르고 짜여졌다.
다음과 같은 공격이 가능
공격자 시나리오:
1. `cgroupsPerQOS=false` 노드가 있는 클러스터에 침투
2. namespace 하나에 Pod create 권한만 있어도 됨
3. Pod resize subresource를 POST → 해당 노드의 kubelet panic
4. 노드 NotReady → 그 노드의 모든 워크로드 evict
5. 1~4를 다른 노드에 반복 → 클러스터 전체 마비
'Infrastructure' 카테고리의 다른 글
| Kubernetes Makefile을 읽으며 배우는 GNU make (0) | 2026.07.12 |
|---|