아래 실습은 macOS(v11.1)에서 진행되었습니다.
네트워킹은 프로세스 간 통신에 관한 것으로 도커 네트워킹도 이와 다르지 않다. 도커 네트워킹은 주로 도커 데몬이 실행중인 호스트 머신에서 컨테이너간 그리고 컨테이너와 외부 간의 통신을 설정하는데 사용된다.
도커는 각각의 특정 사용 사례에 맞는 서로 다른 유형의 네트워크를 지원한다. (아래 도커 네트워크 모드에서 설명)
그렇다면 도커 네트워크와 virtual machine(VM) 또는 physical machine의 네트워크와는 어떤 점이 다를까?
1. VM은 NAT 및 host networking과 같은 구성을 지원한다. 도커에서는 host networking을 지원하지만 Linux에서만 가능하다.
2. 도커 컨테이너를 사용할 때 완전히 별도의 네트워킹 스택이 아닌 네트워크 네임스페이스를 사용하여 네트워크를 완전히 분리할 수 있다.
도커 네트워크는 각각 unique한 이름과 ID를 지니게 되고, 한 개의 드라이브를 소유하고 있다.
아래 명령어를 통해 도커 네트워크를 확인할 수 있다.
$ docker network ls
그러면 위와 같이 default로 3개의 도커 네트워크 모드가 있는 것을 확인할 수 있다.
3가지 도커 네트워크 모드 외 추가적으로 도커 네트워크에는 Ipvlan과 Macvlan이 있는데 이 두 가지 모드까지 함께 알아본다.
컨테이너를 설치하면 default로 연결되는 네트워크로, 동일한 Host Computer에 Container들간 통신하기 위한 모드이다.
(https://docs.docker.com/engine/tutorials/networkingcontainers/)
도커 컨테이너를 기본 생성했을 때 사용되는 브릿지 모드에 대해서 알아본다.
도커를 설치하면 자동으로 host 머신의 네트워크 인터페이스에 docker0이라는 가상 인터페이스(virtual interface)가 생성된다. docker0은 일반적인 인터페이스가 아닌, virtual ethernet bridge로 컨테이너가 생성되면 이 bridge(docker0)에 컨테이너의 인터페이스가 하나씩 바인딩되어 통신하게 된다.
(👇 아래 실습에서 docker0에 바인딩된 인터페이스 확인)
컨테이너는 외부와 통신하기 위해 2개의 네트워크 인터페이스를 함께 생성한다.
하나는 컨테이터 내부 namespace에 할당되는 eth0 인터페이스
, 그리고 다른 하나는 호스트 네트워크 docker0에 바인딩되는 veth 이름 형식의 veth 인터페이스
이다.
docker0 브리지는 veth 인터페이스와 호스트의 eth0 인터페이스를 연결해주는 중간 다리 역할로, 이를 통해 컨테이너 내부의 eth0 인터페이스가 veth 인터페이스를 통해 외부와 통신할 수 있게 된다.
2대의 컨테이너를 직접 생성해봄으로써 브릿지 모드에 대해 알아본다.
# bridge1 이름으로 컨테이너 생성
$ docker run -d --name bridge1 busybox
# bridge2 이름으로 컨테이너 생성
$ docker run -d --name bridge2 busybox
$ docker ps
두 컨테이너를 생성하고 default 브릿지인 docker0
에 연결된 인터페이스를 확인해보면 생성한 2대의 컨테이너 veth 인터페이스가 연결된 것을 확인할 수 있다.
$ brctl show docker0
생성한 컨테이너 내부로 접속하여 좀 더 알아본다.
# bridge1 컨테이너 bash로 실행
$ docker exec -it bridge1 bash
# 네트워크 정보 확인
$ ip -c a
# bridge2 컨테이너 bash로 실행
$ docker exec -it bridge2 bash
# 네트워크 정보 확인
$ ip -c a
두 컨테이너의 ip가 127.17.x.x
대역으로 동일한 것을 알 수 있다.
이는 docker0 브릿지를 활용하여 컨테이너를 생성할 때, DHCP가 아닌 IPAM에 정의된 172.17.x.x
IP 대역을 컨테이너에 순차적으로 할당되기 때문이다.
$ docker network inspect bridge
생성된 두 컨테이너는 docker0을 통해 서로간 통신이 가능하고, 외부와도 통신이 가능하다.
# docker1 -> docker2
$ ping -c 1 172.17.0.3
# docker2 -> 외부(8.8.8.8)
$ ping -c 1 172.17.0.3
Host 컴퓨터와 동일한 네트워크 환경을 사용하는 모드이다. 컨테이너의 호스트 이름도 호스트 머신의 이름과 동일하다.
컨테이너를 실행하여 확인해본다.
우선 host 환경에서 네트워크 정보를 확인한다.
$ ip -c a
그리고 컨테이너 내부에서 네트워크 정보를 확인한다.
# --net 옵션을 통해 네트워크 모드를 host로 설정
$ docker run -d --net host --name host busybox
# 컨테이너 bash로 실행
$ docker exec -it host bash
위의 두 네트워크 정보가 동일한 것을 알 수 있다.
❓ 그렇다면 언제 host 모드 사용할까?
-> NAT가 필요하지 않기 때문에 컨테이너가 광범위한 포트를 처리해야 하는 상황과 성능 향상 및 최적화가 필요할 경우 (다만, 포트 충돌 주의)
아무 네트워크를 쓰지 않고 내부에 lo 인터페이스만 존재하는 모드이다. 이 모드는 컨테이너의 IP를 구성하지 않으며 다른 컨테이너뿐만 아니라 외부 네트워크에도 액세스할 수 없다.
호스트 인터페이스를 가상화하는 기술로, MAC address와 IP adress를 부여하여, 실제 네트워크에 container를 직접 연결하는 방식이다.
(https://docs.docker.com/network/ipvlan/)
enp0s8의 ip 대역을 Ipvlan 모드의 컨테이너들이 사용할 수 있다. 때문에 위 그림처럼 컨테이너들의 ip 대역이 같은 것을 알 수 있다.
하나의 도커 컨테이너를 외부에서 접근하는 동작으로 실습을 진행해본다.
2개의 서버를 생성하여 실습을 진행한다.
# vm1(docker1), vm2(docker2)
# Ipvlan L2 모드 네트워크 생성 및 확인
$ docker network create -d ipvlan --subnet=192.168.50.0/24 --gateway=192.168.50.254 -o parent=enp0s8 my_ipvlanl2
$ docker network ls
$ docker inspect my_ipvlanl2
vm1에서 위 명령어를 실행하면 아래와 같이 출력된다.
# Ipvlan을 사용하는 vm1(docker1) 컨테이너 실행
$ docker run --net=my_ipvlanl2 -it --name test1 --hostname test1 --ip 192.168.50.100 --rm ubuntu:14.04 /bin/bash
# 네트워크 정보 확인
$ ip a
# Ipvlan을 사용하는 vm2(docker2) 컨테이너 실행
$ docker run --net=my_ipvlanl2 -it --name test2 --hostname test2 --ip 192.168.50.200 --rm ubuntu:14.04 /bin/bash
# 네트워크 정보 확인
$ ip a
컨테이너가 정상적으로 생성됐다면 ping을 이용해 통신이 가능한지 확인해본다.
# vm1(docker1)의 test1 컨테이너 내부
# vm2(docker2) 호스트와 통신 가능
$ ping 192.168.50.20
# vm2(docker2)의 test2 컨테이너와 통신 가능
$ ping 192.168.50.200
# 자신의 호스트와 통신 불가능
$ ping 192.168.50.10
# vm2(docker2)의 test2 컨테이너 내부
# vm1(docker1) 호스트와 통신 가능
$ ping 192.168.50.10
# vm1(docker1)의 test1 컨테이너와 통신 가능
$ ping 192.168.50.100
# 자신의 호스트와 통신 불가능
$ ping 192.168.50.20
vm2(docker2)의 test2 컨테이너 내부에서 위 명령어를 실행하면 아래와 같이 출력된다.
추가적으로 vm1 컨테이너에서 vm2로 ping을 날리면 docker0 브릿지가 아닌 부모 인터페이스인 enp0s8을 통해 트래픽이 전달되는 것을 알 수 있다.
# vm1(docker1)의 test1 컨테이너 내부
$ tcpdump -n -i docker0 icmp
$ tcpdump -n -i enp0s8 icmp
$ ping 192.168.50.20
실습을 통해 아래와 같은 통신 여부를 알 수 있다.
- A 호스트 OS에서 기동중인 컨테이너 <-> 다른 호스트 OS (0)
- A 호스트 OS에서 기동중인 컨테이너 <-> 다른 호스트 OS에서 기동중인 컨테이너 (0)
- A 호스트 OS에서 기동중인 컨테이너 <-> 호스트 OS (x)
Ipvlan은 호스트OS의 IP대역대를 할당 받기 때문에 컨테이너와 같은 호스트만 통신이 불가하고 다른 호스트 및 컨테이너리는 통신이 자유로운 것을 알 수 있다.
하나의 네트워크 인터페이스를 여러개의 별도의 Mac 주소를 가지는 네트워크 인터페이스로 분리하여 사용할 수 있는 네트워크 기술이다.
(https://hicu.be/macvlan-vs-ipvlan)
Macvlan은 하나의 부모 인터페이스를 이용하여 여러개의 자식 인터페이스를 생성한다. 즉, 각 컨테이너들의 네트워크 인터페이스는 자식 인터페이스가 되고 호스트의 네트워크 인터페이스가 부모 인터페이스가 된다. 자식 인터페이스는 각각 별도의 Mac 주소와 Macvlan 모드를 가질 수 있다. 모드는 자식 인터페이스 생성 시 설정 가능하고, 모드에 따라 Macvlan의 Packet 전송 정책이 달라진다.
Macvlan도 Ipvlan과 마찬가지로 호스트 컨테이너에서 해당 호스트와의 통신은 불가능하지만 다른 호스트와는 통신이 가능하다.
(https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=alice_k106&logNo=220984112963)
2개의 서버를 생성하여 실습을 진행한다.
802.1q trunk bridge mode로 Macvlan에 대해 알아본다.
| eth0s8.51
과 같이 점이 포함된 상위 인터페이스 이름을 지정하면 도커는 이를 eth0의 하위 인터페이스로 해석하고 하위 인터페이스를 자동으로 만든다.
# vm1(docker1)
# 점이 포함된 상위 인터페이스 지정(enp0s8.51) -> subinterface 자동 생성
$ docker network create -d macvlan --subnet=192.168.51.0/24 --ip-range=192.168.51.0/25 -o parent=enp0s8.51 macvlan51
$ docker network ls
$ docker inspect macvlan51
$ ip -c a
Macvlan에서는 호스트 NIC가 비규칙(Promiscuous) 모드를 사용해야 한다. 만약 Promiscuous mode가 설정되어 있지 않을 경우, 컨테이너의 MAC 주소가 목적지로 되어 있는 패킷이 Docker 호스트의 인터페이스로 전달되더라도 패킷을 컨테이너로 전달하지 않아 통신이 정상적으로 이루어지지 않기 때문이다.
Promiscuous mode 란?
- 네트워크 인터페이스는 패킷 도달 시 패킷의 2계층 목적지 주소를 확인
- 2계층 목적지 주소가 해당 인터페이스의 주소이거나 브로드캐스트 주소(FF:FF:FF:FF:FF:FF)일 경우, 패킷을 내부로 전달
- 2계층 목적지 주소가 해당 인터페이스의 주소나 브로드캐스트 주소가 아닐 경우 해당 패킷을 폐기함
- Promiscuous mode는 패킷의 목적지 주소가 폐기조건에 해당하더라도 패킷을 내부로 전달하는 방식
- Macvlan은 하나의 인터페이스에 다중의 IP와 Mac 주소를 가지기 때문에 허용해야함
- 불필요한 패킷이 내부로 전달될 수 있으나, 패킷 모니터링 등 특정 목적을 위하여 사용
# enp0s8.51에 Promiscuous mode 허용
$ ip link set enp0s8.51 promisc on
$ ip addr add 192.168.51.127/24 dev enp0s8.51
$ ip -c route
다른 vm에도 똑같이 환경을 세팅해준다.
# vm2(docker2)
# 점이 포함된 상위 인터페이스 지정(enp0s8.51) -> subinterface 자동 생성
$ docker network create -d macvlan --subnet=192.168.51.0/24 --ip-range=192.168.51.128/25 -o parent=enp0s8.51 macvlan51
$ docker network ls
$ docker inspect macvlan51
$ ip -c a
# enp0s8.51에 Promiscuous mode 허용
$ ip link set enp0s8.51 promisc on
$ ip addr add 192.168.51.254/24 dev enp0s8.51
$ ip -c route
그리고 Ipvlan과 같이 각 vm에 컨테이너를 생성하여 통신을 확인해본다.
# Macvlan을 사용하는 vm1(docker1) 컨테이너 실행
$ docker run -it --name c3 --hostname c3 --rm --network macvlan51 ubuntu:14.04
# 네트워크 정보 확인
$ ip a
# Macvlan을 사용하는 vm2(docker2) 컨테이너 실행
$ docker run -it --name c4 --hostname c4 --rm --network macvlan51 ubuntu:14.04
# 네트워크 정보 확인
$ ip a
# vm1(docker1)의 c3 컨테이너 내부
# vm2(docker2) 호스트와 통신 가능
$ ping 192.168.51.254
# vm2(docker2)의 c4 컨테이너와 통신 가능
$ ping 192.168.51.129
# 자신의 호스트와 통신 불가능
$ ping 192.168.51.127
# vm2(docker2)의 c4 컨테이너 내부
# vm1(docker1) 호스트와 통신 가능
$ ping 192.168.51.127
# vm1(docker1)의 c3 컨테이너와 통신 가능
$ ping 192.168.51.2
# 자신의 호스트와 통신 불가능
$ ping 192.168.51.254
그러면 Ipvlan과 같이 같은 서브넷의 존재하는 다른 장비와는 통신이 가능하지만 자신의 호스트와는 통신이 불가한 것을 알 수 있다.
Macvlan은 컨테이너에 고유한 Mac주소를 할당하고, Ipvlan은 호스트와 동일한 Mac주소를 할당한다.
https://jonnung.dev/docker/2020/02/16/docker_network/
https://www.youtube.com/watch?v=9BkecpIJ3jw
https://julie-tech.tistory.com/50
https://bluese05.tistory.com/15
https://docs.docker.com/network/#network-drivers
https://www.metricfire.com/blog/what-is-docker-network-host/
https://earthly.dev/blog/docker-networking/
https://doryoku.tistory.com/m/694
https://hoing.io/archives/24498
https://hicu.be/macvlan-vs-ipvlan
https://sreeninet.wordpress.com/2016/05/29/macvlan-and-ipvlan/