0. Contents

1. Overview

방컨테이너의 네임스페이스모드가 정한다 — 2장브리지 네트워크docker0 · cni0 — 같은 L2이름은 DNS 가 푼다 — 2·5장창구호스트의 -p · NATdocker0 ↔ ens3 는 라우팅 — 3장다른 호스트오버레이 · 라우팅VXLAN 터널 — 3장누가 이 네 칸을 꽂나 — 4장도커 — CNM (Sandbox · Endpoint · Network)쿠버네티스 — containerd 가 CNI 실행 파일을 부른다같은 리눅스 부품을 쓰고, 다른 것은 누가 무엇을 관리하느냐뿐03-1 이 방 안의 프로세스 하나였다면, 이 편은 그 방에서 밖으로 나가는 길 — 브리지, 공개 포트, 터널 — 과 그 길을 꽂는 손이다
02-1 에서 손으로 만든 방·파이프·스위치가 도커의 모드와 쿠버네티스의 CNI 에서 어떤 이름으로 불리는지가 이 편의 전부다

03-1 정리: 방 안의 프로세스 하나

  • 03-1 은 컨테이너를 방 안의 프로세스 하나로 봤다
  • runC 가 방을 만들고, 네임스페이스가 무엇을 볼지, cgroup 이 얼마나 쓸지를 정했다

이 편의 주제: 방에서 밖으로 나가는 길

  • 이 편은 그 방에서 밖으로 나가는 길이다
  • 02-1 에서 손으로 만든 방·파이프·스위치가 다시 나타난다
    • 도커에서는 모드라는 이름으로
    • 쿠버네티스에서는 CNI 플러그인이 치는 명령으로

다루는 질문 네 가지

  • 방을 만드느냐, 만들면 문을 어디로 내느냐 — 모드
  • 브리지와 진짜 문(NIC) 사이는 무엇으로 이어지느냐 — 라우팅과 NAT
  • 호스트 둘 사이는 어떻게 잇느냐 — 오버레이
  • 그 모든 것을 누가 꽂느냐 — CNM 과 CNI
  • 마지막 장에서 Go 서버 하나를 네 자리에서 두드려 이 넷이 실제로 어떻게 보이는지 확인한다

2. Docker Network Modes — 방을 만드느냐, 문을 다느냐

2.1 모드는 새 기술이 아니다

모드: 02-1 부품의 조합

  • docker run --network <모드> 의 모드는 컨테이너의 네트워크 방을 어떻게 꾸밀지 고르는 선택지다
  • 전부 02-1 에서 만든 부품 — 방, 문, 파이프, 스위치 — 의 조합이다
  • 질문은 둘뿐이다
    • 방을 따로 만드는가
    • 만들면 그 방의 문을 어디로 내는가

모드별 방과 문

  • host — 방을 안 만든다
    • 호스트의 방에서 그대로 산다
    • 컨테이너 IP 가 따로 없고, 앱이 여는 포트는 호스트의 포트 공간에 속해 다른 프로세스와 충돌할 수 있다
    • -p 는 적용되지 않는다
  • none — 방은 만들되 문이 없다
    • lo 만 남아 자기 자신과만 통한다
  • bridge — 방을 만들고 문을 호스트 안의 스위치(docker0)에 꽂는다
    • 기본값이고 2장 실습의 구조다
    • 나가는 트래픽은 호스트가 NAT 로 내보내고, 들어오는 연결은 -p 로 받는다
  • macvlan, ipvlan — 방을 만들고 문을 물리 NIC 위에 직접 낸다
    • 컨테이너가 LAN 에 꽂힌 별도 장비처럼 보인다
    • 컨테이너가 호스트 뒤에 숨지 않고 호스트와 나란히 서는 구성이다
    • 둘의 차이는 MAC 을 각자 갖느냐(macvlan) 호스트 것을 나눠 쓰느냐(ipvlan)다
  • overlay — 여러 호스트의 방들을 터널로 이어 같은 스위치에 있는 것처럼 만든다
    • 4장에서 다룬다
  • --network container:<이름> — 남의 방에 같이 산다
    • IP, 포트 공간, lo 를 공유해 127.0.0.1 로 서로에게 닿는다
    • 쿠버네티스 Pod 의 pause 컨테이너가 하는 일이 정확히 이것이다

2.2 셋을 나란히 띄우면 [실습]

실습: 모드만 바꾼 busybox 셋

  • 말로는 다 비슷해 보여서 VM 에서 busybox 를 모드만 바꿔 셋 띄웠다
  • 비교한 것: 안에서 보이는 인터페이스와 경로, 그리고 /proc/PID/ns/net 의 번호
for m in bridge none host; do docker run -d --name m-$m --network $m busybox sleep 600; done
for c in m-bridge m-none m-host; do docker exec $c ip -o -4 addr; docker exec $c ip route; done
sudo readlink /proc/1/ns/net
for c in m-bridge m-none m-host; do sudo readlink /proc/$(docker inspect -f '{{.State.Pid}}' $c)/ns/net; done
busybox sleep 을 모드만 바꿔 셋 — 안에서 본 것1--network bridge (기본값)lo eth0@if52 172.17.0.2/16default via 172.17.0.1 dev eth0ns/net → 4026532265 (호스트와 다름)2--network nonelo 127.0.0.1/8 뿐(경로 없음)ns/net → 4026532344 (호스트와 다름)3--network hostlo ens3 docker0 veth… = 호스트 그대로default via 10.0.0.1 dev ens3ns/net → 4026531840 = 호스트 PID 1번호가 같으면 같은 방 — host 만 호스트와 같다호스트 커널 — 루트 네트워크 방ens3docker010.0.0.198172.17.0.11방 — bridgeloeth0172.17.0.2문 하나 — veth 로 docker0 에2방 — nonelo문 없음바깥으로 가는 길이 없다3host — 방 없음busybox sleep호스트 방에 그대로 산다ens3 · docker0 전부 보임8080 을 열면 호스트의 8080방을 만드느냐(①②) 안 만드느냐(③), 만들면 문을 다느냐(①) 마느냐(②) — 모드는 이 두 질문의 답
실습 출력의 네임스페이스 번호가 증거다 — bridge 와 none 은 새 번호, host 는 호스트 PID 1 과 같은 번호

결과: 번호 하나가 셋을 가른다

  • bridge 와 none 은 호스트와 다른 번호를 받았고, host 는 호스트 PID 1 과 같은 번호다
    • 방을 안 만들었다는 것이 숫자로 보인다
  • none 의 방에는 lo 하나뿐이고 경로가 한 줄도 없다
  • bridge 의 방에는 eth0 이 있고 그 이름의 @if52 는 호스트 쪽 짝 인터페이스의 번호다
    • veth52 라는 이름이 아니라 번호 52 번이다
    • 호스트에서 ip link show master docker0 을 치면 그 번호의 veth 가 docker0 에 꽂혀 있다

포트 공간 확인

  • bridge 컨테이너와 host 컨테이너가 동시에 8080 을 열어도 충돌하지 않는다
  • host 쪽 8080 은 호스트의 ss -lnt 에 나타나고, bridge 쪽 8080 은 그 방 안에만 있다
    • 02-1 의 4.2 와 같은 이야기다

2.3 안과 밖 — 번호로 짝 찾기, 방에 들어가기 [실습]

실습: bridge 컨테이너 하나의 안과 밖

  • 컨테이너 안 — eth0 이 어떤 번호를 달고 있는지, 라우팅 표에 무엇이 적혀 있는지
  • 호스트 — docker0 에 어떤 인터페이스가 꽂혀 있는지, 호스트 라우팅 표에 docker0 으로 가는 줄이 있는지
docker exec m-bridge ip -o link show eth0
docker exec m-bridge ip route
ip link show master docker0
ip route | grep docker0
안팎 대조 명령 네 줄 — 두 줄은 방 안에서, 두 줄은 호스트에서어디서컨테이너 안이면 docker exec명령무엇을 보라대상어느 문·브리지옵션어떻게거르기출력 중 고르기1안 — 문docker exec m-bridgeip -o link showeth0방 안의 eth0 한 줄 — '2: eth0@if52' 의 2 는 내 번호, 52 는 짝의 번호. -o 는 한 줄로2안 — 경로docker exec m-bridgeip route방 안의 라우팅 표 — default via 172.17.0.1 dev eth0. 바깥은 docker0 에게3밖 — 포트ip link showmaster docker0docker0 에 꽂힌 것만 — '52: veth…@if2 master docker0'. 52 가 안의 짝4밖 — 경로ip route| grep docker0호스트 라우팅 표에서 docker0 줄만 — 172.17.0.0/16 dev docker0. 호스트가 컨테이너 IP 로 직접 닿는 이유보라색이 붙은 줄은 방 안에서 친 것 — 같은 ip 명령인데 어느 방에서 치느냐로 보이는 것이 다르다
줄마다 같은 조각이다 — 어디서, 무엇을 보라, 어느 문에, 어떻게. 02-1 의 명령 해부와 같은 꼴
컨테이너 안 — docker exec m-bridge12: eth0@if52 172.17.0.2/162default via 172.17.0.1 dev eth0172.17.0.0/16 dev eth0@if52 — 내 짝은 호스트의 52번172.17.0.1 — 바깥은 docker0 에게호스트 — ip link show master docker0152: veth12ba5d7@if2 master docker02docker0 172.17.0.1/16172.17.0.0/16 dev docker0@if2 — 내 짝은 방 안의 2번도커가 주소와 경로까지 적어 뒀다veth 한 쌍방에 들어가기 — ip netns list 에는 없지만 방은 있다3ip netns list → (빈 출력)4PID=$(docker inspect -f '{{.State.Pid}}' m-bridge)5readlink /proc/$PID/ns/net → net:[4026532265]6nsenter -t $PID -n ip -br addr → lo, eth0@if52 172.17.0.2nsenter 는 네트워크 방만 바꾼다 — 이미지 안에 ip 가 없어도 호스트의 ip 로 본다. 책의 /run/netns 링크 방식은 같은 일을 이름 등록으로 한다도커는 문만 단 게 아니다 — 방, 문, 주소, 경로 넷을 준비한다. 안팎의 짝은 이름이 아니라 번호(@ifN)로 찾는다
02-1 의 5.1 에서 한 nsenter 를 도커 명령과 나란히 — 같은 방을 docker exec 와 nsenter 가 다른 길로 본다

방 안의 라우팅 표 두 줄 읽기

  • 조각으로 끊어 읽으면 된다
컨테이너 안의 ip route 두 줄 — 조각별로목적지어디로 가는 패킷이면다음 홉누구에게 건네라문어느 인터페이스로보조커널이 덧붙인 정보1기본 경로defaultvia 172.17.0.1dev eth0default = 아래 줄에 안 걸리는 나머지 전부. 바깥 세상으로 가는 패킷via 172.17.0.1 = docker0 에게 건네라. 호스트가 라우터가 되어 NAT 로 내보낸다 (02-2)dev eth0 = 방의 유일한 문으로 내보낸다2직접 경로172.17.0.0/16dev eth0scope linksrc 172.17.0.2172.17.0.0/16 = 같은 브리지에 매달린 다른 컨테이너들. 이 줄이 먼저 맞는다 (더 구체적이니까)다음 홉이 없다 = 게이트웨이 없이 ARP 로 직접 찾는다. scope link 가 그 뜻src 172.17.0.2 = 이 줄로 나갈 때 출발지로 적을 주소. 주소를 붙일 때 커널이 자동으로 만든 줄 (proto kernel)패킷은 두 줄을 위에서부터가 아니라 '더 구체적인 줄부터' 맞춰 본다 — 172.17.x 면 ②, 아니면 ①
줄마다 세 조각이다 — 어디로 가는 패킷이면, 누구에게, 어느 문으로. 02-2 의 라우팅 표 읽기와 같은 꼴

짝 찾기: eth0@if52 와 veth12ba5d7@if2

  • 안의 eth0@if52 와 밖의 veth12ba5d7@if2 가 한 쌍이다
  • 이름이 아니라 번호로 짝을 찾는다
    • 안의 2번은 밖의 52번과, 밖의 52번은 안의 2번과 묶여 있다
    • veth52 같은 이름은 없다
  • 안의 기본 경로가 가리키는 172.17.0.1 은 밖의 docker0 주소다
  • 도커가 docker run 한 번에 방, veth, 주소, 경로를 전부 준비했다
    • 02-1 에서 손으로 친 명령 아홉 줄이 그 안에 들어 있다

방에 들어가는 길: 02-1 의 5.1 과 같다

  • ip netns list 는 비어 있어도 PID 로 찾아가면 된다
  • nsenter -t PID -n 은 네트워크 방만 바꾸므로 이미지 안에 ip 명령이 없어도 호스트의 ip 를 쓸 수 있다
  • 책은 /run/netns/ 에 링크를 걸어 ip netns exec 로 들어가는 방식을 쓴다
    • 같은 일을 이름 등록으로 하는 것이라 해도 되고 안 해도 된다
  • 정지된 컨테이너의 State.Pid 는 0 이니 재시작했으면 다시 조회한다

전체 명령과 출력 — 02-docker-modes

2.4 기본 네트워크 셋과 docker0 의 주소

기본 네트워크 객체 셋

  • docker network ls 에는 bridge, host, none 이 기본으로 있다
  • 모드 셋이 네트워크 객체로 미리 만들어져 있는 것이다
  • --network 를 안 주면 bridge 에 붙는다

docker0 의 주소와 상태

  • 호스트 쪽 브리지 이름은 docker0 이고 이 VM 에서는 172.17.0.1/16 이다
  • 이 값은 흔한 예일 뿐 보장되지 않는다
    • 기존 네트워크와 겹치거나 데몬 설정이 다르면 다른 대역을 쓴다
    • docker network inspect bridge 로 실제 값을 본다
  • 컨테이너가 하나도 없을 때 docker0 이 DOWN 으로 보이는 것은 고장이 아니다
    • 02-1 의 3.1 에서 본 그 DOWN 이다

3. Docker Networking Model — 이름 셋, 그리고 호스트 밖으로

3.1 CNM — Sandbox, Endpoint, Network

CNM 의 세 단어

  • 도커는 자기 네트워크 모델에 CNM(Container Network Model)이라는 이름을 붙였다
  • 구현체인 libnetwork 가 세 단어로 구성을 나눈다
  • Sandbox — 컨테이너가 쓰는 네트워크 환경 전부
    • 네트워크 네임스페이스에 인터페이스·경로·DNS 설정을 더한 것
  • Endpoint — 그 Sandbox 를 네트워크 하나에 잇는 접점
    • veth 한 쌍
  • Network — 서로 통하는 Endpoint 들의 집합
    • 브리지 하나

리눅스 브리지 구성과의 대응

  • 리눅스 브리지 구성에 대응시키면 새 부품은 하나도 없다
CNM 의 세 단어 — libnetwork 가 쓰는 이름1Sandbox컨테이너의 네트워크 환경 전부= 네트워크 네임스페이스 + 인터페이스·경로·DNS2EndpointSandbox 를 네트워크 하나에 잇는 접점= veth 한 쌍 (방 안 eth0 + 호스트 쪽 끝)3Network서로 통하는 Endpoint 들의 집합= 브리지 하나 (docker0 또는 사용자 정의)Endpoint 는 호스트가 아니다 — 연결 하나다Sandbox 하나에 Endpoint 를 둘 달면 네트워크 둘에 참여한다호스트 — 컨테이너 하나가 네트워크 둘에1Sandbox — 컨테이너의 방eth02eth12vethvethapp-net (bridge)3db-net (bridge)3다른 앱 컨테이너DB 컨테이너앱끼리는 app-net 으로, DB 는 db-net 으로만 — 네트워크를 나누면 닿는 범위가 갈린다CNM 은 새 부품이 아니라 02-1 의 방·파이프·스위치에 붙인 이름이다 — 모델은 셋, 실제 연결은 드라이버가 만든다
docker network create 로 네트워크를 나누고 docker network connect 로 컨테이너를 둘에 붙이면 이 그림이 된다

Endpoint: 호스트가 아니라 연결 하나

  • Endpoint 를 호스트로 읽으면 안 된다 — 연결 하나다
  • Sandbox 하나에 Endpoint 를 둘 달면 컨테이너 하나가 네트워크 둘에 참여한다
  • 앱용 bridge 와 DB 용 bridge 를 따로 만들고 앱 컨테이너만 둘 다에 붙이면
    • 앱끼리는 app-net 으로 통한다
    • DB 에는 db-net 으로만 닿는다
  • 모델은 이 셋뿐이고, 실제로 어떻게 잇는지는 드라이버(bridge, host, overlay, macvlan …)가 정한다
  • 책의 “custom” 은 드라이버 이름이 아니라 사용자가 만든 네트워크를 가리키는 말이다

3.2 브리지의 바깥 연결 — NAT 는 기본값이지 법칙이 아니다

기본 bridge 구성의 주소와 연결

  • 기본 bridge 구성에서 컨테이너는 사설 주소를 받는다
  • 밖으로 나갈 때는 masquerade 로 출발지가 호스트 주소로 바뀐다
  • 밖에서 들어오는 연결은 -p 로 호스트 포트를 컨테이너 포트에 이어야 받는다
  • 같은 브리지 안의 컨테이너끼리는 컨테이너 IP 와 실제 포트로 바로 통한다

바탕: docker0 과 ens3 는 서로 꽂혀 있지 않다

  • 둘은 호스트 루트 방의 문 둘이고, 사이에 호스트의 IP 계층이 서 있다
  • 컨테이너가 바깥으로 보낸 패킷은 다섯 단계를 거친다
컨테이너 → 바깥, 다섯 단계 — 실습 VM 의 실제 값1방: default via 172.17.0.1 dev eth0바깥행 패킷은 docker0 에게2docker0 의 MAC 이 목적지 → IP 계층으로스위칭이 아니라 위로 올린다3호스트: default via 10.0.0.1 dev ens3ip_forward=1 이라 남의 패킷도 넘긴다4-A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE출발지 172.17.0.2 → 10.0.0.1985ens3 → 케이블답장은 conntrack 장부로 거꾸로 (02-2)docker0 과 ens3 는 꽂혀 있지 않다 — 사이에 ③④ 가 있다호스트 루트 방컨테이너 방eth0 .0.21vethdocker0 .0.12호스트 IP 계층라우팅 → ens33NAT: 출발지 바꿈4ens3 10.0.0.1985밖컨테이너 둘이면 여기서스위칭만 — ③④ 안 거침브리지와 진짜 NIC 를 잇는 것은 케이블이 아니라 호스트의 라우팅 표와 NAT 규칙이다 — 호스트가 작은 라우터
책의 열 단계는 여기서 ens3 를 docker0 에 직접 꽂았다 — 그러면 ③④ 가 사라지고 컨테이너가 LAN 에 직접 선다. 03-2 의 macvlan 이 그 계열

바깥행 패킷의 다섯 단계

  • ① 컨테이너 방의 라우팅이 바깥행 패킷을 docker0 에게 넘긴다
  • ② docker0 에 도착한 패킷은 목적지 MAC 이 docker0 자신이라 스위칭되지 않고 호스트 IP 계층으로 올라간다
  • ③ 호스트 IP 계층은 자기 라우팅 표를 보고 ens3 를 고른다
    • 남의 패킷을 넘겨 주려면 ip_forward 가 켜져 있어야 한다
  • ④ 나가기 직전에 MASQUERADE 규칙이 출발지를 172.17.0.2 에서 호스트 주소 10.0.0.198 로 바꾼다
  • ⑤ ens3 가 케이블로 내보내고, 답장은 02-2 의 conntrack 장부를 따라 거꾸로 돌아온다

들어오는 연결과 컨테이너끼리의 연결

  • 들어오는 연결의 -p 는 같은 자리에서 목적지를 바꾸는 DNAT 규칙이다
  • 컨테이너 둘이 서로 통할 때는 이 다섯 단계 중 docker0 의 스위칭만 쓰고 IP 계층으로 올라가지 않는다

02-1 그림에서 ens3 를 따로 그린 이유

  • 02-1 의 브리지 그림에서 ens3 를 브리지에 꽂지 않고 옆에 따로 그린 이유가 이것이다
  • 책의 열 단계는 반대로 진짜 NIC 를 브리지의 포트로 직접 꽂는다
    • 그러면 호스트 IP 계층을 거치지 않고 컨테이너가 건물 LAN 에 직접 선다
  • 그 구성의 정식 버전이 2.1 의 macvlan 이고, 문서의 실습은 그 방식을 쓰지 않는다

문서가 못 박는 보정 두 가지

  • 이건 기본 IPv4 bridge 의 이야기이지 도커의 법칙이 아니다
    • IPv6, routed gateway 모드, macvlan·ipvlan, 라우팅을 따로 짠 환경에서는 NAT 없이 나간다
  • 책은 도커가 iptables 로 격리한다고 쓰는데, 방화벽 구현을 하나로 고정하면 지금 구성을 놓친다
    • Docker Engine 29.0 에 native nftables 백엔드가 들어왔고, 확인 시점 문서에서는 실험 기능이며 Swarm 에서는 켤 수 없다
    • iptables 명령이 nft 호환 계층 위에서 도는 것과 도커의 native nftables 백엔드는 다른 설정이다
    • 02-2 에서 본 VM 의 iptables 는 nf_tables 백엔드 위의 호환 계층이었다

3.3 Overlay — 호스트 둘 사이의 터널

브리지의 한계: 호스트 안의 연결만

  • 호스트 하나의 브리지는 그 호스트 안의 연결만 준다
  • 두 호스트에 docker0 을 하나씩 만들었다고 둘이 이어지지 않는다
  • 여러 호스트의 컨테이너가 통하려면 상대 주소로 가는 경로, 주소 중복 방지, 노드 사이 전달 방식이 더 필요하다
  • 답은 오버레이이거나 일반 IP 라우팅이다
  • 오버레이는 이미 이어져 있는 호스트 네트워크(underlay) 위에 컨테이너용 논리 네트워크(overlay)를 얹는 것이다
호스트 A컨테이너10.0.0.2overlay 브리지VTEP (vxlan)NIC 192.168.10.11호스트 B컨테이너10.0.0.3overlay 브리지VTEP (vxlan)NIC 192.168.10.12underlay — 그냥 IP 네트워크UDP 4789A 의 VTEP 가 내보내는 패킷 — 바깥부터외부 Ethernetunderlay 다음 홉까지외부 IP192.168.10.11 → 192.168.10.12 (VTEP 끼리)UDP목적지 포트 4789VXLANVNI — 어느 오버레이인지 (24비트)원래 Ethernet 프레임10.0.0.2 → 10.0.0.3, 컨테이너가 보낸 그대로컨테이너의 L2 프레임을 호스트 사이의 L3 로 실어 나르는 터널 — 그래서 MTU 가 줄고, 암호화는 따로 켜야 한다
컨테이너 둘은 같은 스위치에 있는 줄 안다 — 그 착각을 만드는 것이 두 VTEP 와 VNI 하나다

도커 overlay 드라이버: VXLAN 터널

  • 도커의 overlay 드라이버는 VXLAN 으로 이 터널을 만든다
  • 컨테이너가 보낸 이더넷 프레임을 VTEP 가 UDP 4789 패킷에 통째로 담아 상대 호스트의 VTEP 에 보낸다
    • 받은 쪽이 바깥 헤더를 벗겨 원래 프레임을 꺼낸다
  • VLAN ID 가 12비트인 데 비해 VNI 는 24비트라 식별 값이 약 1,677만 개다
    • 규격상의 수이지 그만큼 운영할 수 있다는 보장은 아니다
  • 헤더가 50바이트쯤 붙으니 MTU 를 그만큼 줄여야 한다
  • 캡슐화는 암호화가 아니라서 도커 overlay 의 암호화는 별도 옵션이다
  • 헤더 비트 구조와 MTU 1450 의 계산은 VXLAN 위키에 있다

책과 다른 점: 외부 저장소는 옛 classic Swarm 이야기

  • 책은 여러 호스트의 정보를 나누려면 Consul·etcd·ZooKeeper 같은 외부 저장소가 반드시 필요하다고 쓴다
  • 옛 classic Swarm 의 이야기다
  • 그 옵션은 Docker 23.0 에서 제거됐다
    • 지금의 Swarm 모드는 manager 들이 Raft 합의로 클러스터 상태를 직접 관리한다
  • 도커의 overlay 드라이버를 쓰려면 호스트들이 Swarm 에 참여해 있어야 한다
    • 일반 docker run 컨테이너를 붙이려면 --attachable 네트워크를 만든다
  • 이 편에서는 Swarm 을 세우지 않았다
    • 오버레이의 실물은 4장의 쿠버네티스 노드 사이에서 본다

4. CNI — 런타임이 밖의 실행 파일을 부르는 약속

4.1 CNI 가 정하는 경계

CNI 의 뜻: 규격과 생태계

  • CNI(Container Network Interface)는 컨테이너 실행 시스템이 네트워크 플러그인을 어떻게 부를지 정한 규격이다
  • 그 규격을 따르는 라이브러리와 플러그인 생태계를 함께 가리키는 말이기도 하다
  • CoreOS 의 rkt 에서 출발했고, 도커의 CNM 과는 추상화와 호출 경계가 다르다
  • CNI 는 특정 브리지나 VXLAN 제품의 이름이 아니다

일반적인 리눅스 구성의 역할 분담

  • 런타임이 네트워크 네임스페이스를 준비한다
  • CNI 플러그인이 그 안의 인터페이스와 주소와 경로를 구성한다
    • 필요하면 호스트 쪽 설정도 한다
  • IP 할당은 IPAM 플러그인에 넘길 수 있다
  • 플러그인 여럿을 이어서 인터페이스를 만든 뒤 추가 설정을 얹을 수도 있다
  • 모든 플러그인이 veth 와 브리지와 NAT 를 똑같이 만드는 것은 아니다
플러그인 호출 한 번 — 실행 파일 하나를 부르는 것뿐1CNI_COMMAND=ADD1CNI_NETNS=/proc/2778024/ns/net1CNI_IFNAME=eth0환경변수 — 무엇을, 어느 방에, 무슨 이름으로2stdin ← { "type": "bridge", "ipam": {...} }JSON 설정 — 어떤 플러그인, 주소는 어디서3stdout → { "ips": [...], "routes": [...] }성공하면 JSON 으로 결과 — 받은 주소와 경로명령은 ADD · DEL · CHECK · VERSION, 1.1 에서 STATUS · GC 추가쿠버네티스 노드 — 누가 누구를 부르나kubeletCRIcontainerd둘 다 containerd 가 부른다먼저runC → 방 만들기그다음CNI 플러그인 실행 파일 (bridge 등)①②③ 으로 호출Pod 의 네트워크 방 (CNI_NETNS)플러그인이 이 안에 veth · 주소 · 경로를 만든다 (호스트 쪽 설정도)생김kubelet 은 CNI 를 직접 부르지 않는다 (1.24 에서 옵션 제거) · runC 도 아니다CNM 은 도커가 안에서 관리하는 모델이고, CNI 는 런타임이 밖의 실행 파일을 부르는 약속이다 — 누가 무엇을 관리하는지가 다를 뿐 우열이 아니다
플러그인은 데몬이 아니라 호출당하는 실행 파일 — 다만 Cilium·Calico 같은 구현은 노드 에이전트를 따로 둔다

플러그인: /opt/cni/bin/ 의 실행 파일

  • 설정의 "type": "bridge" 가 그 파일 이름이다
  • 공식 저장소의 플러그인
    • 인터페이스를 만드는 bridge·macvlan·ipvlan·host-device
    • 주소를 주는 host-local·dhcp
    • 뒤에 덧붙이는 portmap·bandwidth
  • Flannel 은 bridge 와 host-local 을 그대로 쓰고 노드 사이 전달만 맡는다
  • Cilium 은 자기 플러그인을 쓴다

bridge 플러그인 한 번의 호출 흐름

호출 전 — containerd/opt/cni/bin/bridge 안호출 후runC 가 방을 만든다pause 가 붙잡는 빈 방설정 읽고 실행 파일 호출ADD · 어느 방 · eth0브리지 cni0 (가상 스위치)노드에 하나 — 없으면 만든다veth 한 쌍 → 방에 eth0다른 끝은 cni0 에주소·경로 적기IPAM 에게 주소를 받아서02-1 의 명령 아홉 줄을 프로그램이 친다결과 JSONstdout 으로 돌려주고 종료Pod 의 podIPkubelet → API 서버실행 파일은 가운데 칸 동안만 산다 — 방은 pause 가, 결과는 containerd 와 kubelet 이 들고 간다
플러그인이 하는 일은 02-1 에서 손으로 친 것과 같다 — 다른 점은 누가 부르느냐와 결과를 어디로 돌려주느냐뿐

규격 버전과 설정 파일

  • 규격 버전(1.1.0)과 런타임·플러그인의 릴리스 버전은 별개다
    • STATUS 나 GC 를 실제로 지원하는지는 따로 본다
  • 설정 파일의 모양과 체이닝은 CNI 위키에 있다

얇은 인터페이스의 범위

  • CNI 는 실행 파일을 부르는 얇은 인터페이스이지만, 그 위에 올라간 네트워크 시스템 전체가 데몬 없이 돈다는 뜻은 아니다
  • 많은 구현이 노드마다 에이전트를 두고 클러스터 컨트롤러를 함께 쓴다
  • CNI 규격만으로 NetworkPolicy, 서비스 로드밸런싱, 암호화가 저절로 생기지도 않는다

4.2 kubelet 과 런타임의 역할

CNI 를 부르는 쪽: 런타임

  • kubelet 은 CRI 로 런타임에 Pod sandbox 와 컨테이너를 만들어 달라고 요청한다
  • 일반적인 containerd 나 CRI-O 구성에서는 그 런타임이 CNI 를 호출해 Pod 네트워크를 준비한다
  • 책은 kubelet 이 직접 CNI 설정을 읽고 플러그인을 관리한다고 썼는데, 지금 구조와 다르다
    • kubelet 의 --network-plugin, --cni-bin-dir 같은 옛 옵션은 1.24 에서 제거됐다
  • 03-1 의 런타임 스택 그림에서 CRI 선 아래, OCI 선 위에 있는 containerd 가 CNI 를 부르는 쪽이다

같은 Pod 의 네트워크 공유

  • 같은 Pod 의 컨테이너들은 네트워크 네임스페이스를 공유하므로 같은 IP 와 같은 포트 공간을 쓴다
  • 서로 localhost 로 통하고, 같은 포트를 같은 방식으로 열면 충돌한다
  • 다른 Pod 사이의 통신은 네트워크 구현이 주는 경로와 정책을 따른다
  • 2.1 의 --network container: 공유와 03-1 의 4.3 이 이 이야기다

CNM 과 CNI: 관리 범위의 구분

  • CNM 과 CNI 는 어느 쪽이 낫다는 비교 대상이 아니다
  • 어떤 시스템이 무엇을 관리하는지 나누어 볼 대상이다
  • 도커 CLI 의 네트워크 설정이 곧 쿠버네티스 Pod 네트워크 설정이 아니다
  • 도커 bridge 의 주소 할당 방식이 쿠버네티스 전체에 그대로 적용되지도 않는다

4.3 대표 구현 넷 — 이름 하나로 패킷 경로를 단정하지 않는다

Cilium
eBPF 로 전달·정책·관찰. IP 가 아니라 워크로드 식별 정보로 정책을 건다. L7(HTTP) 정책은 Envoy 가 함께 관여한다.
VXLAN·Geneve 오버레이도, native routing 도 된다. eBPF 를 쓴다고 오버레이가 없는 게 아니다.
Flannel
Pod 사이 L3 연결에 집중. 노드마다 flanneld 가 서브넷을 배정받고 VXLAN 등으로 전달한다. 설정은 Kubernetes API 또는 etcd 에.
연결성만 준다. NetworkPolicy 집행은 별도 컨트롤러나 Calico·Cilium 조합이 필요하다.
Calico
Pod 네트워킹과 정책 둘 다. 라우팅은 BGP, 필요하면 IP-in-IP 나 VXLAN 캡슐화. 같은 서브넷은 직접, 넘을 때만 캡슐화하는 배치도 있다.
책의 "Calico 는 오버레이를 안 쓴다" 는 틀린 일반화다. 데이터플레인도 여럿이라 이름만으로 경로를 못 정한다.
Amazon VPC CNI
EKS 에서 Pod 에 VPC 주소를 직접 준다. 노드의 ENI 와 VPC 라우팅·보안 그룹·Flow Logs 와 함께 설계한다. 오버레이를 꼭 얹는 모델은 아니다.
오버레이가 없다고 늘 빠른 건 아니다. 인스턴스 유형, ENI 와 주소 한도, 서브넷 여유 IP 가 성능을 정한다. 보안 그룹과 NetworkPolicy 는 다른 층이다.

고르는 기준과 이 장의 목적

  • 고르는 기준은 연결 방식, 쓰는 클라우드, 정책 요구, 운영자가 볼 수 있는 정보, 커널 조건이다
  • 이 장의 목적은 특정 CNI 를 고르는 게 아니다
    • 어떤 구현이든 네임스페이스·인터페이스·주소·경로를 어떻게 구성하는지 읽을 준비를 하는 것이다
  • 5장에서 Cilium 을 노드에 직접 깔고 그 네 가지를 확인한다

5. Container Connectivity — 같은 서버, 부르는 자리 넷 [실습]

실습 구성: Go 서버 하나, 부르는 자리 넷

  • 웹 서버 하나를 컨테이너로 띄우면, 그 서버를 부르는 자리마다 이름과 주소와 포트가 달라진다
  • Go 표준 라이브러리로 짠 작은 웹 서버를 두 단계 Dockerfile(golang 에서 컴파일, scratch 에 실행 파일만)로 빌드해 13.5 MB 이미지를 만들었다
  • VM 에 사용자 정의 브리지 하나, 그 서버 하나, busybox 클라이언트 하나를 띄워 네 자리에서 두드렸다
    • 같은 브리지의 컨테이너, 도커 호스트, 다른 호스트(내 맥), 서버의 방을 공유한 컨테이너
  • 서버는 응답에 자기 호스트명과 상대 주소를 같이 적어 돌려주게 했다
    • 어느 자리에서 온 요청인지 서버 쪽에서도 보인다
서버 하나를 네 자리에서 부르는 명령 — 만들기 넷, 두드리기 넷어디서컨테이너 안이면 docker exec명령무엇을 하라대상이름·주소옵션어떻게확인보기만1이미지 빌드docker build-t cn3-go-web:study.Dockerfile 두 단계 — golang 에서 컴파일, scratch 에 실행 파일만. 결과 13.5 MB2브리지 네트워크 만들기docker network createappnet사용자 정의 bridge 네트워크 — 이름을 풀어 주는 DNS 가 붙는다. docker0 에는 없다3서버 띄우기docker run -d--name web--network appnet-p 127.0.0.1:18080:8080cn3-go-web:study-p 호스트주소:호스트포트:컨테이너포트 — 루프백 18080 → 방 안 8080. 주소를 빼면 모든 주소에 공개4클라이언트 띄우기docker run -d--name cli--network appnetbusybox sleep 600같은 브리지 네트워크에 컨테이너 하나 더. wget 이 들어 있어 두드리기용5같은 브리지에서docker exec cliwget -qO-http://web:8080/이름으로 — 127.0.0.11 이 172.19.0.2 로 풀고, 8080 으로 직접. NAT 없음6내 방에서docker exec cliwget -qO-http://localhost:8080/실패 — localhost 는 cli 자기 방의 lo. 거기엔 서버가 없다7호스트에서curl127.0.0.1:18080성공 — -p 가 적어 둔 DNAT 한 줄이 18080 을 172.19.0.2:8080 으로 바꾼다8방 공유docker run -d--network container:webbusybox→ wget localhost:8080web 의 방에 들어가 산다 — readlink ns/net 번호가 같고, localhost 가 서버다. Pod 의 모양두드리는 명령은 전부 같은 wget/curl 이다 — 다른 건 어느 방에서, 어느 이름으로 치느냐뿐
줄마다 같은 조각이다 — 어디서, 무엇을, 어느 이름으로, 어떻게
도커 호스트 (VM)appnet — 사용자 정의 브리지. 내장 DNS 127.0.0.11web (Go) — 172.19.0.2LISTEN *:8080ns 4026532266shared: localhost:8080 ✓서버가 본 상대: 127.0.0.1cli (busybox)web:8080 ✓1172.19.0.2:8080 ✓localhost:8080 ✗ns 4026532345 — 내 방, 서버 없음이름 → IPhost 127.0.0.1:18080 ✓2DNAT --dport 18080 → 172.19.0.2:8080host 127.0.0.1:8081 ✗공개한 적 없는 포트 — curl exit 7host 0.0.0.0:8080 공개 ✓3-p 0.0.0.0:8080:8080 — 모든 주소에그래서 host 127.0.0.1:8080 도 ✓서버가 본 상대: 172.19.0.1 (브리지 주소)다른 호스트 (내 맥)공인IP:8080 ✓서버가 본 상대: 집 공인 IP공인IP:18080 ✗18080 은 127.0.0.1 에만 공개172.19.0.2:8080 ✗맥에는 172.19 로 가는경로가 없다사설이라서가 아니라 경로가 없어서 — 2.3 오버레이① 같은 브리지 네트워크의 컨테이너에서 이름으로② 호스트에서 루프백 창구 18080 으로③ 모든 주소에 낸 창구 8080 으로 — 호스트·다른 호스트서버는 하나, 부르는 방마다 이름·주소·포트가 다르다 — ✗ 넷이 전부 "내 방" 과 "창구" 와 "경로" 의 문제
같은 결과를 02-1 의 말로 하면 — 방마다 포트 공간이 따로, lo 는 방마다 하나, 창구(-p)는 호스트가 적어 둔 DNAT 한 줄

5.1 이름은 사용자 정의 브리지에서만 풀린다

내장 DNS: 127.0.0.11

  • cli 안의 /etc/resolv.conf 에 nameserver 127.0.0.11 한 줄이 있다
  • 이 주소가 도커가 그 네트워크에 붙여 둔 내장 DNS 다
  • 같은 일을 기본 docker0 브리지에서 하면 wget: bad address 'web-def:8080' 로 실패하고, IP 로는 된다
  • 이름이 풀린 뒤에는 172.19.0.2 의 8080 으로 바로 가고 NAT 도 공개 포트도 거치지 않는다
  • 쿠버네티스에서 Service 이름을 CoreDNS 가 푸는 것이 같은 자리의 확장이다

리슨 주소: 방의 모든 인터페이스

  • 서버가 ss -lnt 에 *:8080 으로 보이는 것이 “방의 모든 인터페이스에서 듣는다” 는 뜻이다
  • 그래서 cli 와 호스트가 172.19.0.2:8080 으로 닿는다
    • 서버를 방 안의 127.0.0.1:8080 에만 바인딩하면 둘 다 실패한다
  • 서버가 돌려준 상대 주소가 자리마다 다른 것도 같은 이유다
    • cli 에서 오면 172.19.0.3
    • 호스트에서 오면 브리지 주소 172.19.0.1
    • 맥에서 오면 집의 공인 IP

5.2 -p 는 호스트가 적어 둔 DNAT 한 줄이다

-p 127.0.0.1:18080:8080 의 실체

  • nat 테이블의 규칙 한 줄이다
-A DOCKER -d 127.0.0.1/32 ! -i br-1eb8518464c8 -p tcp --dport 18080 -j DNAT --to-destination 172.19.0.2:8080

창구로 들어온 패킷이 방까지 가는 길

커널이 차례로 보는 것 넷 — 실습 VM 의 실제 값1curl 127.0.0.1:8080 (VM 안에서)1curl 공인IP:8080 (맥에서)둘 다 '호스트의 8080' 으로 들어온다 — lo 또는 ens3. 18080 은 lo 입구만, 뒤는 같다2-A DOCKER … --dport 8080 (18080 도 한 줄 더)-j DNAT --to 172.19.0.2:8080PREROUTING 에서 목적지를 바꾼다 — -p 가 적어 둔 줄3172.19.0.0/16 dev br-1eb8518464c8라우팅 표 — 이 대역은 브리지로. 네트워크 만들 때 생긴 줄452: veth…@if2 master br-1eb8…브리지가 MAC 표를 보고 web 의 veth 포트로답장은 conntrack 장부가 거꾸로 되돌린다 (02-2)VM 호스트lo :18080, :8080127.0.0.1ens3 :808010.0.0.198 (공인IP)1DNAT (PREROUTING)2→ 172.19.0.2:8080라우팅 → 브리지3br-appnet4vethweb 의 방 — 172.19.0.2Go 서버 LISTEN *:8080호스트 IP 계층의 입구'자기 자신에게 친다' 가 끝까지 자기에게 가는 게 아니다 — 입구에서 목적지가 컨테이너 주소로 바뀌고, 그다음은 컨테이너로 가는 패킷이다
02-2 의 부품이 한 번씩 — Netfilter(DNAT), 라우팅, 브리지, conntrack. 맥에서 오든 VM 자신이 치든 입구만 다르고 그 뒤는 같다

규칙의 범위: 호스트 루프백의 18080 만

  • 호스트 루프백의 18080 으로 온 것만 172.19.0.2:8080 으로 바꾼다
  • 공개한 적 없는 호스트의 127.0.0.1:8081 은 exit 7(연결 거부)로 끝난다
  • cli 가 서버 IP 의 18080 으로 쳐도 거부된다
  • 공개가 방 안의 리슨 포트를 바꾸는 게 아니라 호스트에 창구를 하나 내는 것이다

창구를 내는 주소에 따른 차이

  • 호스트 주소를 적지 않은 -p 0.0.0.0:8080:8080 은 모든 주소에 창구를 낸다
    • 호스트의 127.0.0.1:8080 도 되고 내 맥에서 VM 공인 IP 의 8080 으로도 닿았다
  • 루프백에만 낸 18080 은 맥에서 닿지 않았다
    • 02-2 의 외부 접속 실습과 같은 결과다

5.3 localhost 는 부르는 쪽의 방이다

cli 의 localhost:8080: 실패

  • cli 의 localhost:8080 은 실패했다
  • cli 의 방 번호는 4026532345, web 의 방 번호는 4026532266 으로 다르다
    • 127.0.0.1 은 각자의 lo 다

방을 공유한 컨테이너의 localhost: 성공

  • --network container:web 으로 띄운 컨테이너는 web 과 같은 번호 4026532266 을 받았다
  • 그 안에서 localhost:8080 이 성공했다
  • 서버가 본 상대 주소는 127.0.0.1 이었다
  • 03-1 의 4.3 에서 본 Pod 의 모양이 이것이다
    • “컨테이너에서 localhost 로 DB 에 붙었는데 안 된다” 는 장애의 답이기도 하다

내 맥에서 172.19.0.2:8080: 실패

  • 내 맥에서 172.19.0.2:8080 은 실패했다
  • 사설 주소라서가 아니라 맥에 그 대역으로 가는 경로가 없어서다
  • 겹치지 않는 주소 배정과 노드 사이 라우팅과 전달·방화벽 정책을 갖추거나 3.3 의 오버레이를 얹으면 사설 주소끼리도 통한다
  • 쿠버네티스 네트워크 모델의 “모든 Pod 가 NAT 없이 서로 닿는다” 는 요구가 바로 이 조건을 네트워크 구현에 맡긴 것이다

전체 명령과 출력 — 03-connectivity

6. Discussion

6.1 도구 지도 — 질문에서 명령으로

“연결이 안 된다” 의 다섯 가지 고장

  • “연결이 안 된다” 는 한 문장은 다섯 가지 다른 고장일 수 있다
  • 이 편의 실습이 그 다섯을 하나씩 따로 보여 줬다
방 — 어느 모드인가
호스트와 같은 방인가 → readlink /proc/PID/ns/net
문이 있나 → docker exec … ip -o -4 addr
짝은 몇 번인가 → ip -o link show eth0 의 @ifN
기본 네트워크 셋 → docker network ls
브리지 네트워크 — 이름과 주소
이름이 풀리나 → cat /etc/resolv.conf (127.0.0.11)
브리지에 뭐가 꽂혔나 → ip link show master docker0
브리지 주소는 → docker network inspect bridge
서버가 어디서 듣나 → nsenter -t PID -n ss -lnt
창구 — 호스트 밖으로
공개한 포트는 → docker port web
그 DNAT 규칙은 → iptables -t nat -S DOCKER
나갈 때 NAT 는 → iptables -t nat -S POSTROUTING
호스트가 넘겨 주나 → sysctl net.ipv4.ip_forward
꽂는 손 — CNI
어떤 플러그인인가 → ls /etc/cni/net.d/
실행 파일은 → ls /opt/cni/bin/
브리지 계열인가 → ip -d link show cni0
Pod 의 주소는 → kubectl get pod -o wide

못 닿을 때 확인 순서

  1. 서버가 0.0.0.0 에서 듣는가
  2. 부르는 쪽이 같은 방인가 다른 방인가
  3. 다른 방이면 이름이 풀리는가
  4. 호스트 밖이면 창구를 냈는가
  5. 다른 호스트면 그 대역으로 가는 경로가 있는가

문서 §14 의 다섯 항목

  • 문서 §14 의 말로는 이름 해석, 리슨 주소·포트, 로컬 네임스페이스, 호스트 전달, 원격 경로 다섯이다

핵심 메시지

모드
방을 만드느냐, 문을 어디로 내느냐. 네임스페이스 번호가 host 모드만 호스트와 같다
브리지
docker0 과 ens3 는 꽂혀 있지 않다. 사이는 호스트의 라우팅과 NAT. 이름은 사용자 정의 브리지에서만 풀린다
창구
-p 는 DNAT 한 줄. 방 안의 포트는 안 바뀌고 호스트에 창구가 생긴다. localhost 는 늘 부르는 쪽의 방
터널
호스트 둘은 저절로 안 이어진다. VXLAN 은 L2 프레임을 UDP 에 담아 나르는 터널. 사설이라서가 아니라 경로가 없어서 못 닿는다
꽂는 손
CNM 은 도커가 안에서 관리하는 이름 셋, CNI 는 containerd 가 밖의 실행 파일을 부르는 약속. 우열이 아니라 분담

02-1 에서 손으로 친 명령 아홉 줄이 도커에서는 docker run 한 번이고, 쿠버네티스에서는 containerd 가 부르는 플러그인 실행 파일 하나다. 이름이 바뀌었을 뿐 방과 파이프와 스위치는 같다. 그래서 연결이 안 될 때 볼 곳도 같다. 어느 방에서 부르는지, 이름이 풀리는지, 창구를 냈는지, 경로가 있는지. 다음 장은 그 방들이 수십 대의 노드에 흩어졌을 때 — Pod 와 Service 와 kube-proxy 가 이 편의 브리지·공개 포트·터널을 어떻게 다시 조립하는지다.


7. References

도커의 네트워크 드라이버와 기본 bridge 의 동작, 사용자 정의 브리지에서만 이름이 풀린다는 것은 도커 네트워크 문서에 있다.
"Containers on the default bridge network can only access each other by IP addresses"
[해석] "기본 bridge 네트워크의 컨테이너는 IP 주소로만 서로에게 닿는다." — 5.1 의 web-def 이름 해석 실패의 근거다. 같은 문서에 사용자 정의 네트워크의 자동 DNS 해석, 내장 DNS 가 127.0.0.11 이라는 것, host 모드에서는 -p 가 무시된다는 것이 있다.
→ docs.docker.com — Bridge network driver
→ docs.docker.com — Host network driver
→ docs.docker.com — Macvlan network driver
→ docs.docker.com — Packet filtering and firewalls
CNI 가 환경변수와 표준 입출력으로 플러그인을 부른다는 것, 명령 여섯 개(ADD, DEL, CHECK, VERSION, STATUS, GC)는 CNI 규격 1.1.0 에 있다.
"The runtime must execute the plugin with the following environment variables"
[해석] "런타임은 다음 환경변수를 주고 플러그인을 실행해야 한다." — 4.1 의 CNI_COMMAND·CNI_NETNS·CNI_IFNAME 과 호출 그림의 근거다. kubelet 의 CNI 관련 옵션 제거는 Kubernetes 1.24 변경 사항에, containerd 의 CRI 플러그인이 CNI 를 호출한다는 것은 containerd 문서에 있다.
→ containernetworking/cni — Specification
→ containernetworking/plugins — 기본 플러그인(bridge, host-local, portmap …)
→ containerd — CRI plugin config (cni)
→ kubernetes.io — Network Plugins
VXLAN 의 헤더 구성, VNI 24비트, UDP 4789 는 RFC 7348 에 있고, 도커 overlay 드라이버가 Swarm 을 전제로 한다는 것은 도커 overlay 문서에 있다.
"VXLAN is a Layer 2 overlay scheme on a Layer 3 network"
[해석] "VXLAN 은 L3 네트워크 위의 L2 오버레이 방식이다." — 3.3 의 "L2 프레임을 L3 로 실어 나르는 터널" 의 근거다. 책이 말한 외부 KV 저장소 요구는 classic Swarm 의 것이고 Docker 23.0 에서 제거됐다.
→ RFC 7348 — Virtual eXtensible Local Area Network (VXLAN)
→ docs.docker.com — Overlay network driver
→ docs.docker.com — Networking with overlay networks
쿠버네티스 네트워크 모델이 "Pod 끼리 NAT 없이 닿는다" 를 요구하고 그 구현을 네트워크 플러그인에 맡긴다는 것, 같은 Pod 의 컨테이너가 네트워크 네임스페이스를 공유한다는 것은 쿠버네티스 문서에 있다.
"pods can communicate with all other pods on any other node without NAT"
[해석] "Pod 는 어느 노드의 어떤 Pod 와도 NAT 없이 통신할 수 있다." — 5.3 의 "경로가 없어서 못 닿는 것이고, 그 경로를 네트워크 구현에 맡긴 것" 의 근거다. 대표 구현 넷의 설명은 각 프로젝트 문서를 따랐다.
→ kubernetes.io — The Kubernetes network model
→ Cilium — Introduction · Flannel · Calico — Networking · Amazon VPC CNI