[CS] 컨테이너 네트워킹 기본 원리: 네임스페이스 네트워킹
컨테이너 네트워킹은 결국 리눅스 네트워크 네임스페이스 위에 만들어진다. Docker의 docker0 bridge, Kubernetes의 cni0 bridge, Pod의 eth0 — 이 모든 것이 네임스페이스, veth pair, bridge, 라우팅, NAT라는 동일한 building block 위에 서 있다. 이번 글에서는 이 building block을 하나씩 직접 구성하며, 컨테이너 네트워킹의 기본 원리를 이해한다.
TL;DR
- 네임스페이스 격리:
ip netns add로 격리된 네트워크 공간을 만든다. 각 네임스페이스는 독립된 인터페이스, ARP, 라우팅 테이블을 가진다. - veth pair (1:1 연결): 가상 케이블로 두 네임스페이스를 직접 연결한다. 네임스페이스가 많아지면 O(N²) 연결이 필요해 확장성이 없다.
- bridge (N:N 연결): 가상 스위치(bridge)를 두고, 각 네임스페이스의 veth를 bridge에 연결한다. N개의 네임스페이스가 bridge 하나로 통신할 수 있다.
- 호스트 ↔ 네임스페이스: bridge에 IP를 부여하면 호스트도 네임스페이스 네트워크에 참여한다.
- 외부 통신: 라우팅 + NAT(MASQUERADE)로 outbound, 포트포워딩(DNAT)으로 inbound 트래픽을 처리한다.
실습 환경
TODO: 실습 환경 프로비저닝 방법 정리. VM, Vagrant, multipass 등 사용할 환경에 맞게 작성할 것.
실습에는 리눅스 호스트가 필요하다. 네트워크 네임스페이스(ip netns)를 사용하므로 root 권한이 필요하다.
필요한 도구:
iproute2(ip명령어)iptablesping(연결 확인용)
# 도구 확인
ip -V
iptables -V
실습이 끝난 후 정리:
ip netns del red ip netns del blue ip link del v-net-0 iptables -t nat -F
네임스페이스 생성 및 격리 확인
네트워크 네임스페이스는 프로세스에게 독립된 네트워크 스택(인터페이스, ARP 테이블, 라우팅 테이블, iptables 규칙)을 제공한다. 컨테이너가 각자의 eth0을 가질 수 있는 이유다.
ip netns add red
ip netns add blue
ip netns
# red
# blue
네임스페이스 안에서 인터페이스를 확인하면 loopback만 보인다. 호스트의 eth0 등은 보이지 않는다:
ip netns exec red ip link
# 1: lo: <LOOPBACK> ...
ip -n red link
# 동일한 결과 (축약 문법)
ARP 테이블, 라우팅 테이블도 호스트와 완전히 별개다:
ip netns exec red arp
ip netns exec red route
네임스페이스 간 연결
격리된 네임스페이스끼리 통신하려면 가상 케이블(veth pair)이 필요하다. 연결 방식은 두 가지다:
- veth pair 직접 연결 (1:1): 두 네임스페이스를 케이블로 직접 연결
- bridge 경유 (N:N): 가상 스위치(bridge)를 두고 각 네임스페이스를 연결
어느 방식이든 공통 절차는 동일하다:
- 격리된 공간을 만든다:
ip netns add - 가상 케이블을 만든다:
ip link add ... type veth peer ... - 케이블 끝을 원하는 곳에 꽂는다:
ip link set ... netns ...또는ip link set ... master ... - IP 주소를 붙인다:
ip addr add - 인터페이스를 활성화한다:
ip link set ... up
veth pair로 1:1 직접 연결
veth pair는 양 끝이 있는 가상 케이블이다. 한쪽에 패킷을 넣으면 다른 쪽으로 나온다.
가상 케이블 생성
ip link add veth-red type veth peer name veth-blue
두 개의 가상 인터페이스(veth-red, veth-blue)가 한 쌍으로 생성된다.
각 인터페이스를 네임스페이스에 연결
ip link set veth-red netns red
ip link set veth-blue netns blue
각 끝을 해당 네임스페이스에 꽂는다. 물리 케이블의 한쪽을 기기에 꽂는 것과 같다.
IP 주소 할당
ip -n red addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
인터페이스 활성화
ip -n red link set veth-red up
ip -n blue link set veth-blue up
확인
통신 확인:
ip netns exec red ping 192.168.15.2
ARP 테이블 확인 — 각 네임스페이스에서 상대방의 MAC 주소가 등록되어 있다:
ip netns exec red arp
# 192.168.15.2 → blue의 MAC 주소
ip netns exec blue arp
# 192.168.15.1 → red의 MAC 주소
호스트의 ARP 테이블에는 네임스페이스 내부의 veth IP가 보이지 않는다. 네임스페이스가 격리된 것이다:
arp
bridge 경유 N:N 연결
veth pair의 1:1 직접 연결은 네임스페이스가 많아질수록 O(N²) 연결이 필요하다. 가상 스위치(bridge)를 두면 N개의 네임스페이스가 하나의 bridge를 통해 통신할 수 있다.
기존 veth pair 삭제
veth pair는 한쪽을 삭제하면 다른 쪽도 자동 삭제된다:
ip -n red link del veth-red
bridge 인터페이스 생성
ip link add v-net-0 type bridge
호스트 입장에서 bridge는 eth0 같은 또 다른 네트워크 인터페이스일 뿐이다:
ip link
# ...
# 6: v-net-0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop ...
생성 직후 인터페이스는 DOWN 상태다. 활성화해야 한다:
ip link set dev v-net-0 up
가상 케이블 생성
이번에는 veth pair의 한쪽 이름에 -br 접미사를 붙여 bridge에 연결될 쪽임을 나타낸다:
ip link add veth-red type veth peer name veth-red-br
ip link add veth-blue type veth peer name veth-blue-br
네임스페이스 및 bridge에 연결
1:1 직접 연결에서는 veth 양쪽을 각각의 네임스페이스에 넣었지만, 이번에는 한쪽을 네임스페이스에, 다른 쪽을 bridge에 연결한다.
ip link set veth-red netns red
ip link set veth-red-br master v-net-0
ip link set veth-blue netns blue
ip link set veth-blue-br master v-net-0
-br접미사 네이밍 컨벤션으로 어떤 네임스페이스와 관련된 bridge 쪽 인터페이스인지 알 수 있다.
IP 주소 할당
각 네임스페이스 쪽 veth에 IP를 할당한다. bridge 인터페이스는 스위치 역할만 하면 되니 IP를 할당하지 않는다:
ip -n red addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
인터페이스 활성화
ip -n red link set veth-red up
ip -n blue link set veth-blue up
이제 모든 네임스페이스가 가상 스위치(bridge)에 연결되었고, 서로 통신할 수 있다.
호스트 ↔ 네임스페이스 연결
문제
호스트와 네임스페이스의 네트워크 스택은 분리되어 있다. 호스트에서 네임스페이스 내부 IP(192.168.15.x)로 ping해도 도달할 수 없다.
해결: bridge에 IP 부여
ip addr add 192.168.15.5/24 dev v-net-0
IP를 부여하는 것만으로 되는 이유는 L2 연결이 이미 갖춰져 있기 때문이다:
- bridge(
v-net-0)는 이미 UP 상태 - 네임스페이스들의 veth pair가 이미 bridge에 연결됨
- 네임스페이스 안의 veth에 IP가 이미 할당됨
IP 부여가 트리거하는 일:
- 호스트 커널이 bridge를 자기 인터페이스 중 하나로 인식한다 (마치
eth0처럼) - 커널이 자동으로 라우팅 테이블에 경로를 추가한다:
192.168.15.0/24 dev v-net-0 - 호스트가
192.168.15.x로 패킷을 보내면, 커널이 bridge로 전달하고, bridge가 L2 스위칭으로 올바른 veth에 전달한다
비유하자면, bridge는 이미 네임스페이스들에 물리적으로 꽂혀 있는 스위치다. IP를 주기 전에는 호스트가 이 스위치에 연결되지 않은 상태였고, IP를 주는 순간 호스트도 이 스위치에 랜선을 꽂은 것이다.
확인
ping 192.168.15.1 # red 네임스페이스
ping 192.168.15.2 # blue 네임스페이스
ip route
# 192.168.15.0/24 dev v-net-0 proto kernel scope link src 192.168.15.5
네임스페이스 ↔ 외부 네트워크
여태까지의 네트워크는 호스트 내부에 격리되어 있다. 외부 세계로 통하는 유일한 관문은 호스트의 ethernet port(eth0)다.
네임스페이스 → 외부 (Outbound)
1단계: 라우팅 추가
네임스페이스에서 LAN(192.168.1.0/24)으로 ping하면 Network is unreachable:
ip netns exec blue ping 192.168.1.3 # unreachable
ip netns exec blue route # 외부 경로 없음
bridge IP(192.168.15.5)를 게이트웨이로 지정하는 라우팅을 추가한다:
ip netns exec blue ip route add 192.168.1.0/24 via 192.168.15.5
호스트는 두 네트워크 모두에 발을 걸치고 있으므로(192.168.15.5 + 192.168.1.x) 패킷을 전달할 수 있다.
2단계: NAT 설정
라우팅을 추가해서 unreachable은 해결됐지만, 응답이 돌아오지 않는다. 패킷은 나가지만, 외부 호스트가 192.168.15.x(private IP)로 응답을 보낼 방법이 없기 때문이다.
호스트에서 MASQUERADE(SNAT) 설정으로 나가는 패킷의 출발지 IP를 호스트 IP로 변환한다:
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
3단계: 디폴트 게이트웨이 추가
LAN 대역은 통신되지만, 인터넷(예: 8.8.8.8)은 여전히 unreachable이다:
ip netns exec blue ping 8.8.8.8 # unreachable
ip netns exec blue route
# 192.168.15.0 ← 자기 네트워크
# 192.168.1.0 ← 1단계에서 추가한 LAN 대역
# → 그 외 대역에 대한 경로가 없음
디폴트 게이트웨이를 추가하여 모든 알 수 없는 대역을 bridge를 통해 호스트로 보낸다:
ip netns exec blue ip route add default via 192.168.15.5
외부 → 네임스페이스 (Inbound)
방법 1: 외부 호스트에 라우팅 추가
192.168.15.0/24는 192.168.1.2를 통하라고 외부 호스트에 설정한다. 모든 외부 호스트에 일일이 설정해야 하므로 확장성이 없다.
방법 2: 포트포워딩 (DNAT)
호스트의 특정 포트로 들어오는 트래픽을 네임스페이스로 전달한다:
iptables -t nat -A PREROUTING -p tcp --dport 80 --to-destination 192.168.15.2:80 -j DNAT
패킷 흐름:
- 외부 클라이언트가
192.168.1.2:80으로 요청 - 패킷이 호스트의
eth0에 도착 - PREROUTING 체인에서 DNAT 적용: 목적지를
192.168.1.2:80→192.168.15.2:80으로 변경 - 호스트가
192.168.15.2를 보고 bridge를 통해 blue 네임스페이스로 전달 - blue 네임스페이스의 프로세스가 응답
- 응답 시 커널의 conntrack이 자동으로 출발지를
192.168.15.2→192.168.1.2로 되돌림
외부에서는 호스트 IP만 알면 된다. 네임스페이스 내부 IP를 노출할 필요 없다. 단, 호스트 IP + 매핑된 포트는 알아야 한다. Docker의 -p 80:80 포트 매핑이 바로 이 원리다.
수동 DNAT의 한계
위 예시는 포트 하나(80)에 대한 매핑일 뿐이다. 컨테이너가 많아지면 호스트 포트를 수동으로 매핑해야 하고, 포트 충돌도 수동으로 관리해야 한다.
이 섹션은 컨테이너 네트워킹의 building block을 보여주는 것일 뿐이다. Docker, Kubernetes Service, Ingress가 이 한계를 자동화하고 확장한다:
- Kubernetes Service (ClusterIP): 포트가 아니라 가상 IP + DNS 이름으로 접근. kube-proxy가 iptables 규칙을 자동 관리
- Kubernetes NodePort: 모든 노드에서 같은 포트(30000~32767)로 접근 가능하도록 자동 설정
- Ingress: 80/443 하나로 들어와서 도메인/경로 기반으로 여러 서비스에 라우팅
정리
| 요소 | 역할 | 없으면? |
|---|---|---|
| Namespace | 격리된 네트워크 공간 | 격리할 대상이 없음 |
| veth pair | 격리된 공간을 연결하는 가상 케이블 | 패킷이 네임스페이스 밖으로 못 나감 |
| Bridge | N:N 내부 통신을 위한 가상 스위치 | 1:1로만 연결 가능 |
| Routing + ip_forward | 외부로의 경로 | 호스트 내부에 갇힘 |
| iptables/NAT | 주소 변환 | 패킷은 나가지만 응답이 안 돌아옴 |
각 요소를 좀 더 풀어 보면:
veth pair — 양 끝이 있는 가상 케이블. 격리된 네임스페이스를 외부와 물리적으로 잇는 유일한 수단이다.
ip link add veth-red type veth peer name veth-red-br로 두 개의 가상 인터페이스가 한 쌍으로 생성된다- 한쪽에 패킷을 넣으면 다른 쪽으로 나온다. 물리 케이블과 동일한 동작
- 1:1 직접 연결: 양쪽을 각각의 네임스페이스에 넣는다
- bridge 경유: 한쪽을 네임스페이스에, 다른 쪽을 bridge에 연결한다 (
ip link set ... master v-net-0)
bridge — 이중 역할을 할 수 있다:
- L2 스위치 역할: 네임스페이스끼리 통신 → IP 없이도 된다
- L3 게이트웨이 역할: 호스트가 네임스페이스에 접근 → IP가 필요하다
routing + ip_forward — 외부로 나가기 위한 경로 결정:
- 네임스페이스에 게이트웨이 라우팅 추가 (
ip route add ... via) - 호스트에서
ip_forward활성화 (패킷 전달 허용) - 이게 없으면 패킷이 bridge에서 멈춘다
iptables/NAT — 주소 변환. 라우팅만으로는 응답이 돌아오지 않는다 (private IP 문제):
- SNAT/MASQUERADE: 나가는 패킷의 출발지를 호스트 IP로 변환 (outbound)
- DNAT/포트포워딩: 들어오는 패킷의 목적지를 네임스페이스 IP로 변환 (inbound)
참고 링크
TODO: 참고 자료 링크 추가
댓글남기기