본문 바로가기

Tech/Docker

Docker Network : 호스트와 컨테이너를 위한 네트워크를 구성해보자

컨테이너가 실행될 때, 특정 내부 IP가 할당됩니다. 이때 내부 IP만으로는 외부와의 통신이 불가능하죠. 우리가 컨테이너를 활용하다 보면, 컨테이너와 컨테이너, 컨테이너와 호스트 등 서로 통신이 가능해야 할 순간이 필수일 겁니다. 이를 위해, 도커의 네트워크 구조를 살펴보고, 이러한 구조 아래에서 다양한 네트워크를 구성해 보겠습니다.

도커 네트워크의 구조

도커의 기본적인 네트워크 구조는 아래와 같습니다.

우리가 확인해 봐야 할 요소는 크게 호스트의 eth0, 기본 브리지인 docker0, 컨테이너의 가상 네트워크 인터페이스인 veth 이 3가지입니다. 각각의 요소를 살펴보겠습니다.

eth0

호스트의 eth0은, 실제 우리가 외부와 연결할 때 사용하는 IP가 할당된 호스트 네트워크 인터페이스입니다.

docker0

docker0은 도커가 설치될 때, 기본적으로 구성되는 브리지입니다. 이 브리지의 역할은 호스트의 네트워크인 eth0과 컨테이너의 네트워크와 연결을 해주는 역할을 합니다. 하나의 브리지 안에서 다양한 컨테이너와 연결을 해줄 수 있으며, 새로운 브리지를 생성하는 것도 가능합니다. 가지가 뻗어져 나뭇잎과 연결되는 모습을 연상하면 편합니다.

veth

veth는 컨테이너의 내부 IP를 외부와 연결해 주는 역할을 하는 가상 인터페이스입니다.(virtual eth의 약자입니다.) 컨테이너가 생성될 때 veth가 동시에 생성되며, 이 veth를 호스트의 eth0과 연결시킴으로써 외부와의 통신이 가능해집니다. 그리고 이 연결을 docker0과 같은 브리지가 수행하게 되죠.

 

이러한 네트워크 구조 아래, 도커 자체에서 제공하는 네트워크 드라이버인 Bridge 네트워크, Host 네트워크, None 네트워크, Container 네트워크 등이 있습니다. 도커의 network명령을 통해 생성되어 있는 기본 네트워크 드라이버들을 확인할 수 있습니다. 이때 bridge가 위에서 말한 docker0이라고 보면 되겠습니다.

$ docker network ls

NETWORK ID     NAME          DRIVER    SCOPE
357d25bdb2dc   bridge        bridge    local
394e57d3ebab   host          host      local
085ab69423f0   none          null      local

브리지(Bridge) 네트워크

1) 기본 브리지

우선 기본적으로 생성되어 있는 docker0 브리지를 살펴보겠습니다. docker의 inspect명령을 통해 상세 정보를 불러올 수 있습니다.

$ docker inspect brdige

"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
           {
               "Subnet": "172.17.0.0/16",
               "Gateway": "172.17.0.1"
           }
		]

출력에서 IP 세부 정보를 살펴보면, 172.17.x.x 대역대의 내부 IP를 사용하고 있음을 볼 수 있습니다. 해당 브리지와 연결되는 컨테이너는 172.17.x.x 대역대의 IP를 순차적을 부여받게 될 것입니다. 확인해 보겠습니다.

docker run -it --name ubuntu1 ubuntu

특정 브리지를 연결시켜주지 않으면, 기본적으로 docker0 브리지에 연결됩니다. 컨테이너 내부에서 ifconfig 명령어를 통해 현재 할당받은 ip를 확인해 봅니다. 이때, ubuntu에선 특정 버전 이후 ifconfig가 없을 수 있으니,apt update apt install net-tools 통해 설치해 주도록 합니다.

$ ifconfig

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 2323  bytes 24816714 (24.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1975  bytes 136621 (136.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

해당 컨테이너는 현재 172.17.0.2 ip를 할당받았습니다. 브리지와 동일한 대역대(172.17.x.x)를 사용함을 볼 수 있고, 추가로 컨테이너를 하나 더 실행하고 확인해 보면, 172.17.0.3를 할당받은 걸 확인할 수 있을 것입니다. 브리지 대역대를 순차적으로 할당해 주기 때문이죠.

2) 새로운 브리지 생성

이제는 기본 bridge가 아닌, 새로운 나만의 브리지를 생성해 보겠습니다. 도커의 network 명령어로 쉽게 생성할 수 있습니다.

docker network create --driver bridge new_bridge

해당 브리지의 대역대를 살펴보겠습니다.

$ docker inspect new_bridge

[
    {
        "Name": "new_bridge",
        "Id": "b31f9d8ae3cf7ada52782459b450be180ebfef546685648a43b8d4cd958e4ed6",
        "Created": "2023-04-14T02:31:41.089862423Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                }
            ]
        },

앞서 살펴봤던 기본 브리지와는 달리, 172.20.x.x 대역대를 사용하고 있음을 볼 수 있습니다. 새롭게 생성한 브리지와 연결되는 컨테이너를 새로 생성해 보겠습니다. --net옵션을 사용하여 손쉽게 연결할 수 있습니다.

docker run -it --name ubuntu2 --net new_bridge ubuntu

해당 컨테이너의 ip를 살펴보겠습니다.

$ ifconfig

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.20.0.2  netmask 255.255.0.0  broadcast 172.20.255.255
        ether 02:42:ac:14:00:02  txqueuelen 0  (Ethernet)
        RX packets 2520  bytes 24830812 (24.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2263  bytes 155653 (155.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

새롭게 생성한 new_bridge의 대역대(172.20.x.x)와 동일한 대역을 가짐을 볼 수 있습니다. 현재 기본 bridge와 새롭게 생성한 new_bridge의 네트워크 상황을 그림으로 살펴보면 아래와 같습니다.

그림에서 보면 아시겠지만, ubuntu1과 ubuntu2의 컨테이너 들은 직접적인 연결 점이 없습니다. 당연히 두 컨테이너는 서로 통신이 불가능하겠죠. ubuntu2의 컨테이너에서 ubuntu1의 컨테이너로 ping을 보내보고 응답이 있는지 확인해 보겠습니다. (ping이란? Packet Internet Grouper의 약자로, 일정한 크기(32바이트)의 패킷을 특정 네트워크로 보내, 응답이 있는지 확인해 볼 수 있는 방법입니다. 흔히 서버의 네트워크가 정상적으로 동작하는지 점검하는 데 사용할 수 있습니다.)

ping 172.17.0.2

ubuntu1의 ip인 172.17.0.2로 핑 테스트를 해보면 아무런 응답을 받을 수 없습니다.

3) 브리지 연결 및 삭제

위의 ubuntu1과 ubuntu2와 서로 통신을 할 수 있도록 하기 위해, ubuntu1을 new_bridge로 연결해 보겠습니다. 도커 network의 connect를 통해 쉽게 연결할 수 있습니다.

docker network connect new_bridge ubuntu1

이제 ubuntu1은 new_bridge의 브리지와도 연결되어 있기 때문에, 기존의 bridge와 더불어 2개의 브리지와 연결되어 있을 것입니다. ubuntu1에서 ifconfig를 통해 네트워크 정보를 보겠습니다.

$ ifconfig

eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.20.0.3  netmask 255.255.0.0  broadcast 172.20.255.255
        ether 02:42:ac:14:00:03  txqueuelen 0  (Ethernet)
        RX packets 10  bytes 796 (796.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 10  bytes 876 (876.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

이제는 ubuntu1과 ubutu2 끼리는 서로 통신이 가능할 것입니다. ubuntu1에서 ubunut2로 핑을 보내 보도록 하죠.

$ ping ubuntu2

PING ubuntu2 (172.20.0.2) 56(84) bytes of data.
64 bytes from ubuntu2.new_bridge (172.20.0.2): icmp_seq=1 ttl=64 time=2.03 ms
64 bytes from ubuntu2.new_bridge (172.20.0.2): icmp_seq=2 ttl=64 time=0.406 ms
64 bytes from ubuntu2.new_bridge (172.20.0.2): icmp_seq=3 ttl=64 time=0.264 ms
64 bytes from ubuntu2.new_bridge (172.20.0.2): icmp_seq=4 ttl=64 time=0.278 ms

다음과 같이 응답이 오는 것을 확인할 수 있습니다. 이제는 기본 브리지와의 통신은 필요가 없기에, 기본 브리지와의 연결은 해제하겠습니다. disconnect를 통해 쉽게 해제할 수 있습니다.

docker network disconnect bridge ubuntu1

지금까지의 네트워크 상황을 살펴보면 아래와 같습니다.

이처럼, 브리지를 통해 나만의 네트워크 환경을 구성할 수 있고, 서로의 컨테이너를 연결 및 해제할 수도 있습니다. 생성한 new_bridge의 네트워크가 더 이상 필요하지 않을 때, docker network rm 명령어로 삭제할 수 있습니다.

docker network rm new_bridge

호스트(Host) 네트워크

네트워크를 호스트 모드로 설정하게 되면, 호스트의 네트워크 환경을 그대로 사용할 수 있습니다. 컨테이너가 실행될 때, 새로운 내부 IP를 할당받을 필요 없이 호스트의 네트워크를 곧바로 사용하게 되죠. --net 옵션으로 지정해줄 수 있습니다.

docker run -it --name ubuntu_host --net host ubuntu

위와 같이 컨테이너가 호스트 네트워크를 사용한다면, 마치 호스트 내에서 애플리케이션을 실행한 것과 같아집니다. 덕분에 컨테이너 내에서 실행되는 애플리케이션을 별도의 포트포워딩 없이 곧바로 localhost를 통해, 접속할 수 있게 되죠. host네트워크를 사용하는 컨테이너 내에서 ifconfig 명령어를 통해 네트워크 정보를 살펴보면, 호스트의 네트워크 정보와 동일한 걸 확인할 수 있습니다. 이러한 호스트 모드는 언제 사용하게 될까요? 호스트 네트워크를 직접 사용하기에 NAT(Network Adress Translation) 과정이 필요 없어지게 됩니다. 따라서 성능 향상 및 최적화가 필요하거나 광범위한 포트 처리를 해야 하는 경우 사용할 수 있겠습니다. 하지만, 컨테이너에 보안 문제가 발생하여 외부로부터 공격적인 접근이 온다면, 호스트의 네트워크까지 직접적으로 공격당할 수 있기에 주의해야 합니다.

논(None) 네트워크

None은 말 그대로, 아무런 네트워크를 사용하지 않는 것을 말합니다. 컨테이너를 어떠한 외부와도 연결하지 않을 때 사용합니다. --net 옵션에 none을 입력함으로써 사용할 수 있습니다.

docker run -it --name network_none --net none ubuntu

docker inspect network_none을 통해 해당 컨테이너의 정보를 보면, 네트워크 정보에 어떠한 ip주소도 할당되어있지 않은 상태를 확인할 수 있습니다.

컨테이너(Container) 네트워크

--net 옵션에 특정 컨테이너의 이름을 넣어주면, 해당 컨테이너의 네트워크를 공유하게 됩니다. 아래 명령어를 통해 ubuntu3 컨테이너를 새롭게 생성하고, 네트워크는 기존의 ubuntu2와 공유하도록 하겠습니다.

--net container:[다른 컨테이너의 이름 or ID]

docker run -it --name ubuntu3 --net container:ubuntu2 ubuntu

이제 ubuntu3이 ubuntu2의 네트워크를 공유받았기에, 이 두 컨테이너는 서로 동일한 네트워크를 사용합니다. 애초에 ubuntu3 컨테이너가 생성될 때, 특정 내부 IP를 새로 할당받지 않고, ubuntu2의 IP를 공유받게 됩니다. 이를 그림으로 살펴보면 아래와 같습니다.