컨테이너 네트워킹 (2) - 오버레이 네트워크 - soo:bak
작성일 :
멀티호스트 환경의 문제
Part 1에서 Linux Bridge와 veth 페어를 통해 같은 호스트의 컨테이너들이 통신하는 방법을 살펴보았습니다.
docker0 브리지가 호스트 내부의 컨테이너를 연결하는 가상 스위치라면, 호스트가 여러 대로 늘어났을 때는 어떻게 될까요?
실제 운영 환경에서는 컨테이너가 여러 호스트에 분산 배포됩니다. 이때 docker0 브리지는 호스트 내부에서만 동작하므로, 두 가지 문제가 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
호스트 A 호스트 B
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ docker0 (172.17.0.1) │ │ docker0 (172.17.0.1) │
│ │ │ │ │ │
│ ┌──────────┴──────────┐ │ │ ┌──────────┴──────────┐ │
│ │ │ │ │ │ │ │
│ ┌──────────┐ ┌──────────┐│ │ ┌──────────┐ ┌──────────┐│
│ │ C1 │ │ C2 ││ │ │ C3 │ │ C4 ││
│ │172.17.0.2│ │172.17.0.3││ │ │172.17.0.2│ │172.17.0.3││
│ └──────────┘ └──────────┘│ │ └──────────┘ └──────────┘│
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
│ │
│ 물리 네트워크 │
└────────────────────────────────────┘
각 호스트의 Docker 데몬은 서로의 존재를 모르고, 기본 설정에서 동일한 172.17.0.0/16 대역에서 독립적으로 IP를 할당합니다. 다이어그램에서 보듯이 C1과 C3가 모두 172.17.0.2를 받았습니다.
C1이 C3에 패킷을 보내면 어떻게 될까요? C1은 목적지 172.17.0.2가 자신과 같은 172.17.0.0/16 대역인 것을 보고, “같은 네트워크에 있는 컨테이너구나”라고 판단합니다. 같은 네트워크라면 docker0 브리지를 통해 직접 전달할 수 있으므로, C1은 브리지에 “172.17.0.2의 MAC 주소가 무엇인가?”라는 ARP 요청을 보냅니다.
하지만 C3는 호스트 B에 있습니다. docker0 브리지는 호스트 내부의 가상 스위치이므로, ARP 요청은 물리 네트워크로 나가지 않습니다. 호스트 A의 브리지에 연결된 컨테이너 중에는 응답할 대상이 없고, 패킷은 호스트 A를 벗어나지 못합니다.
IP가 충돌하고, 호스트를 넘어 통신할 경로도 없습니다.
이 문제를 해결하기 위해 오버레이 네트워크가 등장했습니다.
오버레이 네트워크 개념
docker0 브리지는 호스트 내부에 갇혀 있고, 물리 네트워크로는 컨테이너 IP를 라우팅할 수 없었습니다. 그렇다면 물리적으로 분리된 호스트의 컨테이너를 하나의 네트워크로 연결하려면 어떻게 해야 할까요?
오버레이 네트워크는 물리 네트워크 위에 가상의 네트워크를 구축하여 이 문제를 해결합니다. VPN과 터널링에서 설명한 터널링 개념과 유사한 방식입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────┐
│ 오버레이 네트워크 │
│ │
│ 컨테이너들이 같은 네트워크에 있는 것처럼 동작 │
│ │
└─────────────────────────────────────────────────────────────┘
│
캡슐화/역캡슐화
│
┌─────────────────────────────────────────────────────────────┐
│ 언더레이 네트워크 (물리) │
│ │
│ 실제 호스트 간 통신 (IP 라우팅) │
│ │
└─────────────────────────────────────────────────────────────┘
호스트 A의 컨테이너가 호스트 B의 컨테이너에 패킷을 보내면, 호스트 A의 오버레이 모듈이 원본 패킷을 물리 네트워크용 헤더로 감쌉니다(캡슐화). 물리 네트워크는 외부 헤더의 호스트 IP만 보고 패킷을 호스트 B까지 전달합니다. 호스트 B의 오버레이 모듈은 외부 헤더를 벗겨내고(역캡슐화), 원본 패킷을 목적지 컨테이너에 전달합니다.
컨테이너 입장에서는 상대방이 같은 네트워크에 있는 것처럼 보이지만, 실제로는 물리 네트워크를 통해 캡슐화된 패킷이 오가는 구조입니다.
VXLAN (Virtual Extensible LAN)
앞에서 오버레이 네트워크의 개념을 살펴봤습니다. 이 개념을 구체적으로 구현하는 프로토콜 중 가장 널리 사용되는 것이 VXLAN입니다. RFC 7348로 표준화되어 있으며, 원래의 이더넷 프레임(L2)을 UDP/IP 패킷(L3) 안에 넣어 전송합니다.
물리 네트워크는 외부 UDP/IP 헤더만 보고 라우팅하므로, 물리적으로 떨어진 호스트 간에도 하나의 L2 네트워크처럼 동작할 수 있습니다.
VXLAN 헤더 구조
VXLAN으로 캡슐화된 패킷의 구조입니다.
1
2
3
4
5
6
7
┌────────────────────────────────────────────────────────────────┐
│ 외부 이더넷 │ 외부 IP │ 외부 UDP │ VXLAN │ 내부 이더넷 │ 내부 IP │
│ 헤더 │ 헤더 │ 헤더 │ 헤더 │ 헤더 │ 헤더 │
│ (14B) │ (20B) │ (8B) │ (8B) │ (14B) │ ... │
└────────────────────────────────────────────────────────────────┘
│ │ │ │
└─ 언더레이(물리) ───┘ └── 오버레이(원본) ────┘
VXLAN 헤더를 기준으로 왼쪽과 오른쪽이 나뉩니다. 왼쪽의 외부 헤더들은 물리 네트워크가 호스트 간 라우팅에 사용하고, 오른쪽의 내부 헤더들은 컨테이너가 보낸 원본 패킷입니다. 외부 헤더가 약 50바이트 추가되므로, 그만큼 전송 효율이 떨어집니다.
VNI (VXLAN Network Identifier)
하나의 물리 네트워크 위에 여러 팀이나 서비스별로 격리된 가상 네트워크를 만들고 싶을 수 있습니다. VXLAN 헤더에 포함된 VNI라는 24비트 식별자가 이 역할을 합니다. 24비트이므로 최대 약 1,600만 개의 가상 네트워크를 구분할 수 있어, VLAN의 12비트(4,096개) 제한을 넘어섭니다.
VXLAN 동작
앞에서 “호스트의 오버레이 모듈이 캡슐화를 담당한다”고 했는데, VXLAN에서는 이 모듈을 VTEP(VXLAN Tunnel Endpoint)라고 부릅니다. 각 호스트에 하나씩 존재하며, 송신 시 컨테이너의 프레임을 VXLAN으로 캡슐화하고, 수신 시 VXLAN 헤더를 제거하여 내부 프레임을 추출합니다.
앞에서 docker0 기본 설정이 IP 충돌을 일으킨다고 했습니다. 오버레이 네트워크는 각 호스트에 겹치지 않는 서브넷을 할당하여 이 문제를 해결합니다. 아래 예시에서는 호스트 A의 컨테이너에 192.168.1.10, 호스트 B의 컨테이너에 192.168.1.20을 할당했습니다.
호스트 A의 컨테이너 C1에서 호스트 B의 컨테이너 C3로 패킷이 전달되는 과정을 따라가 봅시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
호스트 A 호스트 B
10.0.0.1 10.0.0.2
┌────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ 컨테이너 C1 │ │ 컨테이너 C3 │
│ 192.168.1.10 │ │ 192.168.1.20 │
│ │ │ │ ▲ │
│ ▼ │ │ │ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ VTEP │ │ │ │ VTEP │ │
│ │ (캡슐화) │ │ │ │ (역캡슐화) │ │
│ └────────┬────────┘ │ │ └────────┬────────┘ │
│ │ │ │ │ │
└──────────┼─────────────┘ └──────────┼─────────────┘
│ │
│ 외부 IP: 10.0.0.1 → 10.0.0.2 │
│ 내부 IP: 192.168.1.10 → 192.168.1.20
│ UDP 포트: 4789 │
└───────────────────────────────────────┘
물리 네트워크
- C1이 192.168.1.20으로 패킷을 보냅니다.
- 호스트 A의 VTEP가 이 패킷을 VXLAN으로 캡슐화합니다. 외부 헤더에는 호스트 IP(10.0.0.1 → 10.0.0.2)와 UDP 포트 4789가 붙습니다.
- 물리 네트워크는 외부 헤더의 10.0.0.2만 보고 호스트 B까지 전달합니다. 내부의 컨테이너 IP(192.168.1.x)는 물리 네트워크가 알 필요 없습니다.
- 호스트 B의 VTEP가 외부 헤더를 제거하고, 원본 패킷을 C3에 전달합니다.
오버레이 네트워크의 핵심 문제
앞의 VXLAN 동작 예시에서 VTEP가 외부 헤더에 호스트 B의 IP(10.0.0.2)를 넣었습니다. 하지만 VTEP가 192.168.1.20이 호스트 B에 있다는 것을 알아내는 과정은 생략했습니다. 또한 ARP 요청처럼 모든 대상에게 보내야 하는 패킷을 오버레이에서 처리하는 방법도 다루지 않았습니다.
이 두 가지가 오버레이 네트워크를 실제로 운영할 때 부딪히는 핵심 문제입니다.
1. 목적지 VTEP 찾기
호스트 A의 VTEP가 192.168.1.20을 목적지로 하는 패킷을 받았을 때, 이 IP가 호스트 B(10.0.0.2)에 있다는 것을 알아야 올바른 외부 IP 헤더를 만들 수 있습니다. “컨테이너 IP → 호스트 IP” 매핑을 알아내는 방법은 크게 세 가지입니다.
- 중앙 컨트롤러: etcd나 Consul 같은 분산 저장소에 매핑을 저장합니다. VTEP는 패킷을 보내기 전에 저장소를 조회하여 목적지 호스트를 알아냅니다.
- 학습: L2 스위치가 MAC 주소를 학습하는 것과 같은 원리입니다. ARP 요청을 모든 VTEP에 전달(flood)하고, 응답이 어느 VTEP에서 왔는지를 기록하여 이후 통신에 활용합니다.
- BGP: 각 호스트가 자신이 담당하는 컨테이너 IP 대역을 BGP(Border Gateway Protocol)로 광고합니다. BGP는 원래 인터넷 라우터 간 경로 교환에 사용되는 프로토콜인데, 컨테이너 네트워킹에서도 호스트 간 경로 정보를 교환하는 데 활용됩니다.
2. BUM 트래픽 처리
VTEP 매핑이 해결되어도 또 다른 문제가 남습니다. 컨테이너가 ARP 요청처럼 “모든 대상에게 보내야 하는 패킷”을 전송하는 경우입니다.
이런 트래픽을 BUM 트래픽이라고 부릅니다. Broadcast(브로드캐스트), Unknown Unicast(목적지를 모르는 유니캐스트), Multicast(멀티캐스트)의 앞글자를 딴 것으로, 공통적으로 여러 대상에게 전달해야 합니다.
물리 L2 네트워크에서는 스위치가 모든 포트로 전달하면 되지만, 오버레이 환경에서는 브로드캐스트 한 번이 수백 개 호스트로 복제될 수 있어 네트워크 부하가 급격히 증가합니다. 이를 완화하는 방법은 세 가지입니다.
- 멀티캐스트 언더레이: 물리 네트워크가 IP 멀티캐스트를 지원하면, 브로드캐스트를 멀티캐스트 그룹으로 전달합니다.
- 유니캐스트 복제: VTEP가 브로드캐스트 패킷을 모든 다른 VTEP에 유니캐스트로 하나씩 복제 전송합니다. 멀티캐스트 지원이 필요 없지만, VTEP 수가 늘어나면 복제량이 증가합니다.
- 프록시 ARP: VTEP가 자신의 매핑 테이블을 사용하여 ARP 요청에 대리 응답합니다. 브로드캐스트 자체를 억제하므로 가장 효율적입니다.
대부분의 컨테이너 네트워크 솔루션은 중앙 컨트롤러와 프록시 ARP를 조합하여 BUM 트래픽을 최소화합니다.
CNI (Container Network Interface)
오버레이 네트워크를 구현하는 방법은 Flannel, Calico, Weave 등 다양합니다. 각 솔루션은 캡슐화 방식, IP 할당, 라우팅 설정이 모두 다릅니다. 컨테이너 런타임(Docker, containerd 등)이 이 솔루션들을 각각 다른 방식으로 호출해야 한다면, 플러그인을 교체할 때마다 런타임 코드를 수정해야 합니다.
CNI(Container Network Interface)는 이 문제를 해결한 표준 인터페이스입니다. 컨테이너 네트워크를 추가할 때와 삭제할 때 어떤 형식으로 호출하고, 어떤 결과를 반환할지를 정의합니다. 런타임은 CNI 규격대로 호출하기만 하면 되고, 실제 네트워크 구성은 플러그인이 담당합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────┐
│ 컨테이너 런타임 │
│ (Docker, containerd, CRI-O) │
└──────────────────────────┬──────────────────────────────────┘
│
CNI 호출
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CNI 플러그인 │
│ (Flannel, Calico, Weave, ...) │
└─────────────────────────────────────────────────────────────┘
│
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
veth 생성 IP 할당 라우팅 설정
CNI 명령
컨테이너 런타임은 컨테이너를 생성하거나 삭제할 때 CNI 플러그인을 호출합니다. 환경 변수로 어떤 작업인지, 어떤 컨테이너인지를 알려주고, 표준 입력으로 네트워크 설정을 넘깁니다. 플러그인은 이 정보를 바탕으로 veth 페어(호스트와 컨테이너를 연결하는 가상 이더넷 쌍) 생성, IP 할당, 라우팅 설정을 수행합니다.
1
2
3
4
5
6
7
8
9
10
11
# 컨테이너 네트워크 추가
CNI_COMMAND=ADD # 수행할 작업 (ADD 또는 DEL)
CNI_CONTAINERID=abc123 # 대상 컨테이너 ID
CNI_NETNS=/var/run/netns/abc123 # 컨테이너의 네트워크 네임스페이스 경로
CNI_IFNAME=eth0 # 컨테이너 안에 만들 인터페이스 이름
/opt/cni/bin/flannel < config.json # 플러그인 실행, 설정은 표준 입력으로 전달
# 컨테이너 네트워크 삭제
CNI_COMMAND=DEL
/opt/cni/bin/flannel < config.json
플러그인 체이닝
하나의 플러그인이 모든 기능을 담당할 필요는 없습니다. CNI는 여러 플러그인을 체인으로 연결하여, 각 플러그인이 한 가지 역할만 수행하도록 조합할 수 있습니다.
1
flannel (오버레이) → portmap (포트 매핑) → bandwidth (대역폭 제한)
Flannel이 오버레이 네트워크를 구성하고, portmap이 호스트 포트를 컨테이너 포트에 매핑하고, bandwidth가 트래픽 제한을 거는 식입니다.
주요 CNI 플러그인
런타임이 CNI 규격대로 호출하면, 실제 네트워크를 어떻게 구성할지는 플러그인이 결정합니다. 같은 “컨테이너 네트워크 추가” 요청이라도 Flannel은 VXLAN 터널을 만들고, Calico는 BGP 경로를 광고합니다. 대표적인 플러그인 세 가지를 비교합니다.
Flannel
CoreOS가 개발한 Flannel은 가장 단순하고 쉽게 시작할 수 있는 오버레이 네트워크입니다. 네트워크 환경에 따라 세 가지 백엔드 중 하나를 선택합니다.
- VXLAN: 기본 옵션. L3 네트워크를 넘어 동작하므로 대부분의 환경에서 사용 가능
- host-gw: 캡슐화 없이 호스트 라우팅을 사용하여 성능이 높지만, 모든 호스트가 같은 L2 네트워크에 있어야 함
- UDP: 사용자 공간 캡슐화. 성능이 낮아 거의 사용하지 않음
설정이 단순하고 Kubernetes와 잘 통합되지만, 네트워크 정책(특정 Pod 간 트래픽을 허용/차단하는 규칙) 기능이 없습니다.
Calico
Flannel이 VXLAN 캡슐화에 의존한다면, Calico는 다른 전략을 선택합니다. 패킷을 감싸는 대신, L3 라우팅으로 직접 전달합니다.
1
2
3
4
5
6
7
8
9
10
11
12
호스트 A (192.168.1.1) 호스트 B (192.168.1.2)
┌─────────────────────┐ ┌─────────────────────┐
│ Pod: 10.244.0.10 │ │ Pod: 10.244.1.10 │
│ │ │ │ ▲ │
│ ▼ │ │ │ │
│ 라우팅 테이블 │ │ 라우팅 테이블 │
│ 10.244.1.0/24 → │ │ 10.244.0.0/24 → │
│ 192.168.1.2 │ │ 192.168.1.1 │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
│ BGP로 경로 교환 │
└──────────────────────────────────┘
호스트 A의 라우팅 테이블에 “10.244.1.0/24 대역은 192.168.1.2로 보내라”는 항목이 있습니다. Pod 10.244.0.10이 10.244.1.10에 패킷을 보내면, 호스트 A는 이 라우팅 테이블을 보고 캡슐화 없이 호스트 B로 직접 전달합니다. 캡슐화가 없으므로 50바이트 오버헤드도 없습니다.
이 라우팅 항목은 각 호스트가 BGP로 자신의 Pod 대역을 광고하여 자동으로 채워집니다. 네트워크 환경에 따라 선택적으로 VXLAN 캡슐화를 사용할 수도 있고, 네트워크 정책도 기본 지원합니다.
Weave
Flannel과 Calico가 중앙 저장소나 BGP에 의존하는 반면, Weave는 메시 네트워크 방식을 택합니다. 각 노드의 Weave 라우터가 다른 모든 노드와 자동으로 TCP 연결을 맺습니다.
1
2
3
4
호스트 A ◄──────────► 호스트 B
▲ ▲
│ │
└────► 호스트 C ◄─────┘
중앙 컨트롤러가 필요 없고, 새 노드를 추가하면 기존 메시에 자동으로 합류합니다.
데이터 전달에는 두 가지 경로가 있습니다. fast datapath는 커널의 VXLAN을 사용하여 성능이 높고, sleeve 모드는 사용자 공간에서 자체 UDP 캡슐화를 수행하여 네트워크 호환성이 높습니다. fast datapath가 기본이며, 커널이 지원하지 않는 환경에서 sleeve 모드로 전환됩니다.
NaCl(Networking and Cryptography library) 기반 암호화를 지원하며, 비밀번호를 설정하면 노드 간 트래픽이 암호화됩니다. 다만 메시 방식의 특성상 노드 수가 N개이면 최대 N(N-1)/2개의 연결이 필요합니다. 노드가 10개면 45개, 50개면 1,225개의 연결이 됩니다. 규모가 커질수록 연결 관리 부담이 늘어납니다.
성능 비교와 선택 가이드
Flannel, Calico, Weave 외에도 Cilium처럼 eBPF 기반으로 커널 수준의 고성능 네트워킹과 L7(애플리케이션 계층) 정책을 제공하는 플러그인도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────┬────────────┬──────────────┬────────────────────┐
│ 플러그인 │ 캡슐화 │ 성능 │ 네트워크 정책 │
├─────────────┼────────────┼──────────────┼────────────────────┤
│ Flannel │ VXLAN │ 중간 │ 없음 │
│ (host-gw) │ 없음 │ 높음 │ 없음 │
├─────────────┼────────────┼──────────────┼────────────────────┤
│ Calico │ 없음/VXLAN │ 높음 │ 있음 │
├─────────────┼────────────┼──────────────┼────────────────────┤
│ Weave │ VXLAN/UDP │ 중간 │ 있음 │
├─────────────┼────────────┼──────────────┼────────────────────┤
│ Cilium │ 없음/VXLAN │ 높음 │ 있음 (L7) │
└─────────────┴────────────┴──────────────┴────────────────────┘
표에서 성능 차이를 가르는 핵심은 캡슐화 여부입니다. VXLAN 헤더 구조에서 본 것처럼 캡슐화는 약 50바이트의 오버헤드와 CPU 부하를 수반합니다. 반면 캡슐화 없는 방식은 이런 오버헤드가 없는 대신, 언더레이 네트워크가 Pod IP를 직접 라우팅할 수 있어야 합니다. BGP 피어링이나 정적 라우팅 설정이 필요하다는 뜻입니다.
선택 가이드
| 상황 | 권장 플러그인 |
|---|---|
| 처음 시작하거나 단순함을 원함 | Flannel |
| 네트워크 정책이 필요함 | Calico 또는 Cilium |
| 최고 성능이 필요함 | Calico (BGP 모드) |
| L7 정책이나 가시성이 필요함 | Cilium |
| 암호화가 필요함 | Weave |
| 클라우드 환경 (AWS/GCP/Azure) | 각 클라우드의 자체 CNI(AWS VPC CNI, Azure CNI 등) 또는 Calico |
처음 시작한다면 Flannel이 적합합니다. 이후 네트워크 정책이나 성능에 대한 요구가 생기면 Calico로 마이그레이션하는 것이 일반적인 경로입니다.
마무리
이 글에서 살펴본 내용을 정리하면:
- 단일 호스트의 docker0 브리지로는 멀티호스트 통신이 불가능하다
- 오버레이 네트워크가 물리 네트워크 위에 가상 네트워크를 구축하여 이 문제를 해결한다
- VXLAN이 가장 널리 쓰이는 오버레이 프로토콜이며, L2 프레임을 UDP/IP로 캡슐화한다
- CNI가 다양한 네트워크 플러그인의 인터페이스를 표준화한다
- Flannel(단순함), Calico(성능+정책), Weave(메시+암호화)가 대표적인 플러그인이다
오버레이, 라우팅, 메시 — 접근 방식은 달라도 목표는 같습니다. 물리적으로 떨어진 컨테이너들이 하나의 네트워크에 있는 것처럼 통신하는 것입니다.
하지만 실제 Kubernetes 환경에서는 Pod IP가 수시로 바뀌고, 여러 Pod가 하나의 서비스를 구성합니다. 이 저수준 네트워킹만으로는 부족합니다. Part 3에서는 Kubernetes의 Service, kube-proxy, DNS가 이 위에 어떤 추상화를 제공하는지 다룹니다.
관련 글
시리즈
- 컨테이너 네트워킹 (1) - 컨테이너 네트워크 기초
- 컨테이너 네트워킹 (2) - 오버레이 네트워크 (현재 글)
- 컨테이너 네트워킹 (3) - Kubernetes 네트워킹