Network Namespace를 통해 하나의 호스트에서 여러 네트워크를 이용하는 법을 이해했으니, 실질적으로 도커는 어떻게 네트워크를 구성하고 컨테이너 간 통신을 허용하는지 그 방식엔 무엇이 있는지 알아볼 차례입니다.


이전 글 목차
https://dev-whoan.xyz/111, 네트워크로 시작하는 쿠버네티스 — 내가 데이터를 보낸다면
https://dev-whoan.xyz/112, 네트워크로 시작하는 쿠버네티스 — 컨테이너 통신, Network Namespace


사전 준비물
Ubuntu 22.04 시스템 * 1


Bridge 네트워크 살펴보기

현재 시스템은 도커가 설치되지 않은 상태로, 다음과 같은 네트워크 구성을 확인할 수 있습니다.

docker@docker:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 3390sec preferred_lft 3390sec
    inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591937sec preferred_lft 604737sec
    inet6 fe80::489e:7eff:fe55:59ee/64 scope link
       valid_lft forever preferred_lft forever
docker@docker:~$ ip netns list
docker@docker:~$ docker
Command 'docker' not found, but can be installed with:
sudo snap install docker         # version 24.0.5, or
sudo apt  install podman-docker  # version 3.4.4+ds1-1ubuntu1.22.04.2
sudo apt  install docker.io      # version 24.0.7-0ubuntu2~22.04.1
See 'snap info docker' for additional versions.
docker@docker:~$

시스템에 도커를 설치하면 네트워크가 어떻게 구성되는지 한번 확인해 보겠습니다. 저는 다음의 명령어를 이용해 설치했습니다.

docker@docker:~$ curl -sSL <https://get.docker.com> | bash
...
Client: Docker Engine - Community
 Version:           27.3.1
 API version:       1.47
 Go version:        go1.22.7
 Git commit:        ce12230
 Built:             Fri Sep 20 11:41:08 2024
 OS/Arch:           linux/arm64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          27.3.1
  API version:      1.47 (minimum version 1.24)
  Go version:       go1.22.7
  Git commit:       41ca978
  Built:            Fri Sep 20 11:41:08 2024
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.7.23
  GitCommit:        57f17b0a6295a39009d861b89e3b3b87b005ca27
 runc:
  Version:          1.1.14
  GitCommit:        v1.1.14-0-g2c9f560
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

================================================================================

To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:

    dockerd-rootless-setuptool.sh install

Visit <https://docs.docker.com/go/rootless/> to learn about rootless mode.

To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to <https://docs.docker.com/go/daemon-access/>

WARNING: Access to the remote API on a privileged Docker daemon is equivalent
         to root access on the host. Refer to the 'Docker daemon attack surface'
         documentation for details: <https://docs.docker.com/go/attack-surface/>

================================================================================

docker@docker:~$ ip a
1: lo: <loopback,up,lower_up> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <broadcast,multicast,up,lower_up> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 3260sec preferred_lft 3260sec
    inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591997sec preferred_lft 604797sec
    inet6 fe80::489e:7eff:fe55:59ee/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <no-carrier,broadcast,multicast,up> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:c0:d7:ec:48 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
       
docker@docker:~$ ip netns list
docker@docker:~$ ip -d link show docker 0
3: docker0: <no-carrier,broadcast,multicast,up> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:c0:d7:ec:48 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 
    vlan_protocol 802.1Q bridge_id 8000.2:42:c0:d7:ec:48 designated_root 8000.2:42:c0:d7:ec:48 root_port 0 root_path_cost 0
    topology_change 0 topology_change_detected 0 hello_timer    0.00 tcn_timer    0.00 topology_change_timer    0.00 gc_timer
    191.28 vlan_default_pvid 1 vlan_stats_enabled 0 vlan_stats_per_port 0 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1
    mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 16 mcast_hash_max 4096 mcast_last_member_count 2
    mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval
    12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3124 mcast_stats_enabled 0 mcast_igmp_version 2 mcast_mld_version 1
    nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
</no-carrier,broadcast,multicast,up></no-carrier,broadcast,multicast,up></broadcast,multicast,up,lower_up></loopback,up,lower_up>

설치된 docker0 네트워크를 확인해보니 bridge 형태의 네트워크인 것을 확인할 수 있습니다. 도커는 설치될 때 docker0이라는 이름의 bridge 네트워크를 기본으로 생성하는데, 그 이유는 컨테이너의 통신이 원활하게 이루어질 수 있게 하기 위함입니다.

  • 이 부분이 이해가 안되신다면, https://dev-whoan.xyz/112 편을 참고하셔서 왜 그럴지 이유를 생각해 보시기 바랍니다.
  • 도커를 설치했을 때 생성되는 네트워크 목록은 다음과 같이 확인할 수 있습니다.
docker@docker:~$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
0ddc27e0481c   bridge    bridge    local
c4c67c5ac924   host      host      local
102a26b1c427   none      null      local

## bridge 네트워크를 확인해 봅시다.
docker@docker:~$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "0ddc27e0481c8189d5d4427bfc22d34705263b847f4df3dfe34cb39070273757",
        "Created": "2024-11-21T01:54:41.435279377Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

그렇다면 지난편에서 살펴본 bridge 네트워크와 동일한 방식으로 동작하는지 확인해 봅시다.

docker@docker:~$ docker run --rm -d busybox sleep 3600
1c233db2d027129a366834107c30b21d7e821dbb6291c6131e56279a2d4d61b2

docker@docker:~$ docker ps
CONTAINER ID   IMAGE     COMMAND        CREATED         STATUS         PORTS     NAMES
1c233db2d027   busybox   "sleep 3600"   2 seconds ago   Up 2 seconds             frosty_cannon

docker@docker:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 3532sec preferred_lft 3532sec
    inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591983sec preferred_lft 604783sec
    inet6 fe80::489e:7eff:fe55:59ee/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:0d:1b:64:27 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:dff:fe1b:6427/64 scope link
       valid_lft forever preferred_lft forever
5: veth2d96e30@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether b6:cd:fd:cd:a3:0b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::b4cd:fdff:fecd:a30b/64 scope link
       valid_lft forever preferred_lft forever
       
### 컨테이너 내부로 접속하여 ip link를 확인해 봅시다
docker@docker:~$ docker exec -it 1c233db /bin/sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
  • @ifN는 attached interface의 약자로, 연결된 인터페이스 번호 N을 알려줍니다.
  • 즉 Host OS는 bridge 인터페이스를 통해 생성한 veth는 인터페이스 아이디 5번을 가지고 있으며, 쌍으로 인터페이스 번호 4를 갖습니다.
  • 실행한 busybox 컨테이너는 veth 인터페이스 4번을 가지고 있으며, 이는 Host OS의 인터페이스 5번과 연결됩니다.
  • 이는 우리가 지난번에 실습한 veth 생성 방식과 동일한 것을 확인할 수 있습니다.

결국 Docker의 bridge 네트워크는 veth를 통해 생성되는 것을 알았습니다.

  • 그렇다면 우리가 bridge 네트워크를 Host에 생성하고, Docker에서 사용할 수 있을까요? 한번 확인해 보시기 바랍니다.

네트워크 네임스페이스 확인하기

도커에서 생성된 네트워크들은 모두 각각의 네트워크 네임스페이스를 가지며, 따라서 같은 네트워크에 속하지 않은, 즉 서로 다른 네트워크에 배포된 컨테이너들은 통신이 불가합니다.

busybox 컨테이너를 다시 실행시키고 네트워크 네임스페이스를 다음의 명령어로 확인해 보겠습니다.

docker@docker:~$ docker run --rm -d busybox sleep 3600
a1225e13b438f10145dfe3a7974aa03e4516059f29a19628050fcbb3a34ee1e8

docker@docker:~$ docker inspect a1225e | grep "Sand"
            "SandboxID": "3813a3982ba836c1122d3c82aaa6c520455e2591115092cd4ab8454366c4532c",
            "SandboxKey": "/var/run/docker/netns/3813a3982ba8",

이때 네트워크 네임스페이스는 /var/run/docker/netns 아래에 생성되기 때문에 ip 패키지를 통해 확인할 수 없습니다.

  • ip 패키지는 기본적으로 /var/run/netns 아래에 생성합니다.

컨테이너의 네트워크 네임스페이스를 ip 패키지로 확인하기 위해 해당 Sandbox 디렉토리를 마운트하고 ip 패키지로 확인해 보겠습니다.

docker@docker:~$ ip netns list
docker@docker:~$ sudo ln -s /var/run/docker/netns/3813a3982ba8 /var/run/netns/busybox

## 컨테이너의 네트워크는 다음과 같이 구성되어 있습니다.
docker@docker:~$ docker inspect a1225 | grep "Networks" --after 17
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "MacAddress": "02:42:ac:11:00:03",
                    "DriverOpts": null,
                    "NetworkID": "0ddc27e0481c8189d5d4427bfc22d34705263b847f4df3dfe34cb39070273757",
                    "EndpointID": "759c8eff452c243a23d321f2425f62acc13cbe13c295a4549c513f497e26436f",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.3",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "DNSNames": null
                }
            }
docker@docker:~$ ip netns list
busybox

## 위와 동일한 네트워크 정보를 추력하는 것을 확인할 수 있습니다.
docker@docker:~$ sudo ip netns exec busybox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

## 확인이 끝난 netns를 다시 원래 상태로 되돌립니다.
docker@docker:~$ sudo rm -r /var/run/netns/busybox
docker@docker:~$ ip netns list
docker@docker:~$
  • Bridge 네트워크에서 서로 다른 네트워크는 별도의 네트워크 네임스페이스를 갖는것을 확인할 수 있습니다.

Bridge Network 생성하기
Bridge 모드는 기본으로 생성되는 Bridge 네트워크 외에도 생성할 수 있습니다.

docker@docker:~$ docker network create my-bridge
ac6f6b0e64686afb591ed79112837db98b2c23bec43f167d02c401f6bc561918

docker@docker:~$ docker network ls
NETWORK ID     NAME        DRIVER    SCOPE
0ddc27e0481c   bridge      bridge    local
c4c67c5ac924   host        host      local
ac6f6b0e6468   my-bridge   bridge    local
102a26b1c427   none        null      local

docker@docker:~$ docker network inspect my-bridge
[
    {
        "Name": "my-bridge",
        "Id": "ac6f6b0e64686afb591ed79112837db98b2c23bec43f167d02c401f6bc561918",
        "Created": "2024-11-21T02:53:50.573300902Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

이제 Bridge 네트워크로 실행한 컨테이너는 모두 종료해 주세요.


Mac VLAN 살펴보기

현재 우리 시스템은 위와 같이 Host OS가 스위치(DHCP Server)인 물리 네트워크로부터 아이피를 할당받고, 여기에 Bridge 모드를 통해 네트워크를 구성하고 있습니다. 이 경우 컨테이너에서 인터넷 연결이 필요할 때 NAT 등을 Host OS의 네트워크 인터페이스에 의존하여 외부와 통신을 수행하고 있습니다.

Mac VLAN 방식은 물리 네트워크에 직접 연결(Bind)하는 방식으로 네트워크를 구성하기 때문에, 이와 같은 오버헤드가 없으며 성능이 뛰어납니다. Mac VLAN은 이름에서 볼 수 있듯이 Mac을 활용한 Virtual LAN으로 Host OS의 Network Interface를 Parent Interface로 하여 하위에 Sub Interfaces를 구성, 컨테이너를 할당함으로써 물리 네트워크를 직접 이용하는 방식입니다.

Mac VLAN이라 이름이 붙은 이유는 이러한 물리 네트워크로 부터 직접적인 처리를 수행하기 위해 컨테이너마다 L2 네트워크와 같이 Mac 주소를 할당하고 통신하기 때문입니다.

  • 스위치에서 같은 포트를 이용하여 맥에 대한 아이피를 할당받기 때문에 물리 네트워크인 스위치(DHCP 서버)에서 하나의 포트에 여러 Mac 주소를 관리하지 못할 수 있습니다.

Mac VLAN은 아래의 명령어로 생성할 수 있습니다.

docker@docker:~$ docker network create --driver macvlan \
  --subnet 192.168.64.0/24 \
  --gateway 192.168.64.1 \
  -o parent=enp0s1 \
  mac-vlan
fea3aa276e8c51ca5197e260a0b6074072e97acca0c7a6bf21426718512a1db3
docker@docker:~$ docker network ls
NETWORK ID     NAME       DRIVER    SCOPE
0ddc27e0481c   bridge     bridge    local
c4c67c5ac924   host       host      local
fea3aa276e8c   mac-vlan   macvlan   local
102a26b1c427   none       null      local

이제 Mac VLAN 네트워크에 busybox 컨테이너를 배포하고 그 특징에 대해 알아봅시다. 이를 위해 Host OS와 같은 물리 네트워크를 사용하는 시스템 한 대가 추가로 필요합니다. 우리의 경우 Host OS에서 도커 네트워크를 알아보기 위한 가상 머신을 생성했기 때문에, 해당 가상머신의 Host OS를 이용하면 되겠습니다. 구분을 위해 해당 시스템을 Hostest OS라고 부르겠습니다.

컨테이너는 192.168.64.232 의 ip를 갖도록 배포할 예정이며, Hostest OS와 동일한 물리 네트워크를 이용할 예정이기 때문에 Hostest OS와 상호 연결이 가능해야 합니다.

또한 컨테이너를 Mac VLAN으로 배포할 때 ip 주소를 직접 정의해 주어야 합니다. 그 이유는 도커의 Mac VLAN은 물리 네트워크에 바인딩되어 마치 가상머신처럼 동작하지만, 실질적으로 DHCP 서버에 아이피 요청(DHCP Discover)을 하지 않기 때문입니다.

그렇기 때문에 DHCP 서버의 아이피 할당 범위 밖의 주소를 할당해야 하며, 컨테이너 Mac VLAN IP 정책을 수립해야 합니다.

## 배포 전에는 ping이 불가합니다.
hostest-os: $ ping 192.168.64.232 -c 4
PING 192.168.64.232 (192.168.64.232): 56 data bytes
ping: sendto: No route to host
Request timeout for icmp_seq 0
ping: sendto: Host is down
Request timeout for icmp_seq 1
ping: sendto: Host is down
Request timeout for icmp_seq 2

--- 192.168.64.232 ping statistics ---
4 packets transmitted, 0 packets received, 100.0% packet loss

## Host OS에서 mac vlan 네트워크를 이용하는 busybox를 배포하니다.
docker@docker:~$ docker run --rm -d --network mac-vlan --ip 192.168.64.232 --name busybox2 busybox sleep 3600
56047bf024cf86e4bfe3d4aeb1d1198975e0ff6070775114efb8459a6696fde9
docker@docker:~$

## 배포 후 Host OS에서 Ping이 가능한 것을 볼 수 있습니다.
hostest-os: $ ping 192.168.64.232 -c 4
PING 192.168.64.232 (192.168.64.232): 56 data bytes
64 bytes from 192.168.64.232: icmp_seq=0 ttl=64 time=1.530 ms
64 bytes from 192.168.64.232: icmp_seq=1 ttl=64 time=9.557 ms
64 bytes from 192.168.64.232: icmp_seq=2 ttl=64 time=1.019 ms
64 bytes from 192.168.64.232: icmp_seq=3 ttl=64 time=1.442 ms

--- 192.168.64.232 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 1.019/3.387/9.557/3.567 ms

그러나 Host OS에서 Mac VLAN 컨테이너로 ping 등 네트워크 연결은 불가능합니다.

docker@docker:~$ docker exec -it 56047b sh
/ # ping 192.168.64.3 -c 4
PING 192.168.64.3 (192.168.64.3): 56 data bytes

--- 192.168.64.3 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss

왜 Host OS로는 연결이 불가능한지, 이를 해결하기 위해선 어떤 솔루션이 있을지 확인해 보시면 좋을 것 같습니다.


IP VLAN 살펴보기

IP VLAN L2
IP VLAN의 경우 Mac VLAN와 비슷하지만 Host OS의 Mac Address를 공유하는 방식입니다.

docker@docker:~$ docker network create -d ipvlan \
  --subnet 192.168.64.0/24 \
  --gateway 192.168.64.1 \
  -o parent=enp0s1 \
  ip-vlan
85f8dcc359c172ff06cbe61cd3d9406f570a7a1f81843affa6604d5a90843842
docker@docker:~$

docker@docker:~$ docker run --rm -d --network ip-vlan --ip 192.168.64.201 --name busybox busybox sleep 3600
c5d3a161d424b0df878f1c495720df277ec642c9231f9c0e5c2b740586106ef3

docker@docker:~$ docker exec -it busybox sh
/ # ping 192.168.64.1
PING 192.168.64.1 (192.168.64.1): 56 data bytes
64 bytes from 192.168.64.1: seq=0 ttl=64 time=11.805 ms
64 bytes from 192.168.64.1: seq=1 ttl=64 time=0.684 ms

## Mac Address가 동일한 것을 확인할 수 있습니다.
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
## 동일한 MAc 주소를 가집니다.
26: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.201/24 brd 192.168.64.255 scope global eth0
       valid_lft forever preferred_lft forever

## Host OS Network Interface 확인
docker@docker:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
## 동일한 Mac 주소를 가집니다.
    link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 2519sec preferred_lft 2519sec
    inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591977sec preferred_lft 604777sec
    inet6 fe80::489e:7eff:fe55:59ee/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:0d:1b:64:27 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:dff:fe1b:6427/64 scope link
       valid_lft forever preferred_lft forever

## 아래는 Hostest OS에서 Ping이 가능한 모습입니다.
hostest-os:$ ping 192.168.64.201 -c 4
PING 192.168.64.201 (192.168.64.201): 56 data bytes
64 bytes from 192.168.64.201: icmp_seq=0 ttl=64 time=1.958 ms
64 bytes from 192.168.64.201: icmp_seq=1 ttl=64 time=0.997 ms
64 bytes from 192.168.64.201: icmp_seq=2 ttl=64 time=1.533 ms
64 bytes from 192.168.64.201: icmp_seq=3 ttl=64 time=1.158 ms

--- 192.168.64.201 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.997/1.411/1.958/0.371 ms

## 마찬가지로 Hostest OS에서 ARP 명령을 보내 봤을 때, Mac VLAN, IP VLAN을 모두 확인할 수 있습니다.
## 그러나 IP VLAN은 Mac VLAN과 다르게 Host OS의 Mac 주소와 동일한 Mac 주소를 가지는 것도 확인할 수 있습니다.
$ arp -an
...
? (192.168.64.1) at 9e:76:e:d4:4f:64 on bridge100 ifscope permanent [bridge]
? (192.168.64.3) at 4a:9e:7e:55:59:ee on bridge100 ifscope [bridge]
? (192.168.64.101) at (incomplete) on bridge100 ifscope [bridge]
## IP VLAN
? (192.168.64.201) at 4a:9e:7e:55:59:ee on bridge100 ifscope [bridge]
## Mac VLAN
? (192.168.64.232) at 02:42:c0:a8:40:e8 on bridge100 ifscope [bridge]
...

IP VLAN L3
IP VLAN L3 모드는 Host OS를 마치 Router처럼 이용하여 격리된 네트워크를 제공하는 방식입니다. L3 모드를 이용하기 때문에 L2의 특징을 갖지 않습니다. 즉 ARP 등을 통해 네트워크 주소를 찾을 수 없으며, 서로 다른 네트워크 간 도달할 수 있는 방법이 없습니다.

이는 바꿔말하면 생성한 IP VLAN L3 네트워크에 대한 모든 제어를 직접 관리할 수 있다는 장점이 됩니다.

docker@docker:~$ docker network create -d ipvlan \
  --subnet 192.168.20.0/24 \
  -o parent=enp0s1 -o ipvlan_mode=l3 \
  ip-vlan-l3
  
docker@docker:~$ docker run --rm -d --network ip-vlan-l3 --ip 192.168.20.11 --name busybox busybox sleep 3600
89dec21dff06582e745d54183905b28446cfcd9c0005c99b28c0e9862c54d386
docker@docker:~$ docker run --rm -d --network ip-vlan-l3 --ip 192.168.20.12 --name busybox2 busybox sleep 3600
01ab7b4facc67b2a73c228d99873e88a9692f96e71bb2db46803daf5fe97f622
docker@docker:~$ docker inspect ip-vlan-l3
[
    {
        "Name": "ip-vlan-l3",
        "Id": "fe835a4899fc7d5302b93edb852496908eeab6b6d4661362c346ed89d916d757",
        "Created": "2024-11-21T13:22:00.408568176Z",
        "Scope": "local",
        "Driver": "ipvlan",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.20.0/24"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "01ab7b4facc67b2a73c228d99873e88a9692f96e71bb2db46803daf5fe97f622": {
                "Name": "busybox2",
                "EndpointID": "6017612d2f7bf47eac2a5eff53a06c785e5e31dc9c2f2cf9f64ebfc93da8c964",
                "MacAddress": "",
                "IPv4Address": "192.168.20.12/24",
                "IPv6Address": ""
            },
            "89dec21dff06582e745d54183905b28446cfcd9c0005c99b28c0e9862c54d386": {
                "Name": "busybox",
                "EndpointID": "d3fe6793ee2b204828ad1e792ec6a2576565214234f5e3f5e0361bf0cd880b88",
                "MacAddress": "",
                "IPv4Address": "192.168.20.11/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "ipvlan_mode": "l3",
            "parent": "enp0s1"
        },
        "Labels": {}
    }
]

docker@docker:~$ docker exec -it busybox sh
/ # ping 192.168.20.12
PING 192.168.20.12 (192.168.20.12): 56 data bytes
64 bytes from 192.168.20.12: seq=0 ttl=64 time=0.118 ms
64 bytes from 192.168.20.12: seq=1 ttl=64 time=0.133 ms
^C
--- 192.168.20.12 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.118/0.125/0.133 ms

주의할 점은 하나의 Host OS 네트워크 인터페이스에 대해 IP VLAN을 여러 개 배포한다면, 하나의 도커 네트워크 생성 명령에서 여러 IP VLAN 대역을 생성해야 합니다.

docker@docker:~$ docker network create -d ipvlan \
  --subnet 192.168.20.0/24 \
  # 네트워크 추가
  --subnet 192.168.30.0/24 \
  -o parent=enp0s1 -o ipvlan_mode=l3 \
  ip-vlan-l3

마치며

오늘은 도커에서 네트워크를 구성할 수 있는 방식들을 살펴보았습니다. 이를 통해 쿠버네티스에서 해결한 네트워크 문제 중 컨테이너 간 통신에 대해 살펴보았습니다. 이번 컨테이너 간 통신을 통해 다음을 이해하셨으면 합니다.

  • 컨테이너 통신 중 외부 통신을 어떻게 수행하는지
  • 컨테이너마다 네트워크가 어떻게 격리되는지
  • 컨테이너간 어떻게 통신을 수행해야 하는지
  • Network 구성을 어떻게 해야 하는지

끝으로 중간중간 tcpdump 등 패킷을 확인할 수 있는 도구를 이용하여 직접 Host OS의 네트워크 인터페이스와 생성된 가상 네트워크 인터페이스 등을 직접 확인하여 어떻게 동작하는지 확인하신다면 좋을 것 같습니다.

만약 '쿠버네티스가 갑자기 왜 나와?' 라는 생각이 드신다면, 쿠버네티스에서 애플리케이션을 어떻게 배포하는지, 배포된 애플리케이션은 어떤 단위를 가지는지 한번 생각해 보시면 좋겠습니다.


그렇다면 이것도…

  • 같은 도커 Bridge 네트워크 내에서 각각 배포된 컨테이너는 통신이 가능할까요? 그 결과와 이유에 대해 생각해 보시면 좋을 것 같습니다.
  • Mac VLAN, Bridge 모드 모두 Host의 네트워크 인터페이스에 의존합니다. 그 차이가 무엇일까요?
  • Mac VLAN에서 IP를 정의하지 않으면 어떻게 동작할까요? 무엇을 주의해야 할까요? IP를 정의하지 않고 안전하게 IP를 할당하려면 어떻게 해야 할까요?
  • Mac VLAN 네트워크를 이용하는 컨테이너와 Host OS 간 연결이 왜 불가능한가요? 해결책이 있을까요?
  • Mac VLAN 모드에서 80 포트를 노출시키고, Host Bridge 네트워크 모드에서 80 포트를 동시에 노출시킬 수 있나요? 그 결과와 이유는 무엇인가요?
  • IP VLAN L3 모드에서 다른 대역을 가지는 Subnet을 생성한 뒤, 컨테이너를 각각 할당한다면 해당 컨테이너들은 서로 통신 가능한가요? 그 결과와 이유는 무엇인가요?
  • 쿠버네티스의 파드는 어떻게 네트워크가 구성되나요?

참고

https://docs.docker.com/engine/network/drivers/bridge/

https://docs.docker.com/engine/network/drivers/macvlan/

https://docs.docker.com/engine/network/drivers/ipvlan/

 

+ Recent posts