Skip to content

필사 모드: Kubernetes 내부 구조 완전 해부 — etcd Raft, 스케줄러, 컨트롤러 리컨사일, CRI/CNI/CSI까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — kubectl apply는 마법이 아니다

"`kubectl apply -f deployment.yaml`를 쳤더니 30초 뒤 Pod가 떠 있더라." 이 한 줄 뒤에서는 **최소 6개의 컴포넌트가 순차적으로 reconcile 루프를 돌고, Raft 합의가 한 번 일어나고, 가상 네트워크 인터페이스가 생성되고, 컨테이너 런타임이 OCI 스펙으로 컨테이너를 띄운다.** Kubernetes를 쓰는 사람은 많지만, 이 30초를 단계별로 설명할 수 있는 사람은 드물다.

> 이전 글 [컨테이너와 Docker 내부 구조](/blog/culture/2026-04-15-container-docker-internals-namespace-cgroup-overlayfs-seccomp-capabilities-deep-dive-guide-2025)에서 namespace와 cgroup, OverlayFS, runc까지 봤다. 이번엔 그 위에 올라탄 Kubernetes가 어떻게 수천 개의 컨테이너를 선언적(declarative)으로 관리하는지, 특히 **"선언적"이라는 말이 내부적으로 무엇을 의미하는지**를 파헤친다.

장애가 났을 때 `kubectl get events`만 보고 원인을 추측하는 것과, "지금 scheduler의 PreFilter 단계에서 NodeAffinity가 실패했고, 그래서 Pending이다"라고 정확히 짚는 것의 차이는 **내부 구조를 아는지 여부**에서 갈린다.

1. Kubernetes의 10,000피트 뷰 — 컨트롤 플레인과 데이터 플레인

Kubernetes 클러스터는 크게 두 층으로 나뉜다.

**컨트롤 플레인(Control Plane)** — "무엇을 해야 하는가"를 결정하는 뇌:

- **kube-apiserver** — REST API 게이트웨이. 모든 요청의 유일한 입구

- **etcd** — 클러스터의 진실 소스(single source of truth). Raft 합의 기반 분산 KV 스토어

- **kube-scheduler** — Pod를 어느 Node에 배치할지 결정

- **kube-controller-manager** — Deployment, ReplicaSet, StatefulSet 등 40+개 컨트롤러의 집합

- **cloud-controller-manager** — 클라우드 제공자별 로드밸런서, 볼륨, 라우트 관리

**데이터 플레인(Data Plane)** — "실제로 실행"하는 근육:

- **kubelet** — 각 Node에서 Pod를 띄우고 상태를 보고하는 에이전트

- **kube-proxy** — Service의 iptables/IPVS 룰을 관리하는 네트워크 프록시

- **컨테이너 런타임** — containerd, CRI-O 등 CRI 구현체

핵심 원칙 하나만 기억하면 된다: **모든 컴포넌트는 etcd를 통해서만 통신하고, 자신의 관심사(resource)를 watch하며 원하는 상태(desired state)와 현재 상태(current state)의 차이를 좁힌다.** 이것이 "선언적"의 정체다.

2. `kubectl apply` 한 줄의 여정 — 30초를 밀리초 단위로

다음 YAML을 apply했다고 하자.

apiVersion: apps/v1

kind: Deployment

metadata:

name: nginx

spec:

replicas: 3

selector:

matchLabels: { app: nginx }

template:

metadata:

labels: { app: nginx }

spec:

containers:

- name: nginx

image: nginx:1.25

**T+0ms — kubectl 클라이언트**

1. `~/.kube/config` 읽어서 API Server 주소와 인증서 로드

2. YAML을 JSON으로 변환

3. `PATCH /apis/apps/v1/namespaces/default/deployments/nginx`로 전송 (Server-Side Apply 사용 시)

**T+5ms — kube-apiserver (HTTP 레이어)**

1. TLS 종료, 클라이언트 인증서 검증

2. **Authentication**: 누구인가? (x509, Bearer Token, OIDC 등)

3. **Authorization**: 무엇을 할 권한이 있는가? (RBAC, ABAC, Webhook)

4. **Admission Controllers (Mutating)**: 요청을 수정할 수 있나? (기본 값 주입, 사이드카 주입)

5. **Schema Validation**: OpenAPI 스펙에 맞는가?

6. **Admission Controllers (Validating)**: 정책 위반 없는가? (ResourceQuota, PodSecurity)

**T+20ms — etcd 쓰기**

1. API Server가 etcd에 `PUT /registry/deployments/default/nginx`

2. **Raft 리더**가 proposal을 만들어 팔로워들에게 AppendEntries RPC

3. 과반수(quorum)가 응답하면 commit

4. 리더가 API Server에 ACK

5. API Server는 같은 트랜잭션에서 `resourceVersion`을 올린다

**T+25ms — Watch 이벤트 팬아웃**

- API Server가 Deployment의 watcher들에게 ADDED 이벤트 송신

- `kube-controller-manager`의 Deployment Controller가 이벤트 수신

**T+30~100ms — Deployment Controller 리컨사일**

1. Deployment spec을 읽음 (replicas: 3)

2. 해당 라벨로 ReplicaSet 조회 → 없음

3. **ReplicaSet을 생성** (이것도 API Server 경유 → etcd 저장)

**T+100~200ms — ReplicaSet Controller 리컨사일**

1. ReplicaSet을 watch하던 컨트롤러가 이벤트 수신

2. spec.replicas=3, 현재 Pod 수=0

3. **Pod 3개를 생성** (nodeName 비어있음, Pending 상태)

**T+200~500ms — Scheduler**

1. `spec.nodeName == ""`인 Pod를 watch하던 스케줄러가 이벤트 수신

2. **Filter 단계**: 61개 Node 중 리소스가 부족하거나 taint가 있는 것을 제거 → 15개 남음

3. **Score 단계**: 15개 Node에 점수 매김 (LeastRequestedPriority, ImageLocalityPriority 등)

4. 최고점 Node 선택 → API Server에 `POST /pods/{name}/binding` 호출

5. API Server가 Pod의 `spec.nodeName`을 업데이트

**T+500ms~2s — kubelet**

1. 해당 Node의 kubelet이 자기 Node에 바인딩된 Pod를 watch하다가 이벤트 수신

2. **SyncPod 리컨사일 루프**:

- 볼륨 마운트 (CSI 플러그인 호출)

- 네트워크 네임스페이스 생성 후 CNI 플러그인 호출 → IP 할당

- CRI를 통해 컨테이너 런타임에 "이 이미지로 컨테이너 만들어"

3. containerd가 이미지 pull → 이전 글의 OverlayFS 레이어 구성

4. runc가 namespace/cgroup 설정 후 `execve`로 nginx 실행

**T+3s~30s — 상태 전파**

1. kubelet이 컨테이너 상태를 주기적으로 API Server에 보고 (status subresource)

2. ReplicaSet Controller는 Pod Running 카운트를 보고 status 업데이트

3. Deployment Controller는 ReplicaSet status를 보고 자신의 status 업데이트

4. `kubectl get deploy`에 "3/3 Ready"가 뜬다

**이 30초 동안 등장한 핵심 패턴 두 개만 기억하면 된다.**

1. **Level-triggered reconciliation** — 이벤트를 놓쳐도 상태를 주기적으로 다시 확인한다

2. **Watch-based coordination** — 모든 것이 API Server의 watch stream을 통해 eventual하게 수렴한다

3. etcd와 Raft — 클러스터의 유일한 진실

왜 etcd인가?

Kubernetes 초창기(2014~2015)에는 ZooKeeper, Consul, etcd 모두 후보였다. **etcd가 선택된 이유**:

- gRPC 기반 API (HTTP/2 + Protocol Buffers)

- **Watch**: 클라이언트가 키 변경을 스트리밍으로 구독 가능

- MVCC (Multi-Version Concurrency Control): 과거 버전 조회 가능

- Raft 기반 (ZooKeeper의 ZAB보다 이해하기 쉽고 구현이 단순)

etcd는 Kubernetes API 객체 하나하나를 `/registry/<resource>/<namespace>/<name>` 경로에 저장한다. 예:

/registry/deployments/default/nginx

/registry/pods/default/nginx-6d4cf56db6-abc12

/registry/services/default/nginx

Raft 합의 — 5대 장비에서 정답 하나를 뽑는 법

etcd 클러스터는 보통 **3대 또는 5대**의 홀수로 구성한다. 이유는 **과반수(quorum)** 때문이다.

- 3대 → 2대가 살아있으면 동작

- 5대 → 3대가 살아있으면 동작 (동시에 2대까지 실패 허용)

- 7대 → 4대 필요 (네트워크 RTT 증가로 보통 비추천)

**Raft의 세 가지 역할**:

- **Leader** — 모든 쓰기를 받고 복제

- **Follower** — Leader의 명령을 따름

- **Candidate** — 선거 중

**Leader Election**:

1. Follower가 election timeout(150~300ms 랜덤) 동안 leader 하트비트를 못 받으면 Candidate로 전환

2. term을 +1하고 자신에게 투표, 다른 노드에 RequestVote RPC

3. 과반수 표를 얻으면 Leader, 못 얻으면 다음 term으로

**Log Replication**:

1. 클라이언트 요청이 Leader에게 도착 → log entry 추가

2. Leader가 AppendEntries RPC로 모든 Follower에게 복제

3. **과반수가 디스크에 fsync 완료**했다고 응답하면 commit

4. Leader는 commit된 entry를 상태 머신(etcd의 BoltDB)에 적용

5. 다음 AppendEntries에서 commit index를 알려 Follower들도 적용

etcd 디스크 성능이 Kubernetes의 심장인 이유

모든 쓰기마다 **fsync**가 일어난다. 즉 **etcd의 디스크 fsync 지연이 API 서버의 쓰기 지연**이다.

운영 기준:

- fsync p99가 10ms 미만 → 건강

- fsync p99가 100ms 이상 → API 요청 타임아웃 발생, 클러스터 불안정

- 디스크는 **로컬 SSD** 권장, NFS/EBS gp2는 비추

대규모 클러스터(5000+ node)에서 etcd가 포화되면 전체 컨트롤 플레인이 먹통이 된다. 그래서 Google 내부에서는 **이벤트(Event) 전용 etcd 클러스터**를 분리하는 패턴을 쓴다 (`--etcd-servers-overrides=/events#https://events-etcd:2379`).

4. Watch와 Informer — 수천 대를 0초에 가깝게 동기화

순진한 방법은 망한다

만약 모든 컨트롤러가 "1초마다 Deployment 전체 목록을 가져옴"으로 동작한다면? 5000개 Deployment × 40개 컨트롤러 × 1Hz = 초당 200,000번의 API 호출. 클러스터가 즉사한다.

Watch — gRPC 스트림으로 변경만 받기

API Server는 etcd의 watch를 래핑해서 HTTP long-polling 또는 HTTP/2 스트림으로 클라이언트에게 변경 이벤트만 보낸다.

GET /api/v1/pods?watch=true&resourceVersion=12345

클라이언트가 받는 이벤트 타입:

- `ADDED` — 새 객체

- `MODIFIED` — 변경된 객체

- `DELETED` — 삭제된 객체

- `BOOKMARK` — "아직 살아있어요" 하트비트 (Kubernetes 1.16+)

Informer — 모든 컨트롤러의 공통 기반

직접 watch를 쓰는 건 고통스럽다. 연결이 끊기면? 재시작 후 처음부터 받나? 그래서 **client-go의 Informer 패턴**이 탄생했다.

┌──────────────┐ List+Watch ┌──────────────┐

│ API Server │ ──────────────> │ Reflector │

└──────────────┘ └──────┬───────┘

│ Delta

┌──────────────┐

│ DeltaFIFO │

└──────┬───────┘

│ Pop

┌──────────────┐ ┌──────────────┐

│ Indexer │◄────►│ Thread-safe │

│ (local cache)│ │ Store │

└──────┬───────┘ └──────────────┘

│ Event

┌──────────────┐

│ EventHandler │

└──────┬───────┘

│ Enqueue

┌──────────────┐

│ WorkQueue │

└──────┬───────┘

│ Process

┌──────────────┐

│ Reconciler │

└──────────────┘

핵심 아이디어:

1. **Reflector**: 첫 List로 전체 상태 받고, 그 뒤로는 Watch로 delta만 받음

2. **Indexer**: 로컬 메모리 캐시. 컨트롤러는 여기서만 읽음(API Server 호출 X)

3. **WorkQueue**: rate-limiting + retry + dedup

4. **Reconciler**: 객체 이름만 받아서, Indexer에서 최신 상태를 읽고 처리

이 구조 덕분에 컨트롤러 하나가 수만 개의 객체를 추적하면서도 API Server 부하는 최소화된다.

5. 스케줄러 — "이 Pod를 어디에 둘까?"

2단계 파이프라인: Filter → Score

Pod가 Pending으로 들어오면 scheduler는 Node 목록을 두 번 훑는다.

**1단계: Filter (Predicate)** — Node가 **조건을 만족하는가?** 예/아니오

- `NodeResourcesFit` — CPU/메모리 요청량이 Node 가용량보다 적은가?

- `NodeAffinity` — nodeSelector/affinity 만족?

- `TaintToleration` — taint를 tolerate하는가?

- `VolumeBinding` — 요구한 PV가 해당 Node의 zone에 있는가?

- `InterPodAffinity` — 다른 Pod와의 어피니티 규칙?

**2단계: Score (Priority)** — 필터를 통과한 Node들을 **0~100점으로 채점**

- `LeastAllocated` — 리소스 여유가 많은 쪽 선호

- `BalancedAllocation` — CPU와 메모리가 균형있게 사용되는 쪽

- `ImageLocality` — 이미 이미지가 있는 Node 선호 (pull 시간 절약)

- `TopologySpreadConstraints` — zone/region별로 골고루 분산

각 플러그인이 점수를 매기고, 가중치를 곱해서 합산 후 최고점 Node가 승자.

Framework 플러그인 — v1.19+의 확장점

1.19부터 scheduler가 **Scheduling Framework**로 재작성되어, 각 단계가 플러그인 훅으로 열렸다.

queueSort → preFilter → filter → postFilter → preScore → score →

normalizeScore → reserve → permit → preBind → bind → postBind

커스텀 스케줄러를 만들고 싶으면 이 훅들을 구현해서 scheduler 바이너리에 플러그인으로 빌드하면 된다 (GPU 토폴로지 스케줄링, 가격 최적화 등).

Preemption — 우선순위 높은 Pod가 들어올 때

모든 Node가 꽉 차서 새 Pod가 스케줄 안 되는데, 그 Pod의 priority가 기존 Pod들보다 높다면?

1. 스케줄러가 "누굴 쫓아내면 이 Pod가 들어갈까?"를 탐색

2. 가장 낮은 우선순위 Pod들을 찾아 **graceful termination**으로 삭제

3. 해당 Pod가 Pending에서 해당 Node로 스케줄

이 때문에 critical한 워크로드는 `PriorityClass`로 높은 값을 주고, 쫓겨나면 안 되는 건 `preemptionPolicy: Never`를 설정한다.

6. 컨트롤러 — Kubernetes의 진정한 엔진

컨트롤러 패턴 한 줄 정의

> **현재 상태를 관찰하고, 원하는 상태와 다르면 액션을 취해서 둘을 일치시킨다. 이 루프를 영원히 반복한다.**

모든 컨트롤러의 핵심은 `Reconcile(key)` 함수 하나다.

func (c *DeploymentController) Reconcile(key string) error {

namespace, name := splitKey(key)

deployment, err := c.lister.Deployments(namespace).Get(name)

if errors.IsNotFound(err) {

return nil // 이미 삭제됨, 할 일 없음

}

if err != nil {

return err

}

// 1. 원하는 상태

desired := deployment.Spec.Replicas

// 2. 현재 상태

rsList, _ := c.rsLister.ReplicaSets(namespace).List(selector)

current := totalReadyPods(rsList)

// 3. 차이를 좁히기

if current < desired {

c.scaleUp(deployment, desired - current)

} else if current > desired {

c.scaleDown(deployment, current - desired)

}

// 4. 상태 업데이트

c.updateStatus(deployment)

return nil

}

kube-controller-manager에 들어있는 40+ 컨트롤러 (일부)

| 컨트롤러 | 감시 대상 | 하는 일 |

|---|---|---|

| Deployment | Deployment | ReplicaSet을 만들고 롤아웃 관리 |

| ReplicaSet | ReplicaSet | Pod 수를 spec.replicas에 맞춤 |

| StatefulSet | StatefulSet | 순서대로 Pod 생성/삭제, stable network ID |

| DaemonSet | DaemonSet | 모든 Node에 Pod 하나씩 |

| Job / CronJob | Job | Pod 완료까지 대기, 실패 시 재시도 |

| Node | Node | NotReady Node의 Pod 축출 |

| Endpoint / EndpointSlice | Service + Pod | Service의 실제 Pod IP 목록 유지 |

| HPA | HPA + metrics | CPU/메모리 기반 replicas 자동 조정 |

| GC | 모든 owner reference | 부모 삭제 시 자식 삭제 |

| ServiceAccount / Token | SA | 자동 토큰 생성 |

| PV / PVC | PV, PVC | 바인딩, 프로비저닝 |

Operator — 사용자 정의 컨트롤러의 패턴

CoreOS가 2016년에 제시한 패턴. **CRD(Custom Resource Definition)**로 도메인 객체를 정의하고, 그 객체를 감시하는 컨트롤러를 만든다.

apiVersion: postgresql.crunchydata.com/v1

kind: PostgresCluster

metadata:

name: my-db

spec:

replicas: 3

storageClass: fast-ssd

backup:

schedule: "0 2 * * *"

PGO Operator는 이 CRD를 watch하다가:

- Primary용 StatefulSet 1개 + Replica용 2개 생성

- Pgbouncer Deployment 생성

- Backup용 CronJob 생성

- Primary 장애 시 Replica를 승격

이 모든 게 "Kubernetes 네이티브"로, 그냥 `kubectl apply`로 시작된다.

7. Service와 kube-proxy — Pod IP가 바뀌어도 접속이 끊기지 않는 마법

문제 — Pod IP는 언제든 바뀐다

Pod가 재시작되면 IP가 바뀐다. 클라이언트가 Pod IP를 직접 참조하면 매번 깨진다. 그래서 **Service**라는 가상 IP(ClusterIP)가 필요하다.

iptables 모드 — 초창기 방식

`kube-proxy`가 각 Node에서 iptables 룰을 동적으로 업데이트한다.

간략화한 iptables 체인

KUBE-SERVICES → KUBE-SVC-XXX (Service ClusterIP 매칭)

KUBE-SVC-XXX → KUBE-SEP-A (33% 확률 DNAT → Pod A)

→ KUBE-SEP-B (33% 확률 DNAT → Pod B)

→ KUBE-SEP-C (33% 확률 DNAT → Pod C)

요청이 Service IP로 오면 랜덤으로 Pod IP 하나로 DNAT된다. 문제는 **Pod가 많을수록 iptables 룰이 선형적으로 증가**해서, 수천 개 Pod에서는 커널이 매 패킷마다 수만 개 규칙을 순회하며 느려진다.

IPVS 모드 — 해시 테이블 기반

`--proxy-mode=ipvs`로 바꾸면 Linux IPVS(IP Virtual Server)를 쓴다.

- 해시 테이블 기반 → O(1) 조회

- 라운드로빈, Least Connection, Source Hash 등 다양한 알고리즘

- 대규모 클러스터(1000+ Service)에서 필수

eBPF 모드 — Cilium의 세상

Cilium은 kube-proxy를 **완전히 대체**한다. iptables/IPVS 대신 **eBPF 프로그램**이 커널의 XDP/TC hook에서 패킷을 가로채 직접 Pod로 라우팅한다. 장점:

- 훨씬 빠름 (hook이 커널 초기에 위치)

- iptables 룰 폭증 없음

- 네트워크 정책, 관측성까지 하나의 프레임워크로

최근(2024~2025) 많은 대규모 클러스터가 `kube-proxy-replacement=strict`로 kube-proxy를 아예 꺼버린다.

8. CRI / CNI / CSI — 3대 플러그인 인터페이스

Kubernetes의 우아함은 **세 가지 핵심 기능을 인터페이스로 추상화**한 데 있다.

CRI (Container Runtime Interface) — "컨테이너를 어떻게 띄울까"

- **Why**: 초창기에는 Docker만 지원했는데, rkt, CRI-O 등이 등장하면서 매번 kubelet 코드를 수정할 수 없었다

- **How**: kubelet이 gRPC로 `RuntimeService`와 `ImageService`를 호출

- **구현체**: containerd (기본), CRI-O (Red Hat 선호), Mirantis cri-dockerd (Docker 호환용)

주요 RPC:

RunPodSandbox — pause 컨테이너로 namespace 그룹 생성

CreateContainer — 해당 sandbox에 컨테이너 생성

StartContainer — 컨테이너 시작

ExecSync — kubectl exec의 실체

2022년 1.24에서 **dockershim이 제거**된 이후 기본은 containerd. Docker 자체는 containerd를 내부적으로 쓰기 때문에 "Docker 이미지"는 그대로 돌아간다.

CNI (Container Network Interface) — "Pod에 IP를 어떻게 줄까"

- **Why**: Kubernetes는 "모든 Pod는 IP를 가진다, 같은 클러스터의 Pod끼리는 NAT 없이 통신한다"만 규정. 구현은 자유

- **How**: CNI 플러그인은 단순한 바이너리. kubelet이 namespace 경로를 인자로 주면 플러그인이 veth/IP/라우트를 설정하고 결과 JSON 반환

대표 플러그인:

- **Flannel** — VXLAN 오버레이. 간단하지만 정책 없음

- **Calico** — BGP 기반 라우팅. NetworkPolicy 네이티브

- **Cilium** — eBPF 기반. L3~L7 정책, 관측성

- **AWS VPC CNI** — Pod에게 VPC ENI의 secondary IP를 직접 할당 (클라우드 네이티브)

- **Weave** — 소프트웨어 메시. 소규모에서 쉬움

CSI (Container Storage Interface) — "볼륨을 어떻게 마운트할까"

- **Why**: in-tree 플러그인(kubernetes 소스에 직접 포함)은 릴리스 속도를 묶어버렸다

- **How**: CSI 드라이버는 **Controller 플러그인**(볼륨 생성/삭제)과 **Node 플러그인**(마운트)으로 구성된 gRPC 서비스

플로우:

1. PVC 생성 → external-provisioner가 CSI Controller에 `CreateVolume` 호출 → 클라우드 API로 실제 디스크 생성

2. Pod 스케줄링 → external-attacher가 `ControllerPublishVolume` → 디스크를 Node에 attach

3. kubelet이 CSI Node에 `NodeStageVolume` + `NodePublishVolume` → 실제 마운트

주요 드라이버: AWS EBS, GCP PD, Azure Disk, Ceph RBD, Longhorn, Rook.

9. API Server 확장 — Aggregation Layer와 Admission Webhook

CRD vs Aggregated API Server

커스텀 리소스 만드는 방법 두 가지:

1. **CRD (CustomResourceDefinition)** — YAML로 스키마만 정의하면 자동으로 REST endpoint 생성. 99%의 경우 이걸로 충분

2. **Aggregation Layer** — 별도 API Server를 띄워 `/apis/metrics.k8s.io/v1beta1` 같은 경로를 담당. metrics-server, kube-aggregator가 이 방식

Admission Webhook — 요청 가로채기

- **Mutating Webhook** — 요청 바디를 수정 (예: Istio의 사이드카 주입)

- **Validating Webhook** — 요청을 거부 (예: OPA Gatekeeper 정책)

웹훅이 느리면 API Server가 멈춘다. `failurePolicy: Fail` 설정한 웹훅이 죽으면 해당 리소스에 대한 모든 요청이 실패한다 — 초보자의 흔한 장애 원인.

10. 실전 디버깅 — 5가지 증상과 근본 원인

증상 1: Pod가 Pending

확인 순서:

kubectl describe pod <name>

Events 섹션을 본다.

- `FailedScheduling: 0/10 nodes are available: 10 Insufficient cpu` → 리소스 부족

- `FailedScheduling: node(s) had taints` → taint/toleration 누락

- `FailedScheduling: pod has unbound immediate PersistentVolumeClaims` → PVC 프로비저닝 실패

증상 2: Pod는 Running인데 Ready 아님

`readinessProbe`가 실패 중. 자주 있는 실수:

- probe가 `/healthz`를 때리는데 앱은 `/health`에서 응답

- probe의 initialDelaySeconds가 너무 짧아 앱이 뜨기 전에 실패

- probe가 HTTPS인데 scheme 설정 누락

증상 3: CrashLoopBackOff

`kubectl logs <pod> --previous`로 이전 컨테이너 로그 본다. 전형적 원인:

- OOM → `kubectl describe` Events에 `OOMKilled`

- 설정 파일 경로 틀림

- 의존하는 Service/DB가 아직 안 떠서 startup이 실패 (initContainer로 해결)

증상 4: Service 연결 안 됨

kubectl get endpoints <service>

- 비어있음 → Service selector가 Pod label과 불일치

- 있는데도 안 됨 → NetworkPolicy가 차단 중일 확률 높음

증상 5: `kubectl exec`가 먹통

- API Server → kubelet의 exec 채널 문제. kubelet 로그 확인

- SNI/인증서 문제 — `kubectl exec --v=8`로 TLS 레벨까지 찍어본다

11. 스케일의 벽 — Kubernetes가 포기하는 지점

공식 최대치 (1.29 기준):

- Node 5,000

- Pod 150,000

- 컨테이너 300,000

- Node당 Pod 110

이 벽을 넘으려면:

1. **Cluster Federation / Multi-cluster** — 여러 클러스터를 묶어 운영 (Karmada, Cluster API)

2. **etcd 분할** — Event 전용 etcd 분리

3. **API Priority and Fairness** — API Server가 중요한 요청부터 처리

4. **Informer 최적화** — 컨트롤러가 `fieldSelector`로 관심 있는 객체만 구독

Uber, Airbnb 같은 곳은 클러스터당 5000 Node를 수십 개 운영하고, 그 위에 멀티 클러스터 컨트롤 플레인을 얹는다.

12. 보안 레이어 — RBAC, NetworkPolicy, Pod Security

RBAC — 누가 무엇을 할 수 있는가

kind: Role

rules:

- apiGroups: [""]

resources: ["pods"]

verbs: ["get", "list", "watch"]

kind: RoleBinding

subjects:

- kind: ServiceAccount

name: my-app

roleRef:

kind: Role

name: pod-reader

원칙: **Least Privilege**. `*` verb나 cluster-admin은 필요할 때만.

NetworkPolicy — 기본은 모두 허용

Kubernetes의 기본 정책은 "모든 Pod끼리 통신 허용"이다. 이건 보안상 위험하다. NetworkPolicy로 default-deny를 만들고 필요한 것만 열어준다.

kind: NetworkPolicy

spec:

podSelector: {} # 모든 Pod에 적용

policyTypes: [Ingress, Egress]

규칙 없음 → 모두 차단

NetworkPolicy는 CNI가 구현해야 한다. Flannel은 기본으로 지원 안 하고, Calico/Cilium이 필요하다.

Pod Security Standards — Admission으로 강제

1.25부터 PodSecurityPolicy가 제거되고 **Pod Security Admission**으로 대체:

- `privileged` — 아무거나 허용 (기본)

- `baseline` — 최소한의 보안 (root 허용 but privileged: false)

- `restricted` — 가장 엄격 (runAsNonRoot, readOnlyRootFilesystem 등)

네임스페이스에 라벨로 적용:

metadata:

labels:

pod-security.kubernetes.io/enforce: restricted

13. GitOps와 선언적 운영 — Kubernetes의 궁극 형태

`kubectl apply`를 사람이 치는 시대는 지났다. 운영 환경에서는:

1. **Git 저장소가 진실** — 모든 YAML이 git에 있음

2. **ArgoCD/Flux**가 git을 watch → 클러스터와 diff → reconcile

3. 개발자는 PR만 올림, 머지되면 자동 배포

이것이 가능한 이유는 **Kubernetes API가 멱등(idempotent)이고 선언적**이기 때문. 같은 YAML을 100번 apply해도 결과는 같다.

이 패턴을 확장하면 **Crossplane** 같은 도구로 클라우드 리소스(RDS, S3, VPC)까지 Kubernetes CRD로 선언 가능하다. "Kubernetes를 universal control plane으로" 쓰는 움직임이다.

14. 실무 교훈 12가지

1. **etcd 백업을 매일 하라** — `etcdctl snapshot save`. 없으면 클러스터 복구 불가능

2. **컨트롤 플레인 노드는 전용 Node로** — 워크로드와 섞으면 OOM으로 죽을 때 클러스터가 같이 죽는다

3. **`requests`와 `limits`를 항상 명시** — 없으면 스케줄러가 판단 불가, 노드 전체가 흔들림

4. **PodDisruptionBudget을 붙여라** — drain 중 서비스가 0개까지 떨어지는 걸 방지

5. **readinessProbe와 livenessProbe는 반드시 다르게 설계** — readiness는 트래픽 받을 준비, liveness는 살아있음 검사. 같으면 롤링 배포 중 재시작 지옥

6. **HPA의 scale down을 보수적으로** — 트래픽 스파이크 직후 스케일 다운하다 재포화

7. **namespace별 ResourceQuota** — 한 팀이 클러스터 전체를 먹는 것 방지

8. **CNI 바꾸기 = 클러스터 재생성** — 프로덕션에서 CNI 스왑은 사실상 불가

9. **Operator 만들기 전에 controller-runtime 학습** — kubebuilder로 시작하면 쉬움

10. **Prometheus + kube-state-metrics + cAdvisor** 삼위일체로 관측성 확보

11. **Secret은 etcd에 base64일 뿐** — SealedSecret, External Secrets Operator, Vault로 암호화

12. **Namespace delete는 영원할 수 있다** — finalizer 남은 리소스가 하나라도 있으면 멈춤. `kubectl patch ... -p '{"metadata":{"finalizers":[]}}'`로 수동 제거

다음 글 예고 — Service Mesh와 eBPF의 만남

Kubernetes가 "컨테이너 오케스트레이션의 운영체제"라면, 그 위에 올라타는 **네트워크 계층**은 최근 10년간 극적으로 변했다. 다음 글에서는:

- **Service Mesh**가 해결한 문제 (회로 차단, 분산 트레이싱, mTLS)와 Sidecar 모델의 비용

- Istio의 데이터 플레인(Envoy)과 컨트롤 플레인(istiod) 구조

- **Ambient Mesh** — Sidecar 없는 새 접근

- **eBPF** 기반 Service Mesh: Cilium의 서비스 메시 (Sidecar-less)

- **Envoy의 내부** — listener, filter chain, cluster, xDS API

- **gRPC-Go**의 클라이언트 사이드 로드 밸런싱과 Mesh의 관계

- WASM 필터 — Envoy 확장의 미래

지금도 `istioctl`을 쓰고 있지만 Envoy 설정을 직접 읽지 못하는 분, eBPF가 어떻게 kernel bypass로 지연을 줄이는지 궁금한 분이라면 다음 글에서 만나자.

> **"Kubernetes는 컨테이너를 배치하는 법을 알지만, 컨테이너끼리 어떻게 대화하는지는 별도 계층의 문제다."** Service Mesh가 그 답을 제시했고, eBPF는 그걸 다시 커널로 끌어내렸다.

현재 단락 (1/364)

"`kubectl apply -f deployment.yaml`를 쳤더니 30초 뒤 Pod가 떠 있더라." 이 한 줄 뒤에서는 **최소 6개의 컴포넌트가 순차적으로 reconcil...

작성 글자: 0원문 글자: 14,952작성 단락: 0/364