작성일 :

컨테이너는 어떻게 네트워크를 격리하는가

컨테이너 기술이 클라우드 인프라의 표준으로 자리 잡으면서, Docker와 Kubernetes는 어디서나 사용되고 있습니다.


그런데 컨테이너는 어떻게 네트워크를 격리하며, 각 컨테이너는 어떻게 독립된 IP 주소를 가질 수 있을까요?


가상화와 컨테이너의 차이

VM (가상 머신)

VM은 하드웨어 가상화 방식으로 동작합니다.


1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────┐
│                     호스트 OS                       │
├─────────────────────────────────────────────────────┤
│                    하이퍼바이저                     │
├─────────────────┬───────────────┬───────────────────┤
│      VM 1       │      VM 2     │       VM 3        │
│  ┌───────────┐  │  ┌───────────┐│  ┌───────────┐    │
│  │ 게스트 OS │  │  │ 게스트 OS ││  │ 게스트 OS │    │
│  ├───────────┤  │  ├───────────┤│  ├───────────┤    │
│  │    앱     │  │  │    앱     ││  │    앱     │    │
│  └───────────┘  │  └───────────┘│  └───────────┘    │
└─────────────────┴───────────────┴───────────────────┘


각 VM은 완전한 OS를 실행하며, 가상 하드웨어(CPU, 메모리, NIC)와 독립된 커널을 갖습니다.


네트워크 관점에서 보면, 가상 NIC가 생성되고 하이퍼바이저가 가상 스위치를 제공하여 완전히 독립된 네트워크 스택을 구성합니다.


컨테이너

반면 컨테이너는 OS 레벨 가상화 방식을 사용합니다.


1
2
3
4
5
6
7
8
9
10
┌─────────────────────────────────────────────────────┐
│                     호스트 OS                       │
│                    (커널 공유)                      │
├─────────────────┬───────────────┬───────────────────┤
│   컨테이너 1    │   컨테이너 2  │    컨테이너 3     │
│  ┌───────────┐  │  ┌───────────┐│  ┌───────────┐    │
│  │    앱     │  │  │    앱     ││  │    앱     │    │
│  │   (격리)  │  │  │   (격리)  ││  │   (격리)  │    │
│  └───────────┘  │  └───────────┘│  └───────────┘    │
└─────────────────┴───────────────┴───────────────────┘


각 컨테이너는 호스트 커널을 공유하면서도, 자신만의 파일시스템과 프로세스 트리, 그리고 격리된 네트워크 스택을 가집니다.


이러한 격리는 Linux 커널의 Namespacecgroups를 통해 구현됩니다.

이제 컨테이너 네트워크 격리의 핵심인 Network Namespace를 직접 살펴보겠습니다.


Linux Network Namespace

네임스페이스(Namespace)는 Linux 커널이 제공하는 격리 기능으로, 프로세스가 시스템 리소스의 독립된 뷰를 가질 수 있게 합니다.


Linux는 여러 종류의 네임스페이스를 지원합니다:

  • PID: 프로세스 ID 격리
  • Mount: 파일시스템 마운트 격리
  • UTS: 호스트명 격리
  • IPC: 프로세스 간 통신 격리
  • User: 사용자/그룹 ID 격리
  • Network: 네트워크 스택 격리


이 중 Network Namespace가 컨테이너 네트워킹의 핵심입니다. Network Namespace는 네트워크 인터페이스, IP 주소, 라우팅 테이블, 방화벽 규칙(iptables), 소켓, /proc/net 등 네트워크와 관련된 모든 것을 격리합니다.


네임스페이스 실험

개념을 이해했으니, 실제로 네트워크 네임스페이스를 생성하고 동작을 확인해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 'red'라는 이름의 새 네트워크 네임스페이스 생성
sudo ip netns add red

# 시스템에 존재하는 네임스페이스 목록 확인
ip netns list
# red

# red 네임스페이스 내에서 네트워크 인터페이스 확인
sudo ip netns exec red ip link
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
#    (루프백 인터페이스만 존재하며, 아직 활성화되지 않은 상태)

# 호스트의 네트워크 인터페이스와 비교
ip link
# 1: lo: ...
# 2: eth0: ...
# (호스트의 인터페이스는 red 네임스페이스에서 보이지 않음)


위 실험에서 확인할 수 있듯이, 각 네임스페이스는 완전히 독립된 네트워크 환경을 가집니다. 호스트의 eth0 인터페이스가 red 네임스페이스에서는 보이지 않습니다.


veth (Virtual Ethernet) 페어

네임스페이스가 완전히 격리되어 있다면, 서로 다른 네임스페이스 간에는 어떻게 통신할 수 있을까요?


이 문제를 해결하는 것이 veth(Virtual Ethernet)입니다. veth는 가상 이더넷 케이블로, 항상 쌍(pair)으로 생성됩니다.


1
2
3
4
5
6
7
8
┌─────────────────┐         ┌─────────────────┐
│  Namespace A    │         │  Namespace B    │
│                 │         │                 │
│   ┌─────────┐   │         │   ┌─────────┐   │
│   │ veth-a  │◄──┼─────────┼──►│ veth-b  │   │
│   └─────────┘   │  veth   │   └─────────┘   │
│                 │  pair   │                 │
└─────────────────┘         └─────────────────┘


한쪽 끝으로 들어간 패킷은 다른 쪽 끝으로 나오므로, 실제 이더넷 케이블처럼 동작합니다.


veth 페어 생성

이제 veth 페어를 생성하여 네임스페이스를 연결해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# veth 페어 생성: veth-red와 veth-red-br 두 개의 인터페이스가 쌍으로 만들어짐
sudo ip link add veth-red type veth peer name veth-red-br

# veth-red를 red 네임스페이스로 이동 (이제 호스트에서는 안 보임)
sudo ip link set veth-red netns red

# red 네임스페이스 내에서 veth-red에 IP 주소 할당 (192.168.1.10, 서브넷 /24)
sudo ip netns exec red ip addr add 192.168.1.10/24 dev veth-red

# veth-red 인터페이스를 활성화 (UP 상태로 전환)
sudo ip netns exec red ip link set veth-red up

# 루프백 인터페이스도 활성화 (localhost 통신에 필요)
sudo ip netns exec red ip link set lo up


각 명령이 수행하는 역할을 정리하면 다음과 같습니다:

명령 역할
ip link add ... type veth peer name ... 양 끝이 연결된 가상 케이블 생성
ip link set ... netns ... 인터페이스를 특정 네임스페이스로 이동
ip addr add ... 인터페이스에 IP 주소 부여
ip link set ... up 인터페이스 활성화

Linux Bridge

veth 페어로 두 네임스페이스를 연결할 수 있지만, 여러 네임스페이스(컨테이너)가 서로 통신해야 한다면 어떻게 해야 할까요?

이때 Bridge가 가상 스위치 역할을 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                    Linux Bridge (docker0)
                    ┌─────────────────────────────┐
                    │                             │
     ┌──────────────┼─────────────┬───────────────┼──────────────┐
     │              │             │               │              │
     ▼              ▼             ▼               ▼              ▼
┌─────────┐   ┌─────────┐   ┌─────────┐     ┌─────────┐   ┌─────────┐
│ veth1   │   │ veth2   │   │ veth3   │     │  eth0   │   │         │
│         │   │         │   │         │     │ (호스트)│   │         │
└────┬────┘   └────┬────┘   └────┬────┘     └─────────┘   └─────────┘
     │             │             │
     │             │             │
┌────┴────┐   ┌────┴────┐   ┌────┴────┐
│컨테이너1│   │컨테이너2│   │컨테이너3│
│ (netns) │   │ (netns) │   │ (netns) │
└─────────┘   └─────────┘   └─────────┘


Bridge 생성

브리지를 생성하고 컨테이너의 veth를 연결하는 과정은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
# docker0이라는 이름의 브리지(가상 스위치) 생성
sudo ip link add docker0 type bridge

# 브리지에 IP 할당 - 이 IP가 컨테이너들의 게이트웨이가 됨
sudo ip addr add 172.17.0.1/16 dev docker0

# 브리지 활성화
sudo ip link set docker0 up

# veth의 호스트 측 끝(veth-red-br)을 브리지에 연결
sudo ip link set veth-red-br master docker0
sudo ip link set veth-red-br up


이렇게 설정하면 브리지에 연결된 모든 컨테이너가 L2 수준에서 통신할 수 있습니다. Docker는 이 과정을 자동으로 수행합니다.


Docker 네트워크 모드

지금까지 살펴본 Network Namespace, veth, Bridge가 Docker 네트워킹의 기반 기술입니다. Docker는 이를 바탕으로 여러 네트워크 모드를 제공합니다.


bridge (기본)

가장 일반적인 모드로, 대부분의 단일 호스트 환경에서 사용됩니다.


1
docker run --network bridge nginx


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
호스트
┌─────────────────────────────────────────────────────┐
│                                                     │
│   docker0 (172.17.0.1)                              │
│   ┌─────────────────────────────────────────────┐   │
│   │                                             │   │
│   │   veth1          veth2          veth3       │   │
│   └────┬─────────────┬──────────────┬───────────┘   │
│        │             │              │               │
│   ┌────┴────┐   ┌────┴────┐   ┌────┴────┐          │
│   │nginx    │   │mysql    │   │redis    │          │
│   │172.17.0.2│   │172.17.0.3│   │172.17.0.4│          │
│   └─────────┘   └─────────┘   └─────────┘          │
│                                                     │
└─────────────────────────────────────────────────────┘


bridge 모드의 특징은 다음과 같습니다:

  • 각 컨테이너가 고유 IP를 할당받음 (172.17.x.x 대역)
  • NAT를 통해 외부와 통신
  • 포트 매핑(-p 8080:80)으로 외부에 서비스 노출


host

호스트의 네트워크 스택을 그대로 사용하며, 네트워크 격리가 없습니다.


1
docker run --network host nginx


1
2
3
4
5
6
7
8
9
10
11
호스트
┌─────────────────────────────────────────────────────┐
│                                                     │
│   eth0 (192.168.1.100)                              │
│                                                     │
│   ┌─────────────────────────────────────────────┐   │
│   │  nginx 컨테이너 (네트워크 격리 없음)        │   │
│   │  - 호스트의 포트 80을 직접 사용              │   │
│   └─────────────────────────────────────────────┘   │
│                                                     │
└─────────────────────────────────────────────────────┘


host 모드는 NAT 오버헤드가 없어 최고 성능을 제공하지만, 포트 충돌에 주의해야 합니다. 성능이 중요한 네트워크 집약적 애플리케이션에 적합합니다.


none

네트워크 연결 없이 완전히 격리된 환경이 필요할 때 사용합니다.


1
docker run --network none alpine


루프백(lo) 인터페이스만 존재하며, 보안이 중요한 배치 작업이나 외부 연결이 필요 없는 컨테이너에 적합합니다.


container

특정 컨테이너의 네트워크 네임스페이스를 다른 컨테이너와 공유합니다.


1
docker run --network container:nginx alpine


1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────┐
│          공유 네트워크 네임스페이스      │
│                                         │
│   ┌─────────────┐   ┌─────────────┐     │
│   │   nginx     │   │   alpine    │     │
│   │             │   │             │     │
│   │ 포트 80     │   │ localhost로 │     │
│   │             │   │ nginx 접근  │     │
│   └─────────────┘   └─────────────┘     │
│                                         │
│          같은 IP, 같은 포트 공간        │
└─────────────────────────────────────────┘


같은 Pod 내의 컨테이너들이 localhost로 서로 통신하는 Kubernetes의 Pod 모델이 바로 이 방식을 사용합니다.


컨테이너에서 외부로

bridge 모드에서 컨테이너의 IP(예: 172.17.0.2)는 사설 주소이므로 외부와 직접 통신할 수 없습니다. 따라서 NAT가 필요합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
컨테이너 (172.17.0.2)
         │
         │ 출발지: 172.17.0.2
         ▼
┌─────────────────┐
│    docker0      │
│    브리지       │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│    iptables     │
│    (MASQUERADE) │   ← NAT 수행
└────────┬────────┘
         │
         │ 출발지: 호스트 IP (예: 192.168.1.100)
         ▼
      외부 네트워크


Docker가 자동으로 iptables 규칙을 설정하여 MASQUERADE(출발지 NAT)를 수행합니다.

1
2
3
# Docker가 자동으로 추가하는 NAT 규칙
# 172.17.0.0/16 대역에서 나가는 패킷의 출발지를 호스트 IP로 변환
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

외부에서 컨테이너로

반대로 외부에서 컨테이너의 서비스에 접근하려면 포트 매핑이 필요합니다.


1
docker run -p 8080:80 nginx


이 명령은 호스트의 8080 포트로 들어오는 트래픽을 컨테이너의 80 포트로 전달합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
외부 요청 (호스트IP:8080)
         │
         ▼
┌─────────────────┐
│    iptables     │
│    (DNAT)       │   ← 목적지 변환
└────────┬────────┘
         │
         │ 목적지: 172.17.0.2:80
         ▼
┌─────────────────┐
│    docker0      │
└────────┬────────┘
         │
         ▼
   nginx 컨테이너


Docker가 DNAT(목적지 NAT) 규칙을 자동으로 추가합니다.

1
2
3
# Docker가 자동으로 추가하는 DNAT 규칙
# 8080 포트로 들어오는 TCP 패킷의 목적지를 컨테이너 IP:포트로 변환
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80

정리

지금까지 컨테이너 네트워킹의 기본 구성요소를 살펴보았습니다.


  1. Network Namespace: 네트워크 스택 격리
  2. veth pair: 네임스페이스 간 연결
  3. Linux Bridge: 가상 스위치
  4. iptables NAT: 외부 통신


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────┐
│                         호스트                              │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              Linux Bridge (docker0)                 │   │
│   └───────────────────────┬─────────────────────────────┘   │
│                           │                                 │
│         ┌─────────────────┼─────────────────┐               │
│         │                 │                 │               │
│    ┌────┴────┐       ┌────┴────┐       ┌────┴────┐          │
│    │  veth   │       │  veth   │       │  veth   │          │
│    └────┬────┘       └────┬────┘       └────┬────┘          │
│         │                 │                 │               │
│  ┌──────┴──────┐  ┌──────┴──────┐  ┌──────┴──────┐         │
│  │ Container   │  │ Container   │  │ Container   │         │
│  │ (netns 1)   │  │ (netns 2)   │  │ (netns 3)   │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

이 기반 기술들이 모여 단일 호스트에서 컨테이너 네트워킹이 동작합니다. 그렇다면 여러 호스트에 분산된 컨테이너들은 어떻게 통신할까요?

Part 2에서는 여러 호스트에 걸친 컨테이너 통신을 위한 오버레이 네트워크를 살펴봅니다.


관련 글

Tags: Bridge, Docker, Namespace, veth, 네트워크, 컨테이너

Categories: ,