[![logo](https://www.hongsnet.net/images/logo.gif)](https://www.hongsnet.net)

# Docker Network 검토

> Docker의 네트워크 개념을 검토하고, 서비스에 적용한다.


# Docker의 Network 방식

Docker는 **크게** **Overlay**, **Bridge**, **host**, **none** 의 `네 가지 Network 방식이 존재`하며, 다음과 같이 개념과 역할에 대해 알아보자.

# Overlay Network [ `Pick` ]

*  **Overlay Network**는 **Docker Swarm에 참여**하는 Docker Daemon 간의 통신을 관리한다.
*  독립 실행형 컨테이너의 Network를 생성하는 방법과 동일한 방식으로 Overlay Network를 생성할 수 있다.
*  기존에 생성된 Overlay Network에 Service를 연결시켜, `Service 간 통신을 활성화`할 수 있다.
*  Overlay Network는 Overlay Network Driver를 사용한다.

> www.hongsnet.net 서비스는 다음과 같이,

> Front-end(HA-Proxy) -> Internal Network -> Backend(Docker) -> Internal Network -> DBMS 의 구성이므로, `Overlay Network`를 이용하여 구성한다.
 
## Ingress Network ( Overlay Network **Sub mode** )

*  **Ingress Network**는 Service의 Node 들간에 **Load Balancing**을 하는 Overlay Network 이다.
*  **Docker Swarm**의 모든 Node가 노출된 Port로 요청을 받게되면, 해당 요청을 IPVS라는 모듈로 전달한다.
*  IPVS는 해당 Service에 참여하는 모든 IP 주소를 추적하고, 그 중 하나를 선택한 뒤 요청을 해당 경로로 Routing 한다.
*  Igreess Network는 Docker Swarm을 Init하거나 Join할 때 **자동으로 생성**된다.

# host

**Host 모드**는 docker0 interface를 사용하지 않고 **Host OS의 네트워크 자원을 사용**한다. bridge 형식으로 사용하지 않기 때문에 **brctl show docker0** 명령으로 검색해도 interfaces에 나타나지 않으며, Container에서 Port를 노출하지 않는 경우 사용할 수가 없으며, `보안상 권장하지 않는 방법`이다.

# None

컨테이너가 LoopBack(127.0.0.1)을 제외한 어떠한 네트워크도 사용하지 않는다. 이는 격리가 필요할 경우 사용된다.


# Bridge

Docker에서 `기본적으로 설정되는 Network 방식`이다. 호스트에서 다음과 같이 **docker network inspect bridge** 명령을 통해 상세정보를 확인해본다.

```bash
# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "43e5b193049b1f29fa48819a0cf137f433cb3e4f512efccaedfa6f034e69e62d",
        "Created": "2020-01-21T05:35:46.711742895+09:00",
        "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,
        "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라는 특성에 맞게, **같은 네트워크(Layer 2)에서 통신이 가능**하다. 즉, Ingress와 같이 외부로 노출하지 않을 경우에는 `기본적으로 외부와의 통신이 불가능`하다.


## docker0 Network ( Bridge Network **Sub mode** )

Docker Host를 설치한 후 host의 Network Interface를 보면, **docker0**라는 Virtual Interface가 존재하는 것을 볼 수 있다. 아래는 Docker를 설친한 host의 Interface를 확인한 정보이다.

```bash
# ifconfig docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 0.0.0.0
        inet6 fe80::42:7aff:fe09:f1d8  prefixlen 64  scopeid 0x20<link>
        ether 02:42:7a:09:f1:d8  txqueuelen 0  (Ethernet)
        RX packets 5489687  bytes 20588189759 (19.1 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 9576525  bytes 26208253051 (24.4 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
```

**docker0** Interface의 특징은 다음과 같다.

*  IP는 자동으로 172.17.0.0/16 네트워크로 설정된다.
*  이 IP는 DHCP를 통해 할당받는 것은 아니며, docker 내부 로직에 의해서 자동으로 할당받는 것이다.
*  docker0는 일반적인 Interface가 아니며, Virtual Etherent Bridge이다.

**docker0**는 Container가 통신하기 위한 `가상 Linux Bridge`이다. Bridge는 기본적으로 Layer2 통신 기반이며, 만약 Container가 생성되면, 이 Bridge에 Container의 Interface가 하나씩 Binding되는 형태이다. 따라서 **Container가 외부로 통신할 때는 무조건 docker0 Interface를 거쳐야 한다.**


## Container Network ( Bridge Network **Sub mode** )

Container Network 방식은 `같은 호스트에 있는 컨테이너들 끼리의 통신`을 제공하는 방법이다. 컨테이너 간의 통신은 내부 도커 네트워크를 통해서 이루어지며, 통신은 호스트의 네트워크로 노출되지 않는다는것을 의미한다.

**--link *CONTAINER*:*ALIAS*** 로 설정하며, *CONTAINER* 는 링크 컨테이너(연결되는 컨테이너), *ALIAS*는 마스터 컨테이너(시작되는 컨테이너)의 내부에서 링크 컨테이너를 참조할 때 사용하는 로컬 이름을 설정 한다.


# Docker Container의 외부통신 구조

Docker host에 Container가 배포되면, **각 Container**에는 격리된 네트워크 환경(namespace)이 제공된다. **이 네트워크 환경은 오로지 각각의 Container 만을 위한 네트워크 환경**이다. 각 Container에는 통신을 위한 인터페이스도 새롭게 할당되며, mac 주소와 private IP도 부여받게 된다.

각 Container 들의 인터페이스는 자신들이 상주하고 있는 Docker host와 통신을 위해 Linux Bridge 방식으로 Binding 되어 있다. 따라서 같은 Docker host 내에 배포된 Container 들 사이에는 각자 할당받은 Private IP를 이용해서 자유롭게 통신이 가능하다.


## Container Port를 외부로 노출(expose)

Container를 생성하면 **기본적으로 외부와 통신이 불가능한 상태**이다. 따라서 외부와 통신을 위해서는 Container를 외부로 노출할 Port를 지정해야 한다. 노출할 Port를 지정하는 방법은 **Container를 생성할 때 -p 옵션을 사용**하면 된다.

```bash
# docker run -d -p 8000:80 --name hongsnet-nginx-1.16
```

만약 docker-compose를 사용할 경우에는 다음과 같이 기술한다.

```bash
version: '3'
services:
  hongsnet-nginx-1.16:
       image: hongsnet-nginx-1.16:latest
       #restart: always
       hostname: 'nginx116.hongsnet.net'
       container_name: hongsnet-nginx-1.16
       ports:
        - "8000:80"
        - "8443:443"
```

위 명령대로 실행하면, **외부에서 Docker 호스트의 8000 포트로 요청되면 컨테이너의 8000번 포트로 해당 요청을 forwarding 하겠다는 의미**이다.

아래와 같이 컨테이너 상태를 살펴보면, 8000 -> 80 포트로 forwarding 되어 있는 것을 볼 수 있다.

```bash
# docker ps
CONTAINER ID        IMAGE                                                          COMMAND                  CREATED             STATUS                PORTS                                                            NAMES
78f248e1817f        hongsnet-nginx-1.16:latest                                     "nginx -g 'daemon ..."   27 minutes ago      Up 27 minutes         0.0.0.0:8000->80/tcp, 0.0.0.0:8443->443/tcp                      hongsnet-nginx-1.16

# netstat -lnp |grep 8000
tcp6       0      0 :::8000                 :::*                    LISTEN      8182/docker-proxy-c
```

## docker-proxy

그런데 위 8000번 포트를 Listen 하는 프로세스는 **docker-proxy** 이다. 이 프로세스의 **목적**은 Docker **호스트로 들어온 요청을 컨테이너로 넘기는 것 뿐**이다. docker-proxy는 Kernel이 아닌, userland에서 수행되기 때문에 Kernel과 상관없이 host가 받은 패킷을 그대로 컨테이너의 port로 넘긴다.

컨테이너를 시작할 때 port를 외부로 노출하도록 설정하게 되면, Docker 호스트에는 **docker-proxy**라는 프로세스가 생성된다.

```bash
# tail -f /var/log/messages
...중략
00:00:00 /usr/libexec/docker/docker-proxy-current -proto tcp -host-ip 0.0.0.0 -host-port 8000 -container-ip 172.19.0.2 -container-port 80
```

**docker-proxy 프로세스**는 컨테이너의 port를 노출하도록 설정한 수 만큼, 추가로 프로세스가 생성된다(run process per port). 만약 하나의 포트를 오픈하는 두 개의 컨테이너를 생성한다면, docker-proxy는 두 개가 생성된다. 또한 한 개의 컨테이너에 두 개의 포트에 대해서 외부로 노출하도록 설정한다면, 마찬가지로 docker-proxy 프로세스는 두 개가 생성된다.

하지만 실제로는 Docker host로 요청이 들어온 패킷이 컨테이너로 전달되는 것은 docker-proxy와는 무관하게 Docker **호스트**의 **iptables에 의해서 동작**된다. 즉, docker-proxy 프로세스를 kill 하더라도, 외부에서 들어오는 요청이 컨테이너로 전달되는데는 문제가 없다는 의미이다.


## iptables를 이용한 DNAT

먼저 Docker 호스트의 iptables를 확인해 보자.

```bash
# iptables -t nat -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0
MASQUERADE  tcp  --  172.19.0.2           172.19.0.2           tcp dpt:22

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:2222 to:172.19.0.2:22
```

위의 iptables 설정내역을 보면, 먼저 **Docker 호스트에 들어온 패킷이 PREROUTING chain을 통해 DOCKER Chain으로 전달**되고, **Docker chain에서는 DNAT로 2222번 포트로 들어온 요청을 172.19.0.2 IP를 가진, 컨테이너의 22번 포트로 포워딩**되는 것을 알 수 있다.

반대로 **컨테이너의 외부로 나갈때**는 POSTROUTING chain을 거쳐, **MASQUERADE 되어 외부로 나간다.**


