본문 바로가기

Infrastructure

쿠버네티스 코드베이스 읽어보기

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