0. Contents

1. Overview

컨트롤 플레인 — 규칙을 쓰는 쪽API server모든 읽기·쓰기의 창구controller-manager노드마다 Pod 범위 배정schedulerPod 를 어느 노드에5Service 10.96.0.10 — 바뀌지 않는 주소3노드 A10.0.0.11podCIDR 10.244.1.0/24kubeletkube-proxyCNI 에이전트Pod10.244.1.5앱보조Pod10.244.1.6앱보조두드린다 (probe)노드 B10.0.0.12podCIDR 10.244.2.0/24kubeletkube-proxyCNI 에이전트Pod10.244.2.7앱보조Pod10.244.2.8앱보조두드린다 (probe)Pod IP 그대로 — NAT 없이21① 방 하나 IP 하나 ② 방끼리는 노드를 넘어 Pod IP 로 ③ 바뀌는 방 앞에 고정 주소 ⑤ 규칙은 위에서 쓰고 패킷은 노드 안 커널이 나른다
3장의 방·문·파이프가 노드 여럿으로 늘어난 모양 — ①②③ 과 그것을 만드는 쪽(⑤)이 Pod 네트워크의 이야기이고, ④ 는 그 위에 얹는 문이다

3장 정리: 서버 한 대 안의 이야기

  • 방(네임스페이스)을 만들었다
  • 문(인터페이스)을 달고, 파이프(veth)로 호스트의 브리지에 꽂았다
  • 호스트가 라우팅과 NAT 로 밖에 내보냈다

이 편: 서버가 여러 대일 때

  • 방은 Pod 라는 이름을 얻는다
  • 서버는 노드가 된다
  • 3장에서 손으로 친 명령은 CNI 플러그인과 kube-proxy 가 대신 친다

쿠버네티스 네트워킹의 문제 네 가지

  • ① 같은 Pod 안의 컨테이너끼리
  • ② 다른 노드의 Pod 끼리
  • ③ 계속 교체되는 Pod 를 고정 주소로
  • ④ 클러스터 밖에서 안으로

문제별 담당과 이 편의 범위

  • ①② 는 Pod 네트워크의 몫이고, ③④ 는 그 위에 얹는 Service·DNS·Ingress 의 몫이다
  • 여기서는 Pod 네트워크 — ①② 와 ③ 의 출발점 — 와 그것을 만드는 쪽(컨트롤 플레인, kubelet, CNI)을 본다

2. Four Problems — 방 하나, 방끼리, 바뀌는 방, 밖에서

네 문제와 푸는 쪽

  • 네 문제를 그림 하나씩으로 둔다
  • 뒤의 절은 전부 이 넷 중 어디에 속하는지로 읽힌다
  • ①② 는 Pod 네트워크가 푼다
    • 4.1 의 세 규칙은 ② 를 위해 Pod 네트워크가 지켜야 하는 요구 사항이다
  • ③④ 는 Pod 네트워크 위에 얹는 Service·DNS·Ingress 가 푼다
Pod — 방 하나10.244.1.5webLISTEN 0.0.0.0:8080sidecarcurl 127.0.0.1:8080localhosteth0 10.244.1.5 · lo 127.0.0.1문 하나, 포트 공간 하나 — 둘이 같이 쓴다1문제 ① — 같은 Pod 안의 컨테이너끼리 통한다같은 방IP 는 컨테이너가 아니라 Pod 에 하나localhost 로 통한다0.0.0.0 은 lo 로 온 것도 받는다포트는 못 겹친다둘 다 8080 을 열면 두 번째가 죽는다
① 같은 Pod 안 — 방을 통째로 나눠 쓴다. 3장에서 --network container:A 로 손으로 했던 것이 Pod 의 기본값이다
노드 APod10.244.1.5앱 :8080노드 BPod10.244.2.7앱 :8080출발 10.244.1.5 → 도착 10.244.2.7노드가 달라도 Pod IP 로 직접출발지도 그대로 — 받는 쪽이 보낸 Pod 를 안다2문제 ② — 다른 노드의 Pod 끼리 통한다Docker bridge 방식 — 호스트IP:포트로 가고, MASQUERADE 로 출발지가 호스트 IP 가 된다. 쿠버네티스는 이것을 Pod 사이에 금지한다
② 다른 노드의 Pod — 호스트 포트도 NAT 도 없이 Pod IP 로. 이 화살표를 실제로 만드는 것이 CNI 다 (§7)
클라이언트DB_HOST=go-webService go-web10.96.0.10 — 안 바뀜이름으로교체 전10.244.1.510.244.2.7교체 후10.244.1.910.244.2.3Deployment 가 이미지를 바꾸면 Pod 를 지우고 새로 만든다 — IP 도 새것Service 는 레이블 app=web 으로 지금 살아 있는 Pod 를 매번 찾는다3문제 ③ — 계속 교체되는 Pod 를 바뀌지 않는 주소로 찾는다
③ 교체되는 Pod — 바뀌는 쪽(Pod IP)과 바뀌지 않는 쪽(Service·DNS 이름)을 떼어 놓는다. StatefulSet 도 고정되는 것은 이름이지 IP 가 아니다
밖의 사용자브라우저 · 외부 시스템공개 경로LoadBalancer · Ingress · GatewayService10.96.0.10Pod 들10.244.x.x4문제 ④ — 클러스터 밖의 사용자가 안의 앱에 닿는다클러스터 밖에서 들어오는 길 — Service 가 있어야 열 수 있고, 공개 경로는 그 위에 얹는다
④ 밖에서 안으로 — ①②③ 이 만든 Pod 네트워크와 Service 위에 문을 하나 더 다는 일이다

2.1 사설 IP 와 라우팅 표

Docker bridge 의 한계: 호스트 한 대 기준

  • Docker 의 기본 bridge 는 호스트 한 대 기준이다
    • 서버 A 의 docker0 과 서버 B 의 docker0 은 서로 모른다
    • 둘 다 172.17.0.0/16 을 써서 주소까지 겹친다
  • 그래서 다른 서버의 컨테이너에 가려면 03-2 의 5.2 처럼 호스트 포트를 열고 NAT 로 나가야 했다
  • 쿠버네티스는 이것을 앱마다 손으로 하지 말고, 클러스터가 처음부터 깔아 두라고 요구한다
지금 — 길이 없다서버 A10.0.0.11컨테이너 172.17.1.2라우팅 표172.17.0.0/16 dev docker0default via 10.0.0.1서버 B10.0.0.12컨테이너 172.17.2.3라우팅 표172.17.0.0/16 dev docker0라우터 10.0.0.110.0.0.0/24 만 안다✗A 는 172.17.2.3 을 자기 docker0 의 옆집으로 안다 — docker0 에서 끝난다라우터도 B 도 172.17.x 를 어디로 보낼지 모른다사설 주소라서가 아니라 세 표에 줄이 없어서다길을 적으면 — 통한다서버 A10.0.0.11컨테이너 172.17.1.2라우팅 표172.17.2.0/24 via 10.0.0.12default via 10.0.0.1서버 B10.0.0.12컨테이너 172.17.2.3라우팅 표172.17.1.0/24 via 10.0.0.11라우터 10.0.0.1172.17.2.0/24 → 10.0.0.12172.17.1.0/24 → 10.0.0.11✓A 의 표에 '172.17.2.0/24 는 B 로', B 의 표에 'A 의 대역은 A 로'두 노드가 다른 대역을 쓰고, 가는 줄과 오는 줄이 모두 있다이 줄들을 누가 어떻게 적느냐가 CNI 의 차이다줄을 적는 방법 셋 — 라우터에 직접 알린다(BGP) · 패킷을 노드 IP 로 감싸 라우터가 몰라도 되게 한다(터널) · 라우터가 이미 아는 VPC 주소를 준다(VPC CNI)
Docker 혼자서는 왼쪽이다 — 두 노드가 같은 172.17.0.0/16 을 쓰고 아무도 줄을 적어 주지 않는다. 오른쪽을 자동으로 만드는 것이 Pod 네트워크다

닿지 못하는 이유: 라우팅 표에 없는 주소

  • 서버 A 의 컨테이너가 서버 B 의 컨테이너 172.17.2.3 에 닿지 못하는 이유는 172.17 이 사설 주소라서가 아니다
  • 서버 A 의 라우팅 표에 172.17.2.3 을 어디로 보낼지 적혀 있지 않다
  • 중간 라우터도 그 주소를 모른다
  • 설령 도착해도 답장이 서버 A 로 돌아올 줄이 서버 B 의 표에 없다

노드 IP 와의 차이

  • 노드 IP 10.0.0.x 도 사설 주소인데 라우터는 거기에 닿는다
  • 라우터 자신의 다리가 10.0.0.1 로 그 대역에 꽂혀 있어 이미 안다
  • 172.17.x 는 각 서버 안의 Docker 가 혼자 정한 번호라 라우터도 다른 서버도 들은 적이 없다
  • 두 서버가 같은 번호를 쓰니 들어도 구분하지 못한다
  • 사설이냐 아니냐가 아니라 라우터의 표에 있느냐가 차이다

필요한 경로 두 줄

  • 가는 길 — 서버 A 와 중간 라우터의 표에 172.17.2.0/24 → 서버 B
  • 오는 길 — 서버 B 와 라우터의 표에 172.17.1.0/24 → 서버 A

경로를 만드는 방식 세 가지

  • 이 두 줄을 어떻게 만드느냐가 CNI 구현의 차이다
  • 진짜 라우터에 경로를 알려준다 (BGP)
  • 패킷을 노드 IP 로 감싸서 라우터가 몰라도 되게 한다 (터널)
  • 처음부터 라우터가 아는 VPC 주소를 Pod 에 준다 (VPC CNI)
  • 7절의 비교표가 이 셋을 나눈다

3. Pod — 방 하나에 IP 하나

3.1 같은 방, 같은 포트 공간

Pod: 네임스페이스 하나에 컨테이너 여럿

  • Pod 는 네트워크 네임스페이스 하나에 컨테이너를 여럿 넣은 것이다
  • 03-2 에서 --network container:web 으로 손으로 했던 구성이 Pod 의 기본값이다
  • IP 는 컨테이너가 아니라 Pod 에 하나 붙는다
  • eth0 과 lo 도 하나, 포트 공간도 하나다
  • 방 하나에 문이 하나인 것이지 네임스페이스의 규칙이 그런 것은 아니다
    • 호스트 루트 방에는 ens3, docker0, lo 가 문 셋이고 IP 도 셋이다
  • IP 는 방이 아니라 문에 붙는다
Pod 110.244.1.5A webLISTEN 0.0.0.0:8080B sidecar8080 도 열면 ✗eth0 10.244.1.5 · lo 127.0.0.1Pod 210.244.1.6CLISTEN 0.0.0.0:8080eth0 10.244.1.6 · lo 127.0.0.112345① B → 127.0.0.1:8080 ✓같은 방. B 의 localhost 는 Pod 1 의 lo② C → 127.0.0.1:8080 ✗C 의 localhost 는 Pod 2 의 lo. A 는 거기 없다③ C → 10.244.1.5:8080 ✓다른 Pod 는 Pod IP 로 간다④ A 와 B 가 둘 다 8080 ✗같은 방, 같은 (IP, 포트). 두 번째가 죽는다⑤ A 와 C 가 둘 다 8080 ✓다른 방. 10.244.1.5:8080 과 10.244.1.6:8080 은 다른 자리127.0.0.1 은 늘 부르는 쪽의 방이고, 포트 충돌의 단위는 방 하나 안의 (IP, 포트) 쌍이다
문서 2.1 의 네 문장이 다섯 경우로 — 0.0.0.0 은 '이 방에 도착한 8080 은 도착 IP 불문 받는다' 이고, 방이 다르면 만나지 못한다

0.0.0.0 과 127.0.0.1 의 뜻

  • 0.0.0.0:8080 에 bind 한 소켓은 “이 방에 도착한 8080 패킷은 도착 IP 를 가리지 않고 받는다” 는 뜻이다
    • 같은 Pod 의 사이드카가 127.0.0.1 로 불러도, 다른 Pod 가 10.244.1.5 로 불러도 받는다
  • 127.0.0.1 은 고정된 장소가 아니라 “지금 패킷을 보내는 프로세스가 있는 방의 lo” 다
  • 커널은 자기 방의 IP 로 가는 패킷을 전부 lo 로 돌린다(local 라우팅 테이블)
    • Pod 안에서 curl 10.244.1.5 를 쳐도 eth0 으로 나가지 않는다

포트 충돌: Pod 단위

  • 같은 Pod 안에서 두 컨테이너가 같은 (IP, 포트) 에 listen 하면 두 번째가 address already in use 로 죽는다 → Pod 는 CrashLoopBackOff 가 된다
  • 0.0.0.0:8080 과 127.0.0.1:8080 도 리눅스는 겹치는 것으로 본다
  • 사이드카를 붙일 때 포트 계획은 Pod 단위로 한다

3.2 공유하는 것과 아닌 것

공유 범위: 03-1 의 4.3 에서 본 구분

  • 네트워크를 공유한다고 전부 같아지는 것은 아니다
  • 나눠 쓰는 것 — net, IPC, UTS
  • 따로 갖는 것 — mnt, pid, cgroup
  • 그래서 웹 서버와 로그 수집기를 컨테이너 둘로 나누면 이미지와 CPU·메모리 제한을 따로 두면서도 localhost 로 통한다
  • 컨테이너의 limits 와 쿠버네티스 네임스페이스 단위의 ResourceQuota 는 다른 층이다
    • “네임스페이스” 라는 말이 리눅스 커널의 격리 단위와 API 객체의 논리 구획, 두 층에서 쓰인다

예외 두 가지

  • hostNetwork: true — Pod 가 자기 방 없이 호스트 방에 들어간다
    • Docker 의 host 모드와 같다
    • Pod IP 가 노드 IP 가 되고 호스트와 포트 공간을 공유한다
    • kube-proxy 와 CNI 에이전트처럼 Pod 네트워크 자체를 만드는 것들이 이렇게 뜬다
  • dual-stack — eth0 하나에 IPv4 와 IPv6 주소가 하나씩 붙는다

3.3 containerPort 의 역할

apiVersion: v1
kind: Pod
metadata:
  name: go-web
  labels:
    app: web                 # Service 와 NetworkPolicy 가 이 꼬리표로 Pod 를 고른다
spec:
  containers:
    - name: web
      image: go-web:cn4
      ports:
        - name: http         # 아래 probe 와 Service 가 숫자 대신 이 이름으로 가리킨다
          containerPort: 8080
          protocol: TCP

containerPort: EXPOSE 와 같은 성격

  • containerPort 는 Dockerfile 의 EXPOSE 와 같은 성격이다
  • 적어도 프로세스가 bind 하지 않으면 아무것도 듣지 않는다
  • 적어도 밖에서 자동으로 열리지 않는다
  • 안 적어도 프로세스가 연 포트는 Pod IP 로 닿는다
  • 포트를 여는 것은 프로세스의 bind, 밖에서 닿게 하는 것은 Service, 막는 것은 NetworkPolicy 다
    • 이 필드는 셋 중 어느 것도 아니다
  • 적는 이유는 사람이 읽기 위해서와 port: http 처럼 이름으로 참조하기 위해서다

3.4 Pod 의 교체와 IP 의 수명

Pod 교체와 새 IP

  • Pod 를 직접 만들면 죽어도 아무도 다시 띄우지 않는다
    • 그래서 Deployment 가 ReplicaSet 을 통해 “이 템플릿으로 3개 유지” 를 맡는다
  • 이미지를 바꾸면 기존 Pod 를 고치는 것이 아니라 새 Pod 를 띄우고 옛 Pod 를 지운다
    • 지운 Pod 의 IP 는 반납되고 새 Pod 는 새 IP 를 받는다
  • 2절의 그림 ③이 그 장면이다
    • 바뀌는 쪽과 바뀌지 않는 쪽을 떼어 놓는 것이 Service 와 DNS 다
컨테이너 재시작 — liveness 실패, 프로세스 crashPod 방 — 그대로10.244.1.5pause잠만 잔다앱 ✗죽음앱 (새)같은 방에방이 남는다pause 프로세스가 네임스페이스를 붙들고 있다veth 도 IP 도 그대로CNI 는 다시 불리지 않는다. kubelet 이 앱만 다시 띄운다Pod 삭제 — 롤아웃, 노드 장애, kubectl delete옛 방 ✗10.244.1.5 반납CNI DEL새 방10.244.1.9pause앱Pod 는 옮겨가지 않는다죽고, 컨트롤러가 다른 UID 로 새로 만든다IP 는 이때만 바뀐다emptyDir 도 이때 사라진다. 이름(StatefulSet)만 되돌아온다IP 가 바뀌는 순간은 '컨테이너가 죽을 때' 가 아니라 'Pod 객체가 새로 만들어질 때' 다
CNI 를 앱이 아니라 pause 의 방에 붙이는 이유 — 앱이 몇 번 죽어도 네트워크는 남는다

IP 가 유지되는 경계: Pod 삭제와 컨테이너 재시작

  • Pod 는 이동하지 않는다
    • “다른 노드로 옮겨갔다” 는 죽고, 컨트롤러가 다른 UID 로 새로 만든 것이다
  • 컨테이너 재시작은 같은 방 안에서 프로세스만 다시 띄우는 것이라 IP 가 그대로다
    • pause 라는 아무 일도 안 하는 작은 프로세스가 방을 붙들고 있다
  • 네임스페이스는 안에 프로세스가 하나라도 있어야 살아 있는 커널 객체다
    • 앱만 있는 방은 앱이 죽는 순간 사라진다
  • emptyDir 이 컨테이너 재시작은 견디고 Pod 삭제에는 사라지는 것도 같은 경계다

StatefulSet 의 “안정적 네트워크 정체성” 범위

  • 고정되는 것 — Pod 이름 web-0, 호스트명, DNS 이름, 붙는 디스크
  • 고정되지 않는 것 — 그 DNS 이름이 가리키는 IP
  • Pod spec 에 “이 IP 를 달라” 는 표준 필드는 없다
    • 고정 주소는 CNI 구현의 별도 기능이다

4. Pod Network — 세 규칙과 경계

4.1 세 규칙

1규칙 1모든 Pod 는 다른 모든 Pod 에Pod IP 로 직접 닿는다노드 A10.244.1.5노드 B10.244.2.7→ 10.244.2.7✓쿠버네티스 — 상대 Pod IP 를 적으면 간다Docker — 호스트IP:포트로 우회2규칙 2그 사이에 NAT 가 없다출발지가 바뀌지 않는다출발 10.244.1.5노드를 건너도받는 쪽10.244.2.7출발지 칸은 10.244.1.5 그대로✓쿠버네티스 — 출발지가 Pod IP 그대로Docker — 출발지가 호스트 IP 로 바뀜3규칙 3노드의 에이전트는그 노드의 Pod 에 닿는다노드 A — 호스트 방kubeletGET 10.244.1.5:8080/healthzPod10.244.1.5✓쿠버네티스 — kubelet 이 Pod IP 로 두드림Docker — Docker 도 같다세 줄은 '무엇' 만 정한다 — 브리지로 할지, 라우팅할지, 터널로 감쌀지는 CNI 플러그인이 정하고, 그래서 플러그인이 여럿이다
노드 한 대 안에서는 Docker bridge 도 셋 다 만족한다 — 쿠버네티스는 노드를 넘어도 깨지지 말라고 요구하는 것이다

세 규칙의 성격과 만드는 쪽

  • 세 규칙은 2절의 문제 ② 를 풀기 위해 Pod 네트워크가 지켜야 하는 요구 사항이다
  • 만드는 것은 CNI 플러그인이고, 어느 것을 깔지는 구성자의 몫이다
  • kubeadm 클러스터는 CNI 를 깔기 전까지 노드가 NotReady 다

Docker 와의 차이: MASQUERADE 조건

  • Docker 가 서버 밖에서 깨지는 자리는 한 곳이다 — MASQUERADE 규칙의 ! -o docker0
    • 브리지 밖으로 나가는 패킷은 전부 출발지가 바뀐다
  • 쿠버네티스가 바꾸는 것은 셋이다
    • 노드마다 겹치지 않는 서브넷을 준다
    • 호스트 라우팅에 10.244.2.0/24 → 노드 B 를 넣는다
    • MASQUERADE 조건을 ! -d 10.244.0.0/16 으로 바꾼다 — Pod 대역끼리는 NAT 하지 않는다
Pod → Pod출발 10.244.1.5도착 10.244.2.7그대로바뀌는 칸: 없음2.2 의 규칙 — 받는 쪽이 보낸 Pod 를 그대로 본다Pod → Service출발 10.244.1.5도착 10.96.0.10:80PREROUTING · DNAT도착 → 10.244.2.7:8080바뀌는 칸: 도착지만kube-proxy 규칙. 라우팅 결정 앞에서 바꿔야 올바른 문으로 나간다Pod → 인터넷출발 10.244.1.5도착 8.8.8.8POSTROUTING · SNAT출발 → 10.0.0.11바뀌는 칸: 출발지만노드를 나가기 직전. 10.244.x 는 인터넷에서 돌아올 수 없다'NAT 없이' 는 Pod 가 Pod 를 Pod IP 로 부를 때의 규칙이다 — Service 는 도착지를, 인터넷행은 출발지를 바꾼다
책의 '모든 컨테이너는 NAT 없이' 는 범위를 빠뜨렸다 — Pod 네트워크(CNI)와 Service 구현(kube-proxy)은 다른 층이다

책의 서술과 층의 구분

  • 책의 “모든 컨테이너는 NAT 없이” 는 범위를 빠뜨린 서술이다
  • Pod 네트워크 층(CNI)과 Service 층(kube-proxy)은 별개다
    • Flannel + kube-proxy 도, Cilium 이 둘 다 맡는 구성도 된다

주의: 정책 기능이 없는 CNI

  • 정책 기능이 없는 CNI 에서는 NetworkPolicy 를 만들어도 아무 일이 안 일어난다
    • kubectl apply 는 성공하고 에러도 없다
  • Flannel 단독 설치와 kind 의 기본 kindnet 이 그렇다
    • kind 에서 kindnet 을 끄고 Cilium 을 까는 이유다

4.2 왕복 경로와 연결 방향

“A 가 B 에 닿는다” 의 세 층

  • 가는 길 — A 의 패킷이 B 에 도착하는가
  • 오는 길 — B 의 답장이 A 에 도착하는가
  • 시작 방향 — 누가 먼저 말을 걸 수 있는가

TCP 와 conntrack

  • TCP 는 SYN·SYN-ACK·ACK 가 오가야 연결이 된다 → 가는 길만 있으면 “ping 은 되는데 curl 은 안 되는” 상황이 생긴다
  • 세 번째 층은 conntrack 이 맡는다
  • 방화벽이 “A 가 시작한 연결의 답장은 통과, B 가 새로 시작하는 것은 차단” 을 구분할 수 있는 것은 그 장부 덕이다
  • NetworkPolicy 에서 egress 만 열어도 응답을 따로 열 필요가 없는 이유가 여기 있다

책의 오류: “Pod 에는 MAC 이 없다”

  • 책의 “Pod 에는 MAC 이 없어 L2 통신이 불가능하다” 는 틀렸다
  • Pod 의 eth0 은 veth 라 MAC 이 있다
  • 쿠버네티스가 Pod 간 연결을 L3 로 요구한다는 것과 인터페이스에 MAC 이 있고 없고는 다른 이야기다
  • 브리지로 스위칭할지, 라우팅할지, 캡슐화할지는 구현의 선택이다
    • 7.4 의 그림이 그 선택이다

4.3 경계 — isolated · flat · island

클러스터 밖과의 관계를 가르는 기준

  • 클러스터 안의 Pod 끼리가 아니라 클러스터 밖과의 관계는 질문 하나로 갈린다
    • 밖의 호스트가 Pod IP 로 직접 패킷을 보낼 수 있는가
격리 isolated밖 호스트클러스터Pod10.244.1.5노드 10.0.0.11✗길이 없다필요한 통신만 따로 뚫는다평탄 flat밖 호스트클러스터Pod10.244.1.5노드 10.0.0.11✓→ 10.244.1.5 직접라우터가 10.244.x 를 안다NAT 없이 진짜 IP 로 서로 보인다섬 island밖 호스트클러스터Pod10.244.1.5노드 10.0.0.11→ 노드 IP : LB 포트SNAT / LB나갈 땐 SNAT, 들어올 땐 LBDocker bridge 를 클러스터 크기로질문은 하나 — 밖의 호스트가 Pod IP 로 직접 패킷을 보낼 수 있나. API 의 모드 이름이 아니라 CNI 설정과 회사 네트워크의 결과다
flat 이어도 Pod IP 는 교체되면 바뀐다 — flat 의 가치는 고정이 아니라 '찾은 IP 를 변환 없이 그대로 쓸 수 있다' 는 데 있다

island 와 flat

  • Docker bridge 가 island 다
    • 대부분의 쿠버네티스 클러스터도 island 다
  • island 의 대가 — 나갈 때 노드 IP 로 변장하니 외부 DB 로그에는 노드 A 의 Pod 50개가 전부 같은 IP 로 찍힌다
    • 클러스터 안에서 금지한 NAT 를 경계에서는 쓰기 때문이다
  • flat 의 대표 — EKS 의 VPC CNI
    • Pod 가 VPC 주소를 받으니 VPC 안의 EC2 가 Pod 에 직접 닿는다
    • ALB 가 Pod IP 를 타깃으로 직접 등록한다
    • 보안 그룹을 Pod 단위로 걸 수 있다
  • flat 이어도 Pod IP 는 교체되면 바뀐다
    • “지금 IP 가 뭔지” 는 Service 와 EndpointSlice 가 늘 알고 있다
    • flat 의 가치는 그 IP 를 찾은 다음 변환 없이 그대로 쓸 수 있다는 데 있다

경계 분류와 overlay·native routing 의 차이

  • 이 분류와 overlay·native routing 은 다른 질문에 답한다
    • 앞은 클러스터 경계의 도달 가능성
    • 뒤는 노드 사이에 패킷을 어떻게 나르느냐
  • overlay + island 가 가장 흔하지만 native routing + island(라우터는 아는데 방화벽으로 막음)도 있다

방향 용어: ingress 와 egress

  • 방향 용어도 책이 거꾸로 적었다
  • ingress 는 기준 대상에 들어오는 것, egress 는 나가는 것
  • Pod A → Pod B 패킷 하나가 A 에게는 egress 이고 B 에게는 ingress 다
  • 대문자 Ingress 리소스는 이 방향 용어와 다른 API 객체다

4.4 주소 공간의 계획

Pod 대역이 겹치면 안 되는 상대 네 가지

  • 노드 네트워크
  • 회사 사설망·VPN
  • 다른 클러스터의 Pod 대역
  • Service 대역

클러스터 전체에서 유일한 Pod IP

  • Docker 에서는 호스트마다 172.17.0.0/16 이 겹쳐도 NAT 로 나가니 상관없었다
  • 쿠버네티스에서는 Pod IP 가 노드를 넘어 그대로 보이므로 클러스터 전체에서 유일해야 한다
  • 10.244.0.0/16 이 자주 보이는 것은 Flannel 의 기본값이라서이지 표준이 아니다
  • 격리된 클러스터 둘이 같은 대역을 써도 당장은 돌아가지만 나중에 잇는 순간 드러난다

노드당 Pod 수의 상한

  • /24 를 받았다고 Pod 256개를 띄울 수는 없다
  • 네트워크 주소와 브로드캐스트, 게이트웨이(브리지 IP), 플러그인 예약분이 빠진다
  • kubelet 의 maxPods 기본값이 110 이다
    • 둘 중 작은 쪽이 상한이다
  • 노드당 /24, 클러스터 /16 이면 노드 256대에 약 28,000 Pod 다
  • 모자라면 떨어진 대역을 풀로 더 붙인다
    • Pod CIDR 이 반드시 하나의 연속된 대역이어야 하는 것은 아니다

5. Control Plane — 규칙을 쓰는 쪽과 패킷을 나르는 쪽

5.1 제어와 데이터

제어 — 규칙을 쓴다. Pod 가 생기거나 죽을 때, 가끔API server상태의 창구controller-managerEndpointSlice · 노드 범위kube-proxyService → iptablesCNI 에이전트veth · 라우팅 · eBPF데이터 — 규칙대로 나른다. 패킷마다, 초당 수만 번. 전부 커널iptables / nftablesDNAT · 차단라우팅 테이블어느 문으로브리지 · veth방 사이eBPF 프로그램Cilium 의 길쓴다 — 쿠버네티스 프로세스는 여기서 끝API server 가 죽어도 패킷은 흐른다 — 이미 쓰인 규칙은 커널에 남고, 바뀌는 것이 반영되지 않을 뿐이다
3장에서 손으로 친 ip link·iptables -A 를 4장의 제어 층이 대신 친다 — 'proxy' 라는 이름과 달리 패킷은 kube-proxy 를 지나지 않는다

컨트롤 플레인 넷의 역할

  • API server — 모든 읽기·쓰기의 창구
  • etcd — 그 뒤의 장부
  • scheduler — 빈 nodeName 을 채우는 배치 담당
  • kube-controller-manager — “3개라고 적혔는데 2개네” 를 메우는 순찰 담당

제어와 데이터의 구분

  • 이 넷 중 누구도 패킷을 나르지 않는다
  • 어떤 Pod 가 Service 의 백엔드인지 보고 규칙을 바꾸는 것은 제어다
  • 그 규칙대로 패킷을 보내거나 주소를 바꾸는 것은 데이터다
  • 3장에서 본 veth·브리지·라우팅 테이블·iptables 규칙이 전부 데이터 층이다
  • 4장에서 새로 나오는 kube-proxy·CNI 에이전트·EndpointSlice 가 그것을 자동으로 설정하는 제어 층이다

5.2 두 단계 IPAM

상위 구성: Node IPAM

  • 4.4 에서 “노드끼리 안 겹치게 하는 것은 상위 구성의 책임” 이라고 했다
  • 그 상위 구성이 kube-controller-manager 안의 Node IPAM 이다
1단계 — 한 번, 노드가 들어올 때. 관리사무소가 동마다 범위를 뗀다kube-controller-manager--cluster-cidr 10.244.0.0/16 --node-cidr-mask-size 24노드 ApodCIDR 10.244.0.0/24노드 BpodCIDR 10.244.1.0/24노드 CpodCIDR 10.244.2.0/24Node.spec.podCIDR 에 적어 두고 끝Pod 가 뜨는 순간에는관여하지 않는다2단계 — 매번, Pod 가 뜰 때. 노드 B 의 경비실이 자기 범위에서 낱개를 꺼낸다CNI IPAM (host-local 등)장부 /var/lib/cni/networks/ 10.244.1.0/24 에서 하나Pod10.244.1.5Pod10.244.1.6Pod10.244.1.7노드 안에서 끝난다빠르고, 컨트롤 플레인이 바빠도 Pod 는 뜬다노드 사이 중복은 1단계의 책임Cilium cluster-pool 과 VPC CNI 는 1단계를 자기 operator·ENI 로 대신한다 — 그때 podCIDR 과 --cluster-cidr 는 읽히지 않는다
'기본값이 뭐냐' 에는 '어떤 기본값이냐' 를 되물어야 한다 — 실행 파일은 --allocate-node-cidrs=false 인데 kubeadm 과 kind 는 true 로 넣어 준다

1단계의 기록 위치: Node 객체의 podCIDR

kubectl get node node-b -o yamlkind: Nodemetadata:name: node-bspec:podCIDR: 10.244.1.0/24podCIDRs: [10.244.1.0/24]status:addresses:- type: InternalIPaddress: 10.0.0.12123① name — 노드 이름kubelet 이 등록할 때 적는다. Pod 의 spec.nodeName 이 가리키는 값② podCIDR — 이 노드의 Pod 들이 쓸 범위쓰는 쪽 kube-controller-manager 의 Node IPAM — 노드 등록 때 /24 하나를 잘라 적음읽는 쪽 이 노드의 CNI — 브리지에 10.244.1.1 을 붙이고 host-local 범위로 삼음읽는 쪽 다른 노드의 CNI — '10.244.1.0/24 는 10.0.0.12 로' 라우팅 줄을 자기 노드에 넣음Cilium cluster-pool · VPC CNI 는 이 필드를 쓰지 않는다 (CiliumNode CRD · ENI)③ InternalIP — 노드 자신의 주소라우터가 아는 그 주소. Pod 범위(②)와 다른 대역이고, ②로 가는 길의 목적지가 된다클러스터 전체로 보면 — Pod IP 만 보고 어느 노드인지 안다node-a10.0.0.1110.244.0.0/24node-b10.0.0.1210.244.1.0/24node-c10.0.0.1310.244.2.0/2410.244.1.x 이면 node-b 다 — 다른 노드들은 이 표를 읽어 '그 범위는 그 노드로' 를 라우팅에 적는다
podCIDR 은 관리사무소가 동 현관에 붙인 '이 동은 101~199 번' 안내판이다 — 자기 동의 경비실도, 다른 동의 배달부도 이 안내판을 읽는다
옵션뜻
--allocate-node-cidrs노드에 Pod 블록을 나눠 줄지. 실행 파일 기본값 false, kubeadm·kind 는 true 로 넣어 준다
--cluster-cidr쪼갤 전체 범위
--node-cidr-mask-size노드 블록 크기. 기본 IPv4 /24, IPv6 /64
--configure-cloud-routes나눈 블록을 클라우드 라우팅 테이블에도 적을지 — 켜면 터널 없이 flat 이 된다
--service-cluster-ip-rangeService 주소 범위. API server 설정과 맞춰야 하고, Pod IPAM 과는 다른 기능이다

실제 옵션 확인

  • 내 클러스터에서 실제로 어떤 옵션이 붙어 실행됐는지는 kube-system 의 kube-controller-manager Pod 명세에 있다
kubectl -n kube-system get pod -l component=kube-controller-manager -o yaml | grep -- '--'
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'

설정을 읽는 쪽이 다른 경우

  • --cluster-cidr 를 바꿨는데 Pod IP 가 그대로라면 설정이 안 먹은 것이 아니라 그 설정을 읽는 쪽이 아닌 것이다
  • Cilium 의 cluster-pool 모드와 VPC CNI 는 1단계를 자기 operator 와 ENI 로 대신하고 podCIDR 필드를 무시한다

5.3 Pod 생성 순서 — kubelet, CRI, CNI, pause

1kubectl applyPod 객체만 생김. nodeName 비어 있음2schedulerrequests·affinity·taint 로 노드 고름 → nodeName 기록3kubelet'내 노드다' → CRI 로 containerd 에 sandbox 요청4containerd방(netns) 만들고 pause 띄움5CNI ADDveth · IPAM 으로 IP · 라우팅. CNI_NETNS = pause 의 방6runc앱 컨테이너를 그 방에 넣고 실행7kubelet → APIRunning, podIP 10.244.1.7 보고노드 안에서 — 컨트롤 플레인은 ②까지⑤가 ⑥보다 앞 —네트워크는 앱이 아니라 방에 붙는다CNI 를 부르는 것은 kubelet 이 아니라 런타임이다 — CNI 설정 문제는 containerd 로그에 먼저 나타난다
3장 03-2 의 '런타임이 밖의 실행 파일을 부른다' 가 Pod 생성 흐름 안에서 — 방 먼저, 네트워크 다음, 앱은 마지막

배치와 노드 안의 순서

  • scheduler 는 “지금 CPU 사용률 30% 인 노드” 가 아니라 “예약된 requests 합이 아직 여유 있는 노드” 를 고른다
    • 방이 비어 있어도 예약이 차 있으면 못 들어가는 호텔과 같다
  • 그 뒤는 전부 노드 안이다
    • kubelet 은 CRI 로 containerd 에 sandbox 를 요청한다
  • containerd 가 방을 만들어 pause 를 띄운 뒤 CNI 를 부른다
    • 그 다음에야 runc 가 앱 컨테이너를 그 방에 넣는다
  • CNI 가 앱보다 앞에 있는 것이 3.4 의 “앱이 죽어도 네트워크는 남는다” 의 이유다

겹쳐 있는 약속 세 가지

  • 여기에 약속이 셋 겹쳐 있고 서로 섞이지 않는다
  • CRI — kubelet 이 런타임(containerd, CRI-O)을 부르는 약속
  • OCI — 런타임이 저수준 실행기(runc, crun)를 부르는 약속
  • CNI — 런타임이 네트워크 플러그인(Flannel, Calico, Cilium)을 부르는 약속
  • 셋은 독립적으로 고른다
    • Cilium 은 CRI 구현체가 아니고 containerd 는 네트워크를 모른다
kubectl get pods -A -w --field-selector spec.nodeName=node-1   # 한 노드에 배정된 Pod 만, -w 로 변화 추적

5.4 구성요소 사이의 통신

API serverHTTPS :6443kubectlkubeletkube-proxyschedulercontroller-manageretcdgRPC :2379 · 복제 :2380API server 만kubeletHTTPS :10250logs · exec · port-forward노드 안 — 네트워크 포트가 아니라 소켓과 실행kubeletCRI · unix socketcontainerdexec · stdin JSON/opt/cni/bin/…ADD · DEL · CHECK …거의 모든 화살표가 API server 로 모인다 — etcd 는 그 뒤에 숨고, Pod 가 etcd 에 직접 닿는 구조는 없다
문서 4.4 의 표를 방향으로 읽으면 — 중앙에서 노드로 가는 화살표는 kubectl logs·exec 뿐이고, 노드 안의 둘은 로컬 호출이다

포트와 접근 경로

  • 표의 포트는 대표값이다
    • API server 앞의 로드밸런서가 443 을 쓸 수 있다
  • Pod 가 클러스터 상태를 알고 싶으면 API server 에 물어야 RBAC 을 거친다
  • etcd 에는 Secret 이 기본값으로 평문으로 들어 있으니 Pod 가 거기 닿으면 안 된다

5.5 kube-proxy 의 역할과 모드

kube-proxy 의 역할: 규칙만 쓴다

  • kube-proxy 는 Service 와 EndpointSlice 를 감시하고 노드의 전달 규칙을 쓴다
  • 이름에 proxy 가 들어 있지만 패킷은 kube-proxy 프로세스를 지나지 않는다
    • 아주 옛날 userspace 모드가 그랬고 이름이 거기서 왔을 뿐이다
  • 실제 DNAT 는 커널의 netfilter 가 한다
  • kube-proxy 가 죽으면 규칙 갱신이 멈출 뿐 기존 규칙으로 계속 동작한다
모드상태비고
userspace삭제됨패킷이 프로세스를 지나던 원조
iptables계속 사용. 많은 배포판의 기본Service 수천 개면 규칙이 길어져 느려진다
IPVSv1.35 부터 deprecated책이 “새 권장” 이라고 쓴 것. 지금은 아니다
nftablesv1.33 부터 Stable, 커널 5.13+iptables 의 후계. 규칙이 많아도 빠르다

노드마다 하나씩 도는 이유

  • DaemonSet 이라 모든 노드에 하나씩 돌고, 각자 자기 노드의 iptables 에 같은 규칙을 쓴다
  • 노드마다 필요한 이유 — DNAT 는 보내는 Pod 가 있는 노드에서 일어난다
    • Service IP 는 어느 인터페이스에도 없는 가상 주소라 자기 노드를 떠나기 전에 진짜 Pod IP 로 바뀌어야 한다
  • 규칙의 실제 모양과 PREROUTING·OUTPUT 두 입구는 노드의 nat 테이블에 그대로 적혀 있다
  • Cilium 은 이 일을 eBPF 로 대신할 수 있다(kubeProxyReplacement)

6. Probes — kubelet 의 상태 검사

6.1 Running 과 Ready 의 차이

상태를 적는 세 층

  • 문은 열었는데 주방이 준비 중이면 손님을 들이면 안 된다
  • 쿠버네티스는 이 둘을 세 층으로 적는다
    • Pod 전체 단계 status.phase — Pending · Running · Succeeded · Failed · Unknown
    • Pod 조건 status.conditions — PodScheduled · ContainersReady · Ready
    • 컨테이너별 상태 status.containerStatuses — Waiting · Running · Terminated

네트워크에서 중요한 층: Ready 조건

  • 네트워크에서 중요한 것은 두 번째 층의 Ready 다
  • Service 는 Ready 가 True 인 Pod 에만 트래픽을 보낸다

STATUS 열: 세 층을 섞은 요약

  • kubectl get pods 의 STATUS 열은 세 층을 섞어 사람이 보기 좋게 만든 요약이다
  • CrashLoopBackOff 는 phase 가 아니라 컨테이너 상태 Waiting 의 reason 이다
    • 그때 phase 는 Running 이다
  • STATUS 가 Running 이어도 READY 열이 0/1 이면 그 Pod 는 Service 에서 트래픽을 못 받는다
    • 두 열을 같이 본다
  • probe 는 phase 를 건드리지 않는다
    • readiness 실패는 Ready 조건을 바꾼다
    • liveness·startup 실패는 그 컨테이너의 재시작을 바꾼다

6.2 세 probe 의 차이 — 실패했을 때의 동작

시간0컨테이너 시작startup 2초 × 30 (최대 60초)1성공 — 역할 끝liveness 10초 × 3 → 실패하면 컨테이너 죽이고 재시작 (처음부터)readiness 5초 × 3 → 실패하면 Ready False, 회복하면 Ready True. 재시작 없음✓✗✓ReadyDB 끊김 → 트래픽만 뺌회복 → 다시 받음startup 이 있으면 liveness 에 initialDelaySeconds 가 필요 없다 — 성공 전엔 나머지 둘이 시작도 안 한다
셋은 두드리는 방법이 아니라 실패했을 때 하는 일이 다르다 — 둘은 재시작, 하나는 손님만 안 받기. 어느 것도 Pod phase 를 바꾸지 않는다

probe 세 가지의 용도

  • startup — 느리게 뜨는 앱을 위한 것
    • “다 뜰 때까지 liveness 로 죽이지 마”
  • liveness — 데드락처럼 프로세스는 살아 있는데 멈춘 상태를 잡아 재시작
  • readiness — 살아 있고 멈추지도 않았지만 지금은 요청을 못 받는 상태(워밍업, 과부하, DB 끊김)에서 트래픽만 뺀다
    • 시작 때만이 아니라 운영 중에도 계속 쓰인다
    • 재시작 없이 들어갔다 나왔다 한다

probe 를 안 적었을 때

  • probe 를 안 적으면 쿠버네티스가 알아서 앱 안을 봐 주지 않는다
  • 컨테이너 프로세스가 떠 있으면 바로 Ready 다
  • 앱이 안에서 멈춰 있어도 모른다

6.3 spec 과 앱 코드

Pod spec — kubelet 이 읽는다: 어디를 몇 초마다livenessProbe:httpGet:path: /healthzport: httpperiodSeconds: 10앱 코드 — 앱이 실행: 두드리면 뭐라 답할지HandleFunc("/healthz", → 200)HandleFunc("/readyz", → db.Ping ? 200 : 503)ListenAndServe("0.0.0.0:8080")127.0.0.1:8080 에만 bind 하면 kubelet 의 요청은 거절된다pathportkubeletGET 10.244.1.7:8080/healthz호스트 방에서 veth 건너 Pod IP 로 — 진짜 HTTPPod10.244.1.7앱exec 만 다르다 — CRI 로 컨테이너 안에서 명령 실행scratch 이미지엔 sh 도 curl 도 없어 exec 는 못 쓴다spec 은 전화번호, 코드는 받는 사람 — 번호만 적어 두면 kubelet 은 실패만 받고, 받는 사람만 두면 아무도 걸지 않는다
probe 는 Pod 가 아니라 컨테이너 단위다 — Pod 의 Ready 는 모든 컨테이너가 Ready 이고 readiness gate 까지 True 일 때
방식앱이 준비할 것비고
httpGet그 경로에 200~399 를 돌려주는 핸들러가장 흔하다. scheme: HTTPS 는 인증서를 검증하지 않는다
grpcgRPC Health Checking 서비스 구현v1.27 부터 내장. 숫자 포트만
tcpSocket없음. 포트만 열려 있으면 된다얕다. 앱이 멈춰 있어도 성공
exec이미지 안에 그 명령이 있어야 한다scratch 이미지에는 sh 도 없다

방식 선택: 앱의 창구 종류에 맞춘다

  • HTTP 를 받으면 httpGet
  • gRPC 면 grpc
  • 자기 프로토콜만 쓰는 DB 면 tcpSocket 이나 exec(pg_isready)
  • 포트가 아예 없는 워커면 exec

네트워크 probe 의 조건: 0.0.0.0 bind

  • httpGet·tcpSocket·grpc 는 kubelet 이 호스트 방에서 Pod IP 로 보내는 진짜 패킷이다
  • 앱이 0.0.0.0 에 bind 해야 한다
    • 4.1 의 규칙 3 이 필요했던 이유다
  • exec 는 CRI 로 컨테이너 안에서 명령을 돌리므로 이 제약이 없다

6.4 /healthz 와 /readyz 를 나누는 이유

      startupProbe:
        httpGet: { path: /healthz, port: http }
        periodSeconds: 2
        failureThreshold: 30        # 2초 × 30 = 최대 60초 기다려 준다. 빨리 뜨면 빨리 끝난다
      livenessProbe:
        httpGet: { path: /healthz, port: http }
        periodSeconds: 10
        timeoutSeconds: 1
        failureThreshold: 3         # 멈춘 뒤 약 30초 안에 재시작. 재시작은 비싸니 느긋하게
      readinessProbe:
        httpGet: { path: /readyz, port: http }
        periodSeconds: 5
        timeoutSeconds: 1
        failureThreshold: 3         # 약 15초 안에 Service 에서 뺀다. 빼는 것은 싸니 빠르게

liveness 의 범위: 재시작으로 나아지는 문제만

  • liveness 는 재시작으로 나아지는 문제만 잡아야 한다
  • /healthz 가 DB 에 ping 을 날리게 만들면 이렇게 된다
  • DB 장애 → 웹 Pod 50개가 전부 liveness 실패 → 50개 전부 재시작, 캐시 소실
  • DB 복구 → 50개가 동시에 워밍업하며 DB 를 다시 때림 → DB 다시 느려짐 → 반복

나누는 기준: 재시작하면 나아지나

  • 웹 프로세스는 멀쩡했는데 재시작이 문제를 키운 것이다
  • 기준은 “재시작하면 나아지나” 다
  • DB 끊김처럼 재시작해도 안 나아지는 것은 /readyz 에서 503 으로 돌려 트래픽만 뺀다
  • 경로 이름 자체에 쿠버네티스가 정한 동작은 없다
    • 그 200 이 무엇을 확인한 결과인가가 전부다

시간 값 읽는 법

  • failureThreshold 는 “도달하면” 이다
    • 3이면 세 번째 실패에서 조치한다
  • periodSeconds × failureThreshold 는 설계용 어림값이지 복구 시간의 보장이 아니다
    • 조치가 시작되는 시점이고, 새 컨테이너가 Ready 되기까지는 별도다
  • startup 이 있으면 liveness 에 initialDelaySeconds 를 줄 필요가 없다

6.5 Ready 가 트래픽에 반영되는 과정

1kubelet (노드 B)/readyz 3연속 실패 → API server 에 Pod Ready=False2EndpointSlice 컨트롤러endpoint 10.244.2.7 → conditions.ready: false — 주소는 지우지 않는다3kube-proxy (모든 노드)KUBE-SEP-B 규칙 제거 — Service IP → 10.244.2.7 DNAT 줄이 빠진다4커널 (모든 노드)10.96.0.10 으로 온 패킷은 남은 10.244.1.5 로만 간다Pod IP 10.244.2.7 로 직접 보내면 여전히 간다 — readiness 는 Service 백엔드 목록에서 빼는 것이지 Pod 를 격리하는 것이 아니다①~③ 은 비동기. 다시 성공하면 같은 길을 거꾸로 밟아 규칙이 돌아온다 — 그래서 preStop 에 sleep 을 두는 관행이 있다
두드려 보고 안 맞으면 그 Pod 로 패킷을 안 보낸다 — Endpoints 가 아니라 EndpointSlice 가 기준이고, 주소는 남긴 채 ready 표시만 바꾼다

EndpointSlice: Service 의 백엔드 목록

  • EndpointSlice 는 “이 Service 의 백엔드 Pod IP 목록” 이고 kube-proxy 가 읽는 것도 이것이다
  • 책 시절의 Endpoints 는 객체 하나에 모든 IP 를 넣어, Pod 가 수천 개면 갱신 때마다 전부 다시 보내야 했다
  • 그래서 슬라이스당 100개로 쪼갰고, v1.33 부터 Endpoints 는 deprecated 다
  • 책의 “readiness 실패 시 Endpoints 에서 IP 가 사라진다” 는 지금 구성에 그대로 적용되지 않는다
    • 주소는 남고 ready 표시만 바뀐다
  • publishNotReadyAddresses 를 켠 Service 는 준비 안 된 Pod 에도 보낸다
    • StatefulSet 의 DB 클러스터 초기화처럼 준비 전에도 서로 찾아야 할 때 쓴다

롤링 업데이트와 readiness

  • 롤링 업데이트는 새 Pod 가 Ready 가 되어야 다음 옛 Pod 를 지운다
  • readiness 가 없으면 컨테이너가 뜨자마자 Ready 로 보니, 앱이 워밍업 중인데 옛 Pod 가 전부 사라진다
  • readiness probe 가 무중단 배포의 전제 조건인 이유다

readinessGates: 밖의 조건까지 기다리기

  • 컨테이너는 준비됐는데 밖의 조건이 남을 때가 있다
  • ALB 가 새 Pod IP 를 타깃에 등록하는 데 10~30초가 걸린다
    • 그 사이 옛 Pod 가 지워지면 502 가 난다
  • readinessGates 에 조건 이름을 선언하면 kubelet 은 컨테이너 준비와 그 조건을 같이 평가한다
  • ALB 컨트롤러가 “등록 끝” 이라고 status 에 적어 줄 때까지 Pod 는 Ready 가 되지 않는다

주의: 조건을 바꿔 줄 컨트롤러가 없을 때

  • 선언만 하고 그 조건을 True 로 바꿔 줄 컨트롤러가 없으면 문제가 된다
  • status 에 없는 조건은 False 로 치므로 Pod 가 영원히 Ready 가 안 된다
  • 3.3 의 containerPort 와 같은 종류다
    • 적었다고 동작하는 것이 아니다

7. CNI — 약속, IPAM, 구현

7.1 여섯 작업과 sandbox 의 방

CNI 규격의 범위

  • CNI 는 런타임이 실행 파일을 띄우고 stdin 으로 JSON 을 주고 stdout 으로 JSON 을 받는 약속이다
    • 03-2 의 4절에서 그린 그대로다
  • 규격은 호출 방식과 입력·결과 형식만 정한다
    • 브리지·라우팅·터널 중 무엇을 쓸지는 정하지 않는다
  • 규격 현재 버전은 1.1.0 이다
    • 책의 “0.4 이며 곧 1.0” 은 지났다
작업목적
ADD / DEL연결(veth·IP·라우팅) / 해제(자원 반납)
CHECK이 연결이 기대한 구성과 맞나 — 특정 방의 배선 점검
STATUS (1.1)플러그인이 새 요청을 받을 수 있나 — 기사가 출근했나
VERSION지원하는 규격 버전
GC (1.1)살아 있는 sandbox 목록을 주고 나머지 잔여물을 치우게

STATUS 가 생긴 이유

  • STATUS 가 생긴 것은 Cilium 류 때문이다
  • 실행 파일은 있는데 에이전트가 아직 안 떴으면 ADD 가 실패한다
  • 런타임이 미리 물어보면 그 노드를 잠시 NotReady 로 두고 Pod 를 안 보낸다

호출 대상은 sandbox 의 방

  • 환경변수 CNI_NETNS 는 /var/run/netns/cni-… 같은 경로이고, 그 방은 pause 가 잡고 있는 sandbox 의 방이다
  • CNI_CONTAINERID 도 앱 컨테이너가 아니라 sandbox 의 ID 다
    • 앱이 재시작돼도 CNI 는 다시 불리지 않는다
  • 플러그인은 로그를 stderr 로 보내야 한다
    • stdout 에 찍으면 런타임의 JSON 파싱이 깨져 Pod 가 ContainerCreating 에 멈춘다

“Cilium” 은 실행 파일 하나가 아니다

  • /opt/cni/bin/cilium-cni 는 작은 실행 파일이다
  • 실제 일은 노드마다 떠 있는 cilium-agent 에 유닉스 소켓으로 넘긴다
  • 규격은 “실행 파일이 호출된다” 만 정하고, 그 안에서 에이전트에 위임하든 말든 자유다

7.2 chaining 과 Multus 의 차이

“플러그인이 여러 개” 의 두 뜻

  • chaining — 한 네트워크에 플러그인을 직렬로 건다
    • bridge 가 veth 와 IP 를 만든 결과(prevResult)를 portmap 이 받아 hostPort 의 DNAT 규칙을 더한다
    • bandwidth 가 제한을 건다
    • 문은 여전히 eth0 하나다
  • Multus — Pod 에 네트워크를 여럿 붙이는 메타 플러그인이다
    • eth0 외에 net1 이 생기고 IP 도 둘이 된다
    • 3.1 의 “IP 는 문에 붙는다” 가 여기서 드러난다
  • 쿠버네티스 자체는 eth0 만 안다
    • net1 은 Service 와 NetworkPolicy 의 대상이 아니다

7.3 IPAM 의 결과와 적용

5.2 의 2단계(경비실) 내부

  • 메인 플러그인(bridge)이 IPAM 플러그인(host-local)을 또 실행 파일로 부른다
  • 돌려받은 JSON 을 그대로 적용한다
{
  "cniVersion": "1.1.0",
  "ips":    [{ "address": "10.244.1.10/24", "gateway": "10.244.1.1" }],
  "routes": [{ "dst": "0.0.0.0/0", "gw": "10.244.1.1" }],
  "dns":    { "nameservers": ["10.96.0.10"], "search": ["default.svc.cluster.local", "svc.cluster.local"] }
}

결과 JSON 읽기

  • address 의 /24 가 “옆집 명단” 의 크기다
    • Cilium 이면 여기가 /32 다
  • gateway 가 브리지 IP, routes 가 라우팅 테이블에 들어갈 줄이다
  • IPAM 은 숫자를 정하기만 하고 인터페이스를 건드리지 않는다
    • 적용은 메인 플러그인이, 패킷 전달은 커널이 한다
    • 역할이 세 겹이다
  • dns 는 host-local 이 정하는 값이 아니라 설정 파일의 것을 되돌려 준 것이다
    • 실제 Pod 의 /etc/resolv.conf 는 kubelet 이 dnsPolicy 를 보고 쓴다

설정 경로와 읽는 쪽

  • 설정은 /etc/cni/net.d/, 실행 파일은 /opt/cni/bin/ 에 두는 것이 관례다
    • 그 경로를 읽는 것은 kubelet 이 아니라 containerd 다
  • v1.24 에 kubelet 의 --network-plugin, --cni-bin-dir 옵션이 제거됐다
    • 오래된 글을 따라 kubelet 에 붙이면 시작조차 안 된다

회수: 할당의 일부

  • 회수도 할당의 일부다
  • host-local 은 /var/lib/cni/networks/<망>/10.244.1.10 파일로 “쓰는 중” 을 표시한다
  • 노드가 갑자기 꺼져 DEL 이 안 불리면 그 IP 가 영영 안 풀린다
    • /24 는 253개뿐이라 누적되면 Pod 가 IP 없음으로 못 뜬다
  • GC 가 그 잔여물을 치우라는 요청이다

7.4 구현 넷 — 두 축, 그리고 /24 와 /32

구현노드 사이 전달정책·주소·상태
CalicoIP 라우팅 또는 VXLAN·IP-in-IP. BGP 는 선택NetworkPolicy + 확장 정책. Felix 가 노드 상태 반영
CiliumeBPF. VXLAN·Geneve 터널 또는 네이티브 라우팅NetworkPolicy + L7 확장. CRD 로 상태 관리, IPAM 여러 방식
Flannel노드에 서브넷 주고 VXLAN 등으로 연결정책 집행 없음. netpol.enabled 로 집행기 추가 가능
VPC CNIENI 의 VPC 주소를 Pod 에 직접VPC 서브넷·ENI 한도가 제약. 정책은 설정 확인

구현을 가르는 축 두 개

  • 노드 사이를 터널로 감싸느냐, 라우터가 Pod 대역을 알게 하느냐
  • 정책을 집행하느냐, IPAM 은 누가 하느냐

책의 분류와 지금의 차이

  • 책의 분류 — Calico 는 BGP 라우팅, Flannel 은 정책 됨, Weave 는 현역 — 는 전부 지금과 다르다
  • Weave 는 2024년 6월에 저장소가 닫혔다

터널의 비용과 한계

  • 터널은 기반 네트워크가 노드 IP 만 알면 되지만, 패킷마다 헤더가 붙어 MTU 가 1450 으로 준다
  • 터널이라고 암호화되는 것은 아니다
    • VXLAN 은 UDP 로 감쌀 뿐이라 중간에서 뜯으면 Pod 패킷이 그대로 보인다

같은 노드 안의 차이: /24 와 /32

  • 같은 노드 안에서도 구현이 갈린다
  • Pod 에 /24 를 주느냐 /32 를 주느냐다
/24 브리지 방식 — Flannel, kindnetPod A 의 ip route10.244.1.0/24 dev eth0 scope link ← 옆집 명단default via 10.244.1.1PodA .5PodB .6cni0 스위칭호스트 IP 계층 — 지나지 않음B 는 옆집 → ARP → 브리지가 스위칭. 호스트 IP 계층은 지나지 않는다/32 라우팅 방식 — Cilium, CalicoPod A 의 ip routedefault via 169.254.1.1169.254.1.1 dev eth0 scope link ← 명단 없음PodA .5PodB .6호스트 IP 계층 — 반드시 지남10.244.1.6 dev vethB · 정책 · eBPF · 관찰 여기169.254.1.1 은 실제 주소가 아니다 — 호스트 쪽 veth 가 ARP 에 대신 답한다옆집 없음 → 전부 게이트웨이로 → 호스트가 vethB 로 넘긴다노드는 둘 다 10.244.1.0/24 를 받는다 — /32 는 그 대역을 Pod 에게 숨겨서 모든 라우팅 결정을 호스트로 올리는 설정이다
책의 'Pod 에는 MAC 이 없어 L2 가 안 된다' 는 틀렸다 — L2 를 못 쓰는 게 아니라 안 쓰기로 한 것이고, 그 덕에 호스트가 모든 Pod 통신의 문지기가 된다

/32: “이 대역에 나 혼자”

  • 인터페이스에 IP 를 붙이면 커널이 “같은 대역은 게이트웨이 없이 직접” 이라는 scope link 라우트를 자동으로 만든다
  • /32 면 옆집이 없으니 그 줄이 안 생기고, 모든 패킷이 게이트웨이로 간다
  • 게이트웨이는 호스트라서 호스트의 라우팅 테이블이 “10.244.1.6 은 vethB 로” 를 보고 넘긴다

비유: 복도가 있는 층과 관리실로만 열리는 층

  • 비유로는 복도가 있는 층(/24)과 모든 방문이 관리실로만 열리는 층(/32)이다
  • 관리실을 반드시 지나게 하면 거기서 “A 는 B 방에 못 들어감” 을 막고, 누가 어디 갔는지 기록하고, 검사를 더할 수 있다
  • Cilium 과 Calico 가 복도를 없앤 이유다
  • 169.254.1.1 은 실제 주소가 아니라 “일단 veth 반대편으로” 라는 자리표시자다
    • 호스트 쪽 veth 가 ARP 에 대신 답한다(proxy ARP)

7.5 Cilium 의 구성 요소

구성 요소역할어디서
cilium-operator클러스터 전체 — 노드마다 Pod 블록 나눔(5.2 의 1단계). 패킷 경로 아님Deployment Pod 1~2개
cilium-agent노드마다 — 블록에서 IP 꺼냄(2단계), eBPF 프로그램 로드, 정책 반영DaemonSet, hostNetwork
cilium-cniPod 생성 때 런타임이 부름. agent 에 위임/opt/cni/bin/
cilium CLI설치, cilium status, 연결성 테스트관리자 PC, kubeconfig
cilium-dbg특정 노드 agent 의 내부 상태, eBPF 맵cilium-agent Pod 안
Hubble흐름 관찰. Relay 가 노드별 정보를 모으고 CLI·UI 로Relay 는 Pod

cilium CLI 와 cilium-dbg 의 구분

  • 옛 문서는 둘 다 cilium 이라고 써서 복붙하면 command not found 가 난다
  • 관리자 PC 의 cilium status — 클러스터 전체가 정상인가
  • Pod 안의 cilium-dbg status — 이 노드의 agent 가 어떤 상태인가

플러그인 선택: 제약에서 시작

  • 플러그인을 고를 때는 기능 이름 개수가 아니라 제약에서 시작한다
  • 라우터를 건드릴 수 있나 — 없으면 터널, VPC 면 VPC CNI
  • 정책이 L3/L4 로 충분한가 — HTTP 경로까지면 Cilium
  • 커널 버전이 되나 — eBPF 는 5.10+, nftables kube-proxy 는 5.13+
  • 장애 때 무엇을 볼 수 있나 — Hubble 같은 흐름 관찰

성능 판단

  • “eBPF 라서 빠르다” 는 단정하지 않는다
  • 같은 정책, 같은 트래픽으로 재야 안다

8. Discussion

8.1 3장 재료의 새 이름

3장에서4장에서누가 치나
ip netns add 로 만든 방Pod sandbox. pause 가 붙들고 있다containerd
ip link add veth + 브리지에 꽂기CNI ADD런타임이 부른 플러그인
ip addr add 10.203.0.2/24IPAM 결과의 ips[].addresshost-local 또는 CNI 의 IPAM
호스트의 ip route addpodCIDR 과 노드 간 라우팅·터널CNI 에이전트
-p 의 DNAT 한 줄Service 의 KUBE-SVC → KUBE-SEP 규칙kube-proxy
MASQUERADE ! -o docker0MASQUERADE ! -d 10.244.0.0/16CNI 또는 노드 설정
curl 로 직접 두드리기kubelet 의 probekubelet

핵심 메시지

방 하나, IP 하나
IP 는 문에 붙고 Pod 방에는 문이 하나다. 127.0.0.1 은 부르는 쪽의 방이고, 포트 충돌의 단위는 방 안의 (IP, 포트) 쌍이다
세 규칙
Pod IP 로 직접, NAT 없이, 노드가 Pod 에 닿는다. Docker bridge 를 노드 여럿으로 늘린 것이고 MASQUERADE 조건 한 칸이 다르다
두 단계 IPAM
범위는 노드 등록 때 한 번, 낱개는 Pod 가 뜰 때마다 노드 안에서. IP 는 Pod 객체가 새로 만들어질 때만 바뀐다
제어와 데이터
kube-proxy 와 CNI 에이전트는 규칙을 쓰고 빠진다. 패킷은 커널이 나르고, API server 가 죽어도 흐른다
probe
kubelet 이 호스트 방에서 Pod IP 로 두드린다. spec 과 코드 두 자리가 맞아야 하고, 셋의 차이는 실패했을 때 하는 일이다
/24 와 /32
옆집 명단을 주느냐 숨기느냐. 숨기면 모든 Pod 통신이 호스트를 지나고, 거기가 정책과 관찰이 붙는 자리다

3장이 “컨테이너는 방 안의 프로세스 하나” 였다면 이 편은 “Pod 는 IP 하나를 가진 방이고, 그 방들이 노드를 넘어 NAT 없이 닿아야 한다” 다. 그 요구를 실제로 만드는 것은 노드의 라우팅 테이블과 nat 테이블에 적힌 규칙이고, 누가 그 줄을 썼는지 — CNI 인지 kube-proxy 인지 — 를 나누어 읽으면 Pod 네트워크와 Service 구현이 다른 층이라는 것이 보인다.

9. References

세 규칙(Pod 간 NAT 없는 직접 통신, 노드 에이전트의 Pod 접근)과 네트워크 구현을 쿠버네티스가 지정하지 않는다는 것은 클러스터 네트워킹 문서에 있다.
"pods can communicate with all other pods on any other node without NAT"
[해석] "Pod 는 다른 어느 노드의 어느 Pod 와도 NAT 없이 통신할 수 있다." — 4.1 의 세 규칙과 2절 그림 ②의 근거다. 같은 문서에 "Kubernetes does not ship with a network plugin" 이라고 적혀 있어 4.1 의 "구성자가 설치한다" 와 NetworkPolicy 미집행 함정의 근거가 된다.
→ kubernetes.io — Cluster Networking
→ kubernetes.io — Services, Load Balancing, and Networking (The Kubernetes network model)
Pod 안 컨테이너가 네트워크 네임스페이스·IP·포트 공간을 공유하고 localhost 로 통신한다는 것, hostNetwork 와 dual-stack 은 Pod 문서와 dual-stack 문서에 있다.
"Within a Pod, containers share an IP address and port space, and can find each other via localhost"
[해석] "Pod 안에서 컨테이너들은 IP 주소와 포트 공간을 공유하고 localhost 로 서로를 찾는다." — 3.1 과 2절 그림 ①의 근거다. containerPort 가 선언일 뿐이라는 것은 Pod API 레퍼런스의 ports 필드 설명에, Pod 가 노드 사이를 이동하지 않고 같은 UID 로 재스케줄되지 않는다는 것은 Pod lifecycle 문서에 있다.
→ kubernetes.io — Pods
→ kubernetes.io — Pod Lifecycle
→ kubernetes.io — IPv4/IPv6 dual-stack
kube-controller-manager 의 Node IPAM 플래그와 기본값(--node-cidr-mask-size 24/64, --cidr-allocator-type RangeAllocator)은 명령 레퍼런스에 있다.
"--allocate-node-cidrs Should CIDRs for Pods be allocated and set on the cloud provider."
[해석] "Pod 용 CIDR 을 할당해 노드에 설정할지." — 5.2 의 표와 두 단계 IPAM 그림의 근거다. 실행 파일 기본값과 kubeadm 이 넣는 값이 다르다는 것은 kubeadm 의 ClusterConfiguration(networking.podSubnet) 문서로 확인된다. 책의 --cluster-CIDR 대소문자와 "/24 = 2의 24제곱" 은 오류다.
→ kubernetes.io — kube-controller-manager
→ kubernetes.io — Ports and Protocols
세 probe 의 동작, 네 방식, 간격·임계치 필드와 기본값, gRPC probe 의 Stable 시점, probe 가 kubelet 에서 수행된다는 것은 컨테이너 probe 문서와 설정 가이드에 있다.
"A probe is a diagnostic performed periodically by the kubelet on a container."
[해석] "probe 는 kubelet 이 컨테이너에 주기적으로 수행하는 진단이다." — 6.3 의 "kubelet 이 호스트 방에서 Pod IP 로 두드린다" 와 그림의 근거다. readiness 실패가 EndpointSlice 의 ready 조건으로 반영되고 Endpoints 가 v1.33 부터 deprecated 라는 것은 EndpointSlice 문서와 1.33 릴리스 노트에 있다. 책의 "probe 가 Pod phase 를 정한다" 는 상태 계층을 섞은 서술이다.
→ kubernetes.io — Container probes
→ kubernetes.io — Configure Liveness, Readiness and Startup Probes
→ kubernetes.io — EndpointSlices
→ kubernetes.io — Pod readiness gate
CNI 1.1.0 의 여섯 작업, 환경변수, stdin/stdout/stderr 규칙, prevResult 를 넘기는 chaining, IPAM 위임과 결과 형식은 CNI 규격에 있다.
"CNI_NETNS: A reference to the container's 'isolation domain'. If using network namespaces, then a path to the network namespace"
[해석] "CNI_NETNS 는 컨테이너의 격리 영역, 네트워크 네임스페이스를 쓴다면 그 경로다." — 7.1 의 "그 방은 pause 의 방" 과 5.3 그림의 ⑤번 단계의 근거다. 책의 "CNI 0.4 이며 곧 1.0" 은 현재 상태가 아니고, "Pod 에는 MAC 이 없다" 는 규격 예제의 interfaces[].mac 필드와 맞지 않는다. kubelet 의 CNI 옵션 제거는 v1.24 변경 사항에 있다.
→ containernetworking/cni — Specification
→ cni.dev — host-local IPAM plugin
→ kubernetes.io — Network Plugins
kube-proxy 모드의 현재 상태(nftables Stable v1.33, IPVS deprecated v1.35)와 Cilium 의 구성 요소·라우팅 모드·kube-proxy 대체는 각 문서에 있다.
"kube-proxy ... is not itself in the data path"
[해석] "kube-proxy 는 데이터 경로에 있지 않다." — 5.5 와 5.1 그림의 근거다. 책의 "IPVS 가 새로운 권장 방식" 은 지금 적용하지 않는다. Weave Net 저장소 보관(2024-06-20)은 GitHub 저장소 안내에 있고, Calico 를 "오버레이 없음" 으로 고정한 서술은 Calico 의 VXLAN·IP-in-IP 옵션과 맞지 않는다.
→ kubernetes.io — Virtual IPs and Service Proxies
→ docs.cilium.io — Component Overview
→ docs.cilium.io — Routing (encapsulation / native routing)
→ docs.tigera.io — Overlay networking (VXLAN / IP-in-IP)
→ github.com/weaveworks/weave — archived