해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.
Day 63 - Diving into Container Network Namespaces
우리는 쿠버네티스나 도커를 사용할 때 컨테이너 네트워킹을 당연하게 여긴다.
하지만 자동화의 껍질을 벗겨보면, 그 안에는 리눅스 커널의 격리 기술인 Network Namespace와 가상 인터페이스들이 복잡하게 얽혀 있다.
해당 프레젠테이션에서는 CNI(Container Network Interface) 플러그인이 자동으로 수행해 주는 과정을 리눅스 기본 명령어(ip, iptables)만을 사용하여 직접 구축하는 과정을 설명한다.

구축 목표 아키텍처
Local Host: 호스트 리눅스 머신
Network Namespaces: webapp, sleep (두 개의 격리된 컨테이너 환경)
Bridge: app-net-0 (가상 스위치 역할)
Veth Pairs: 네임스페이스와 브릿지를 연결하는 가상 케이블
IP 대역: 192.168.52.0/24
❓ 왜 이런 아키텍처가 필요한가?
단순히 프로세스를 실행하는 것과 달리, 컨테이너 환경에서 위와 같은 복잡한 네트워크 구조를 갖추는 이유는 격리와 통신의 균형을 맞추기 위해서다.
독립된 네트워크 공간 (Namespace):
가상 연결 통로 (Veth Pair):
격리만 되어 있으면 통신이 불가능하다.
랜선을 꽂듯 호스트와 컨테이너를 물리적으로 연결할 수 없으므로, 리눅스 커널이 제공하는 가상의 파이프인 Veth Pair를 통해 네임스페이스 밖으로 통로를 만들어주어야 한다.
중앙 스위치 (Bridge):
여러 컨테이너가 서로 통신하려면 이 가상 케이블들을 하나로 묶어줄 허브가 필요하다.
Bridge가 바로 그 역할을 하며, 같은 네트워크 대역 (L2) 안에서 패킷을 전달해준다.
외부 세상과의 소통 (NAT/Masquerading):
컨테이너가 가진 사설 IP(192.168.52.x)는 호스트 밖의 인터넷 세상은 모르는 주소다.
따라서 외부로 나갈 때는 호스트의 공인 IP로 위장(Masquerading)하여 나가는 NAT 설정이 필수적이다.
실습은 리눅스 환경에서 진행하며 root 권한이 필요하다.
우선 네트워크 도구가 설치되어 있는지 확인한다
# net-tools 설치 확인 (ifconfig, netstat 등)
apt-get install net-tools -y # 필요 시 설치
# 현재 라우팅 테이블 확인
ip route
가장 먼저 webapp과 sleep이라는 이름의 네트워크 네임스페이스를 생성한다.
이는 도커 컨테이너를 생성하는 것과 같지만, 아직 네트워크 인터페이스는 없는 상태다.
# 네임스페이스 생성
ip netns add webapp
ip netns add sleep
# 생성된 네임스페이스 확인
ip netns list
이 상태에서 webapp 내부의 인터페이스를 확인해보면, 로컬 루프백(lo)만 존재하며 외부와 단절되어 있음을 알 수 있다.
# webapp 네임스페이스 내부에서 ip link 명령어 실행
ip netns exec webapp ip link
# 결과: LOOPBACK 인터페이스만 보임 (DOWN 상태)
두 네임스페이스를 연결해 줄 가상 스위치(Bridge)를 생성한다. 이미지에 따라 이름은 app-net-0로 지정한다.
# 브릿지 타입의 인터페이스 생성
ip link add app-net-0 type bridge
# 브릿지 인터페이스 활성화 (UP)
ip link set dev app-net-0 up
# 확인
ip link show app-net-0
해당 브릿지는 물리적 L2 스위치와 같은 역할을 하며, 추후 연결될 인터페이스들의 MAC 주소를 학습하여 트래픽을 스위칭한다.
이제 가상 랜선 역할을 하는 Veth (Virtual Ethernet) 쌍을 생성해야 한다.
Veth는 항상 Pair로 생성되며, 한쪽은 네임스페이스에, 다른 한쪽은 브릿지에 연결된다.
# Veth 쌍 생성
# veth-webapp: 네임스페이스 쪽 인터페이스
# veth-webapp-br: 브릿지 쪽 인터페이스
ip link add veth-webapp type veth peer name veth-webapp-br
# veth-webapp을 webapp 네임스페이스 안으로 이동
ip link set veth-webapp netns webapp
# veth-webapp-br을 브릿지(app-net-0)에 연결 (Master 설정)
ip link set veth-webapp-br master app-net-0
# Veth 쌍 생성
# veth-sleep: 네임스페이스 쪽 인터페이스
# veth-sleep-br: 브릿지 쪽 인터페이스
ip link add veth-sleep type veth peer name veth-sleep-br
# veth-sleep을 sleep 네임스페이스 안으로 이동
ip link set veth-sleep netns sleep
# veth-sleep-br을 브릿지(app-net-0)에 연결
ip link set veth-sleep-br master app-net-0
이제 물리적인 연결은 끝났다. 통신을 위해 각 인터페이스에 IP 주소를 할당하고 인터페이스를 활성화(UP)해야 한다.
2-5-1. 네임스페이스 내부 인터페이스 IP 할당
이미지 아키텍처에 맞춰 sleep은 192.168.52.1/24, webapp은 192.168.52.2/24를 할당한다.
# Sleep 네임스페이스: 192.168.52.1 할당 및 활성화
ip netns exec sleep ip addr add 192.168.52.1/24 dev veth-sleep
ip netns exec sleep ip link set veth-sleep up
# Webapp 네임스페이스: 192.168.52.2 할당 및 활성화
ip netns exec webapp ip addr add 192.168.52.2/24 dev veth-webapp
ip netns exec webapp ip link set veth-webapp up
2-5-2. 브릿지 IP 할당 (Gateway 역할)
브릿지 자체에도 IP를 할당해야 한다.
이 IP(192.168.52.5)는 각 네임스페이스들이 외부로 나갈 때 사용하는 Default Gateway가 된다.
# 브릿지에 Gateway IP 할당
ip addr add 192.168.52.5/24 dev app-net-0
2-5-3. 브릿지 측 Veth 인터페이스 활성화
물리 스위치에 케이블을 꽂아도 포트가 shutdown 상태면 통신이 안 되는 것처럼, 브릿지 쪽에 연결된 veth 인터페이스들도 켜줘야 한다.
ip link set dev veth-webapp-br up
ip link set dev veth-sleep-br up
이제 webapp에서 sleep으로 핑을 보내면 네임스페이스 간 통신은 성공한다.
ip netns exec webapp ping 192.168.52.1
# 성공! (L2 통신)
내부 통신은 되지만, 네임스페이스 안에서 외부(예: 8.8.8.8)로 핑을 보내면 실패한다.
이를 해결하기 위해 라우팅 테이블과 NAT 설정이 필요하다.
2-6-1. Default Route 추가
각 네임스페이스는 모르는 목적지로 갈 때 누구에게 패킷을 보내야 할지 모른다.
브릿지(192.168.52.5)를 기본 게이트웨이로 설정한다.
# Webapp에 기본 경로 추가
ip netns exec webapp ip route add default via 192.168.52.5
# Sleep에 기본 경로 추가
ip netns exec sleep ip route add default via 192.168.52.5
2-6-2. IP Forwarding 활성화
리눅스 커널은 기본적으로 보안상 패킷 포워딩 (라우팅) 기능이 비활성화되어 있다.
호스트가 라우터 역할을 할 수 있도록 이를 켠다.
sysctl -w net.ipv4.ip_forward=1
2-6-3. IPTables NAT (Masquerading) 설정
네임스페이스의 사설 IP(192.168.52.x)는 인터넷 세상에서 알 수 없는 주소다.
따라서 패킷이 호스트 밖으로 나갈 때, 호스트의 물리 인터페이스 IP로 변환(NAT)해서 나가도록 설정해야 한다.
# 192.168.52.0/24 대역에서 출발하는 패킷은
# 호스트의 외부 인터페이스를 통해 나갈 때 IP를 변조(Masquerade)한다.
iptables -t nat -A POSTROUTING -s 192.168.52.0/24 -j MASQUERADE
이제 모든 설정이 완료되었다. 네임스페이스 내부에서 외부로 핑을 보내본다.
# Webapp에서 구글 DNS로 핑 테스트
ip netns exec webapp ping 8.8.8.8
정상적으로 응답이 온다면, 도커나 쿠버네티스 없이 리눅스 커널 기능만으로 완벽하게 작동하는 컨테이너 네트워크를 구축한 것이다.
해당 실습에서 리눅스 기본 명령어만을 사용하여 컨테이너 네트워크를 바닥부터 구축해 보았다.
해당 과정은 다음과 같이 요약할 수 있다.
Namespace 생성 (격리)
Bridge 및 Veth 생성 (연결)
IP 할당 및 Routing 설정 (주소 부여)
IPTables NAT 설정 (외부 통신)
해당 과정이 바로 우리가 쿠버네티스를 사용할 때 CNI (Container Network Interface) 플러그인이 자동으로 수행해 주는 작업의 실체다.
CNI(Container Network Interface)의 역할
쿠버네티스 환경에서 Pod 하나를 생성할 때마다 운영자가 위 명령어들을 일일이 칠 수는 없다.
대규모 클러스터에서는 수백, 수천 개의 파드가 수시로 생성되고 삭제되기 때문이다.
이때 등장하는 것이 CNI다.
쿠버네티스의 Kubelet은 파드를 생성할 때 CNI 표준 규격에 맞춰 네트워크 플러그인 (예: Calico, Flannel, AWS VPC CNI 등)을 호출한다.
자동화: CNI 플러그인은 실습했던 ip link add, ip netns, iptables 등의 명령어를 (혹은 그에 상응하는 API를) 사용하여 눈깜짝할 사이에 네트워크를 구성한다.
IPAM (IP Address Management): 실습에서 수동으로 192.168.52.1, .2를 지정했던 것과 달리, CNI는 남는 IP를 자동으로 계산하여 파드에 할당하고 관리한다.
표준화: CNI라는 공통된 인터페이스 덕분에, 하부 네트워크 구현체가 바뀌더라도(Flannel → Calico) 쿠버네티스 설정을 크게 바꾸지 않고 네트워크를 갈아끼울 수 있다.
결국, 파드 네트워크가 안되는 에러를 마주했을 때, 해당 하부 구조(Namespace, Veth, Bridge, IPTables)를 이해하고 있다면 트러블슈팅의 깊이가 달라질 것이다.
결국, 컨테이너 네트워킹은 사실 정교하게 조립된 리눅스 커널 기능의 집합일 뿐이다.