[Docker in Action] Single-host networking

2022. 5. 1. 16:49DevOps/Docker & Kubernetes

 

 

 

Networking background (for beginners)

애플리케이션을 도커 컨테이너에 배포했다면, 외부에서 애플리케이션을 사용할 수 있도록 해당 컨테이너로의 접근을 허용해주어야 합니다. 이 전반적인 과정들을 이해하기 위해서는 도커가 컨테이너의 네트워크를 어떻게 관리하는지를 알아야 하며, 이를 위해 기본적으로 "네트워크"에 대한 기본적인 지식들을 짚고 넘어가도록 하겠습니다.

 

Basics: Protocols, interfaces, and ports

 

 

프로토콜(Protocol)은 컴퓨터나 네트워크 장비가 서로 통신하기 위해 미리 정해놓은 "규약"을 의미합니다. 쉽게 이야기해서 비트 스트림을 어디서 어떻게 끊어서 어떤 의미를 부여해서 해석할 것인지를 정해놓은 것으로써, Sender와 Receiver가 정상적으로 리소스를 주고받기 위해서는 동일한 프로토콜을 사용해야 합니다. HTTP(HyperText Transfer Protocol), TCP(Transfer Layer Protocol)등이 프로토콜에 해당됩니다. 

 

네트워크 인터페이스(network interface)를 이해하기 위해서는 먼저 "인터페이스"가 가리키는 의미에 대해 짚고 넘어갈 필요가 있습니다. 인터페이스란 "시스템과 시스템을 연결하기 위한 표준화된 접근 방법"을 의미하는 말로써, USB 저장장치를 A 컴퓨터에서도 사용하고 B 컴퓨터에서도 사용하기 위해서는 표준화된 형태, 잭의 모양 및 크기, 공통된 드라이버 소프트웨어 등이 필요한 것처럼, 어떤 두 대상이 네트워크 통신을 하기 위해서는 이렇게 물리적 / 비 물리적 규격이 필요한데, 이것을 인터페이스라고 합니다. 친구에게 편지(이메일이 아닌 물리적 편지)를 보내기 위해서는 집주소를 알아야 하며, 규격에 맞게 보내는 쪽의 주소와 받는 쪽의 주소를 적어야 하듯, 외부 클라이언트에서 호스트 머신의 특정 서비스에 접근하기 위해서는 위의 그림처럼 인터페이스라는 규격을 지켜야 하며, 인터넷 네트워크 환경에서 인터페이스는 IP주소를 갖기 때문에 외부에서 이 주소를 사용해서 원하는 대상과 통신을 할 수 있게 됩니다.

 

앞서 설명한 네트워크 인터페이스가 "주소"를 의미하는 것이라면 포트(port)는 구체적인 "수신인"을 의미합니다. 실제로 네트워크 인터페이스를 통해 클라이언트로부터 특정 호스트에게로 메시지가 도달할 수 있지만, 하나의 호스트에서는 여러 서비스들을 운영하고 있을 수 있습니다. 하나의 호스트에서 웹 서버와 데이터베이스 서버, 캐시 클라이언트를 동시에 운영할 수 있고, 각각의 클라이언트는 서로 다른 "프로토콜"을 사용할 수 있기 때문에 클라이언트는 특정 서비스를 사용하기 위해서 네트워크 인터페이스 이외에 "포트"를 명시해 주어야 합니다. 

 

 

Bigger picture: Networks, NAT, and port forwarding

도커의 네트워크 시스템을 제대로 이해하기 위해서는 "브릿지(Bridge)" 네트워크 인터페이스를 이해해야 합니다. 브릿지는 아래 그림과 같이 특정 네트워크에 있는 여러 호스트들을 다른 네트워크에 있는 여러 호스트들과 연결하기 위해서 하나의 인터페이스를 사용하도록 하는 구조를 의미합니다. 도커는 하나의 호스트 머신에서 여러 개의 컨테이너를 띄우는 구조를 택하고 있기 때문에 이러한 브리지 네트워크 방식을 사용해서 외부와 통신하게 됩니다. 

 

 

Docker container networking

도커의 네트워크는 "first-class entitiy"로 취급됩니다. 이 뜻은, 도커에서 네트워크는 하나의 인터페이스 객체로써 기능한다는 의미이며, 별도의 lifecycle을 갖고, 컨테이너나 볼륨과 같은 다른 객체들에 종속성을 갖지 않는다는 것입니다. 따라서 "docker network" 라는 subcommand를 통해 Volume처럼 Network를 생성할 수 있고, 컨테이너에 네트워크를 바인딩할 수도 있습니다. 실제로 터미널에 "docker network ls"를 실행하게 되면 현재 존재하는 도커의 네트워크 객체들의 목록이 반환됩니다.

 

 

도커 네트워크는 위에 표시된 것처럼 "bridge, host, none"의 3가지로 나눌 수 있습니다. bridge는 bridge 드라이버에 의해 제공되는 default network를 의미합니다. bridge 네트워크는 바로 다음 섹션에서 자세하게 설명하겠지만, 동일한 호스트 머신에서 동작하는 컨테이너들끼리의 통신을 가능하도록 도와줍니다. host 네트워크는 컨테이너에 별도의 networking interface를 추가하지 않고 바로 호스트 머신의 네트워크 인터페이스를 그대로 사용하도록 합니다. none 네트워크는 null 드라이버를 사용하며, 해당 네트워크가 설정된 컨테이너는 자기 자신을 제외하고는 어떤 컨테이너와도 통신할 수 없게 됩니다. 

 

 

Creating a user-defined bridge network

도커의 브리지 네트워크를 이해하기 위해 우선 Custom Bridge Network를 생성해 보겠습니다. default 브리지 네트워크를 사용하지 않고 커스텀 브리지 네트워크를 를 새롭게 생성해서 사용하는 이유는 (default 브리지 네트워크가 이전 버전 도커와의 호환성을 위해 service discovery, load balancing 등의 기능들을 지원하지 않기 때문입니다. )

The default bridge network maintains compatibility with legacy Docker and can- not take advantage of modern Docker features including service discovery or load bal- ancing. Using it is not recommended. So the first thing you should do is create your own bridge network.

 

아래의 커맨드를 사용해서 커스텀 도커 브릿지를 생성해보겠습니다.

docker network create \
 --driver bridge \
 --label project=dockerinaction \
 --label chapter=5 \
 --attachable \
 --scope local \
 --subnet 10.0.42.0/24 \
 --ip-range 10.0.42.128/25 \
 user-network

 

아래와 같이 "user-network"라는 이름의 network 객체가 정상적으로 생성된 것을 확인할 수 있습니다.

 

 

Exploring a bridge network

바로 위에서 생성된 "user-network" 브리지 네트워크를 컨테이너에 attach 해보겠습니다. 아래의 커맨드를 통해 새로운 컨테이너를 생성하고 shell에 접속합니다

docker run -it \
 --network user-network \
 --name network-explorer \
 alpine:3.8 \
 sh

 

해당 컨테이너에서 사용 가능한 IPv4 주소들의 리스트를 출력해보면 다음과 같이 "loopback interface"와 "ethernet interface"가 존재하는 것을 확인할 수 있습니다.

Loopback Interface는 쉽게 말해서 "자기 자신"을 가리키는 주소를 가지고 있는 인터페이스입니다. 컨테이너 자기 자신 안의 특정한 응용프로그램을 참조해야 할 경우, 이 인터페이스를 사용해서 컨테이너 내부의 프로세스와 통신합니다.

 

Ethernet Interface는 "외부와 통신하기 위한" 인터페이스입니다. 도커 컨테이너가 외부와 통신하려면 컨테이너의 응답 값이 도커를 지나 호스트 머신을 지나 밖으로 나가야 합니다. 마찬가지로 외부 클라이언트에서 도커 컨테이너로 요청을 전송하려면 클라이언트의 요청 값이 호스트 머신을 지나 도커를 지나 애플리케이션이 동작하고 있는 컨테이너로 전달되어야 합니다. 

 

기본적으로 호스트 머신은 도커와 상관없이 외부와 통신하기 위한 이더넷 인터페이스를 가지고 있습니다. 이를 "Host eth0"이라고 하겠습니다. 이 호스트 머신 위에 도커가 올라가게 되며, 도커는 "docker0" 브릿지를 통해 호스트의 이더넷 인터페이스(Host eth0)와 내부 컨테이너와 도커 엔진이 통신할 수 있는 가상 인터페이스인 veth를 연결합니다. 

 

위의 컨테이너에서 살펴보았듯, 도커 컨테이너는 외부와 통신하기 위한 Ethernet Interface를 가집니다. 이를 "Container eth0"이라고 하겠습니다. 이 인터페이스는 호스트가 컨테이너 생성 시에 생성하는 veth와 연결됩니다. 

 

정리하면 도커는 Container eth0 -> veth -> docker0 -> Host eth0 -> Outer Network로 이어지는 네트워크 흐름을 통해 외부와 컨테이너와의 네트워크 통신을 관리하게 되는 것입니다.

 

 

 

 

 

Special container networks: host and none

 

Host

host 네트워크 옵션을 주어 컨테이너를 생성하면 별도의 네트워크 어댑터 없이 컨테이너가 생성됩니다. (without any special network adapters or network namespace). 컨테이너는 호스트 환경에서 직접 동작하는 것처럼 호스트 환경의 네트워크를 직접 사용할 수 있으며, 호스트에서 동작하고 있는 localhost의 서비스들에 접근할 수도 있습니다. 

 

host 네트워크 옵션은 주로 System Service나 infrastructure Component 등, 호스트 전체의 네트워크 시스템에 접근해야 하는 컨테이너들에 적용하여 사용할 수 있으며, 말 그대로 호스트가 사용하는 네트워크 인터페이스를 그대로 노출하는 것이기 때문에 Third-party 컨테이너의 경우에는 사용하지 않는 것을 권장합니다.

 

None

none 네트워크 옵션을 주어 컨테이너를 생성하면, 도커는 해당 컨테이너에 어떠한 이더넷 인터페이스도 부착하지 않기 때문에 해당 컨테이너는 네트워크로부터 완전히 격리됩니다. (컨테이너 자기 자신의 네트워크 인터페이스는 존재하지만, 외부와 연결되는 Adapter가 존재하지 않기 때문에 격리되는 것입니다.) 따라서 Loopback Interface를 통해 자기 자신의 프로세스들에만 접근할 수 있으며, Terminal Text Editor, Random Password를 생성하는 프로세스 등 외부와의 접근이 필요하지 않은 컨테이너들의 경우 None 옵션을 사용합니다.

 

 

Handling inbound traffic with NodePort publishing

외부 클라이언트에서 특정 컨테이너로 요청을 보내기 위해서는 외부 네트워크에서 호스트 네트워크를 지나 도커 컨테이너로 요청을 잘 Forwarding 해주어야 합니다. 앞서 외부 클라이언트에서 네트워크 인터페이스를 통해 호스트 네트워크의 IP주소를 알 수 있다고 했기 때문에 실제로 필요한 것은 외부 클라이언트에서 IP주소와 함께 명시된 포트(Port)를 도커 내부의 컨테이너의 포트로 매핑해주는 단계입니다.

 

도커 컨테이너를 생성할 때 "Port Publication" 설정을 할 수 있게 되는데, 컨테이너의 특정 포트와 호스트의 특정 포트를 연결하는 것을 의미합니다. Port Publication은 컨테이너 생성시에 설정하게 되며, 컨테이너 생성 후에는 변경할 수 없습니다. 아래와 같이 컨테이너를 생성할 때 -p (혹은 --publication) 옵션을 통해 해당 컨테이너에서 오픈할 포트를 설정하면 도커가 호스트에서 사용 가능한 포트 중의 하나를 랜덤으로 배정하여 컨테이너의 포트와 연결해줍니다. (아래의 경우 컨테이너의 8080번 포트와 호스트의 58760 포트를 매핑하게 되며 외부에서 58760번 포트를 통해 접근하면 도커 컨테이너의 8080번 포트에 접근할 수 있게 됩니다.)

 

 

호스트의 포트가 랜덤으로 설정되는 것을 방지하기 위해 명시적으로 호스트의 포트도 지정하고 싶다면 <ContainerPort>:<HostPort>의 형식으로 다음과 같이 설정하면 됩니다.

 

 

 

 

 

반응형