5.1 네트워크 모델
컨테이너 하나를 노출하는 일은 어렵지 않다. 문제는 호스트 한 대에서 컨테이너 수십 개가 함께 동작할 때 생긴다. 호스트 IP는 하나뿐이므로 컨테이너들은 호스트 포트를 나눠 써야 하고, 어느 컨테이너가 몇 번 포트를 차지하는지 조정하는 일이 배포 작업의 일부가 된다. 애플리케이션은 자신이 실제로 노출되는 포트를 배포 시점에야 알게 되고, 설정 파일에는 포트 매핑 표가 쌓인다. Kubernetes는 이 포트 조정 문제를 스케줄러가 풀어야 할 숙제로 두는 대신 네트워크 모델 차원에서 제거했다. 모든 Pod에 클러스터 안에서 유일한 IP를 부여하고, 그 IP로 NAT 없이 서로 통신하게 한다는 원칙이다.
IP-per-Pod 원칙
Kubernetes 네트워크 모델은 특정 구현이 아니라 요구사항 목록이다. 공식 문서가 제시하는 조건은 세 가지다.
- 모든 Pod는 클러스터 안에서 유일한 자신의 IP를 갖는다.
- 어떤 노드의 Pod든 다른 모든 노드의 모든 Pod와 NAT 없이 통신한다.
- 노드 위의 에이전트(kubelet, 시스템 데몬)는 그 노드의 모든 Pod와 통신한다.
Pod 안의 컨테이너들은 network namespace를 공유한다. 같은 Pod의 컨테이너끼리는 localhost로 통신하고, 포트 충돌도 Pod 내부에서만 조정하면 된다. 바깥에서 보면 Pod 하나가 IP를 가진 작은 가상 머신처럼 동작한다.
이 원칙이 단순화하는 것이 몇 가지 있다. 먼저 포트 매핑이 사라진다. 애플리케이션이 수신 대기하는 포트가 곧 다른 Pod가 접속하는 포트이므로, 호스트 포트와 컨테이너 포트를 잇는 변환 표를 관리할 필요가 없다. 다음으로 주소의 일관성이 생긴다. NAT가 없으므로 Pod가 스스로 인식하는 IP와 상대방이 보는 IP가 같고, 자기 주소를 등록하는 클러스터형 소프트웨어(데이터베이스 replication, 분산 코디네이터)가 VM에서 쓰던 방식 그대로 동작한다. 마지막으로 관찰이 단순해진다. 로그와 패킷 캡처에 찍히는 IP가 곧 그 Pod의 IP다.
CNI: Pod에 네트워크를 연결하는 계약
이 요구사항을 실제로 충족하는 일은 Kubernetes 본체의 몫이 아니다. Pod에 인터페이스를 만들고 IP를 할당하는 작업은 CNI(Container Network Interface) 플러그인이 담당한다. CNI는 컨테이너 런타임과 네트워크 플러그인 사이의 표준 계약으로, 런타임이 Pod의 network namespace를 만든 뒤 플러그인 바이너리를 실행하는 방식으로 동작한다. 설정 파일은 노드의 /etc/cni/net.d에, 플러그인 바이너리는 /opt/cni/bin에 있다.
계약의 중심은 두 명령이다. Pod가 생성될 때 런타임은 플러그인에 ADD를 요청하고, 플러그인은 namespace 안에 인터페이스(보통 veth 쌍)를 만들고 IPAM으로 IP를 할당하고 라우팅을 설정한 뒤 결과를 JSON으로 반환한다. Pod가 삭제될 때는 DEL이 호출되어 할당했던 자원을 반납한다.
flowchart TD
KL["kubelet: Pod 생성 지시"] --> RT["컨테이너 런타임"]
RT --> NS["network namespace 생성"]
NS --> ADD["CNI 플러그인 ADD 호출"]
ADD --> IF["veth 생성 + IP 할당"]
IF --> RES["결과 반환, Pod Running"]
DEL2["Pod 삭제"] --> DELC["CNI DEL 호출: IP 반납"]
어느 플러그인을 쓰는가에 따라 클러스터 네트워크의 성격이 달라진다. Flannel은 가장 단순한 축으로, VXLAN 오버레이로 노드 간 Pod 통신을 연결하는 데 집중하고 NetworkPolicy는 구현하지 않는다. Calico는 BGP 기반 라우팅을 중심에 두고 NetworkPolicy 집행 기능이 강하다. Cilium은 eBPF로 데이터 경로를 구성해 L7 수준의 가시성과 정책까지 확장한다.
오버레이 모드와 라우팅 모드
노드 A의 Pod가 노드 B의 Pod로 패킷을 보내려면, 그 패킷이 노드 사이의 물리(언더레이) 네트워크를 건너야 한다. 언더레이는 노드 IP만 알 뿐 Pod IP를 모르는 경우가 많고, 이 간격을 건너는 방식이 두 갈래로 나뉜다.
오버레이 모드는 Pod의 패킷을 노드 간 UDP 패킷 안에 캡슐화한다. 대표 기술이 VXLAN이다. 바깥 헤더의 출발지와 목적지는 노드 IP이므로 언더레이는 노드끼리의 통신만 지원하면 되고, Pod IP 대역을 전혀 몰라도 된다. 네트워크 장비에 아무것도 요구하지 않아 어디서나 동작하는 대신, 패킷마다 캡슐화 헤더가 붙어 오버헤드가 생기고 문제 추적 시 캡슐화를 한 겹 해제해야 한다.
라우팅 모드는 캡슐화 없이 Pod IP 대역을 실제 네트워크에 라우팅한다. Calico가 BGP로 각 노드의 Pod CIDR를 광고하는 방식이 대표적이다. 패킷이 원형 그대로 흐르므로 성능과 추적성에서 유리하지만, 언더레이가 Pod IP의 경로를 알아야 하므로 BGP를 수용하는 네트워크 환경이나 클라우드 라우팅 테이블 연동이 필요하다.
| 항목 | 오버레이 (VXLAN) | 라우팅 (BGP 등) |
|---|---|---|
| 캡슐화 | 있음 (UDP 터널) | 없음 |
| 언더레이 요구사항 | 노드 간 통신만 | Pod CIDR 경로 인지 |
| 패킷 오버헤드 | 캡슐화 헤더만큼 증가 | 없음 |
| 문제 추적 | 터널 안팎 두 층 확인 | 일반 IP 라우팅과 동일 |
많은 플러그인이 두 모드를 함께 제공하며, 같은 클러스터에서도 노드 위치에 따라 섞어 쓰는 구성이 가능하다.
네 가지 통신 경로
지금까지의 내용을 통신 경로 기준으로 정리하면 네 가지로 나뉜다. 각 경로를 담당하는 계층이 다르다.
| 경로 | 매개 | 담당 |
|---|---|---|
| 컨테이너 ↔ 컨테이너 (같은 Pod) | localhost | network namespace 공유 |
| Pod ↔ Pod | Pod IP 직접 | CNI 플러그인 |
| Pod → Service | 가상 IP (ClusterIP) | kube-proxy 규칙 |
| 외부 → Service | NodePort, LoadBalancer, Ingress | kube-proxy + 외부 장치 |
첫 두 경로가 이 챕터에서 다룬 영역이다. 셋째 경로부터는 새 문제가 시작된다. Pod IP는 Pod가 다시 만들어질 때마다 바뀌므로, 바뀌는 IP를 가려 주는 안정적인 접점이 별도로 필요하다. 그 답이 5.2의 Service이고, 외부 트래픽의 수용은 5.4의 Ingress와 Gateway API로 이어진다.
정리
- Kubernetes 네트워크 모델은 IP-per-Pod 원칙이다. 모든 Pod가 유일한 IP를 갖고 NAT 없이 통신하므로 포트 매핑 관리가 사라지고 주소가 일관된다.
- 원칙의 실제 구현은 CNI 플러그인이 담당한다. 런타임이 Pod 생성과 삭제 시점에 ADD와 DEL을 호출하는 계약이다.
- 노드 간 Pod 통신은 캡슐화하는 오버레이 모드와 Pod CIDR를 직접 광고하는 라우팅 모드로 나뉜다.