[Docker] Docker Networking & Proxy

목차 

Docker Network 구조에 대해선 대강 알았는데 이 기회에 Docker Network구조에 대해 자세히 공부를 해보도록 하겠다. 

Docker Network

Docker network에서 가장 핵심적인 단어를 뽑자면 CNM (Container Networking Model), Bridge네트워크를 기본적으로 알아야한다.

CNM (Container Networking Model)

Docker가 사용하는 기본 네트워크 모델이다. CNM은 세 가지 기본 구성 요소로 나뉜다

  • Sandbox: 각 컨테이너마다 고유의 네트워크 스택을 제공하는 곳이다. IP 주소, 라우팅 테이블, 방화벽 규칙 등이 여기에 들어간다.
  • Endpoint: 네트워크 상의 장치 역할을 한다. 각 컨테이너는 네트워크에 접속할 때, 이 엔드포인트를 통해 통신한다.
  • Network: 여러 엔드포인트를 연결하여 네트워크를 구성합니다. 여기서 Bridge, Overlay, Host 등의 네트워크 유형을 지원한다.

Bridge Network

Docker의 기본 네트워크 드라이버로, 컨테이너들이 같은 호스트 내에서 서로 통신할 수 있게 해준다. 이를 Linux Bridge와 Veth 인터페이스로 관리한다.

  • Linux Bridge
    Bridge는 네트워크에서 여러 인터페이스를 연결하는 가상 스위치 역할을 한다. Linux 커널에서는 기본적으로 bridge-utils를 사용하여 브리지를 관리하는데, Docker가 생성하는 브리지 네트워크는 바로 이 기능을 활용한다.(그래서 brctl show를 보면 bridge network에 연결된 각컨테이너들의 interface를 확인할 수 있다.)
    Docker가 컨테이너를 띄울 때, 기본 브리지 네트워크를 만들고, 각 컨테이너를 이 브리지 네트워크에 연결한다.
  • veth pair (Virtual Ethernet Pair)
    컨테이너는 물리적 네트워크 인터페이스를 직접 사용할 수 없기 때문에, 대신 veth pair라는 가상 네트워크 인터페이스를 사용한다.
    veth는 두 개의 네트워크 인터페이스가 페어로 연결되어 한쪽에서 전송한 데이터가 다른 쪽으로 가는 가상 터널처럼 동작한다.
    예를 들어, 하나의 veth 인터페이스는 컨테이너에 있고, 다른 하나는 호스트의 브리지 네트워크에 연결된다. 이 두 개의 인터페이스는 서로 페어로 연결되어 데이터가 오고 간다.

veth와 eth0

컨테이너 내부에서 보면 eth0가 기본 네트워크 인터페이스로 보이는데, 사실 이 eth0는 외부의 veth 페어와 연결되어 있다. 컨테이너 외부에서는 이 인터페이스가 veth0로 표시되며, 이를 통해 호스트와 연결된 브리지로 데이터를 주고받습니다.
예를들어 Docker에서 컨테이너를 생성하면, 호스트의 docker0라는 기본 브리지에 연결된다. docker0은 앞서 말했듯이 Docker가 자동으로 만드는 가상 브리지이다.

이 브리지에 컨테이너를 연결하기 위해 veth pair가 생성되는데,하나는 호스트의 브리지 (docker0)에 붙고, 다른 하나는 컨테이너 안에 배치된다.(컨테이너 내에서는 eth0로 보임)

 

도커 네트워크의 패킷 흐름
위의 도커 네트워크의 패킷 흐름을 정리하자면
1. 컨테이너에서 네트워크 패킷을 전송하면, 이는 eth0를 통해 컨테이너 밖으로 나간다.
2. 그 패킷은 veth pair를 타고 호스트의 브리지 네트워크로 전달된다.
3. 브리지는 적절한 네트워크 라우팅을 통해 외부 네트워크로 패킷을 전달한다.

(Container의 네트워크(eth0)는 vethxxxxxxx의 숫자보다 하나 작은 값을 가진다.)

docker-proxy

kernel이 아닌 사용자 환경에서 수행되기 때문에 kernel과 상관없이 host가 받은 패킷을 그대로 container의 port로 전달. port를 외부로 노출하도록 설정하게 되면, docker host에는 docker-proxy 라는 프로세스가 자동으로 생성

Docker overlay

  • Overlay network는 서로 다른 Host(node)에서 서비스되는 컨테이너를 네트워크로 연결하는데 사용 되고, 이런 네트워크 생성을 위해 overlay network driver를 사용한다.
  • 네트워크로 연결된 여러 Docker Host안에 있는 Docker Daemon간의 통신을 관리하는 가상 네트워크이다.
  • 컨테이너는 overlay network의 서브넷에 해당하는 IP 대역을 할당 받고, 받은 IP를 통해 상호간의 내부통신을 수행한다.
  • 따라서, overlay network에 포함되어 있는 모든 컨테이너들은 서로 다른 Docker Host에 있는 컨테이너와 같은 서버에 있는 것처럼 통신이 가능해진다.

Docker Network Type

  • Bridge: 기본 네트워크. 동일한 호스트 내에서 컨테이너 간 통신 가능.
  • Host: 컨테이너가 호스트와 동일한 네트워크 네임스페이스를 공유하게 되어 네트워크 성능이 뛰어나지만 격리 수준이 낮다.
  • None: 네트워크를 사용하지 않음. 격리된 상태 유지.
  • Overlay: 여러 호스트에 걸쳐 있는 컨테이너 간 통신을 가능하게 한다. (주로 Swarm이나 Kubernetes 환경에서 사용)
  • Macvlan: 컨테이너가 호스트의 실제 네트워크 인터페이스와 직접 통신할 수 있게 한다. 물리적 네트워크와의 통합에 용이하다.

docker network "host"

docker run 명령어 host --net 옵션을 붙여주면 네트워크를 Docker의 기본 네트워크인 Docker0을 사용하는것이 아닌 호스트 운영체제에서 직접 PID를 할당 받아 서비스한다. 즉 Host Network를 사용한다.

docker run -d --name=nginx_host --net=host nginx:latest

 

따라서 컨테이너에는 별도의 IP가 부여되지 않는다.

~$ docker inspect nginx_host | grep IPAddress 
"IPAddress": "",
"IPAddress": "",

 

docker network create
docker에서 사용하는 기본 Bridge네트워크가 아닌 별도의 Bridge 네트워크를 만들어 컨테이너를 연결시킬수 있다.

docker network create kdjnet

 

이 네트워크에 여러대의 컨테이너를 올린후 이름으로 ping을 날려보면 통신이 가능하다. 왜냐하면 Docker DNS에 의한 Service Discovery 기능을 사용할 수 있기 때문이다. 기본 Docker Bridge는 Docker DNS 사용이 불가능하다.

또한 다음과 같이 특정 IP대역 지정도 가능하다.

~$ docker network create \
> --driver bridge \
> --subnet 172.30.1.0/24 \ 
> --ip-range 172.30.1.0/24 \  
> --gateway 172.30.1.1 \
> kdj-net

 

여기서 Docker bridge의 2계층통신이 정확이 어떻게 되는건지 궁금하여 통신흐름대로 다시 한번 정리해보았다.

MAC 주소 기반 통신

Bridge 네트워크는 마치 스위치처럼 동작한다. 각 컨테이너는 자신의 고유한 MAC 주소를 가지며, Bridge는 이 MAC 주소를 사용하여 컨테이너 간에 데이터를 전달한다.

호스트가 컨테이너로 데이터를 보내려면, 호스트는 해당 컨테이너의 MAC 주소를 알아야 한다. 호스트는 네트워크의 ARP(Address Resolution Protocol)를 사용하여 컨테이너의 IP 주소를 MAC 주소로 변환한다.

데이터를 전송할 때, 호스트는 해당 컨테이너의 MAC 주소를 이용해 Bridge 네트워크를 통해 데이터를 보낸다. 이 데이터는 Bridge 네트워크에서 올바른 컨테이너의 veth 인터페이스로 전달된다.

 

통신 과정 요약

컨테이너 생성

  • 컨테이너가 생성되면 Docker가 자동으로 veth pair를 만든다.
  • 한쪽 veth는 호스트 쪽 네트워크(Bridge 네트워크)에 연결되고, 다른쪽 veth는 컨테이너 내부로 연결된다.

데이터 전송

  • 호스트에서 컨테이너로 데이터를 보내려면, 호스트는 컨테이너의 MAC 주소를 알아야 한다.
  • ARP 프로토콜을 통해 IP 주소를 MAC 주소로 변환
  • 호스트는 데이터를 Bridge 네트워크를 통해 컨테이너의 MAC 주소로 보낸다.
  • 이때, Bridge 네트워크는 MAC 주소를 보고 데이터를 올바른 veth 인터페이스로 전달합니다.

데이터 수신

  • 컨테이너는 veth pair를 통해 데이터를 받는다. 호스트에서 전송된 데이터는 veth를 타고 컨테이너의 네트워크 인터페이스로 들어간다.

반대로, 컨테이너에서 호스트로도 데이터를 보낼 수 있다. 이때도 veth pair가 데이터를 전달해준다.

 

veth pair는 두 개의 네트워크 포트가 서로 연결된 가상 케이블과 같다. 한쪽 포트에서 데이터를 보내면 이 케이블을 통해 다른 쪽 포트로 전달된다.

호스트와 컨테이너 간의 네트워크 통신은 veth pair를 통해 이루어지며, 각각의 인터페이스는 고유한 MAC 주소를 가진다. 호스트는 MAC 주소를 사용하여 컨테이너의 네트워크 인터페이스를 찾고 데이터를 전달한다. 이 과정에서 호스트는 컨테이너의 MAC 주소를 기반으로 통신하며, 데이터는 브리지를 통해 컨테이너로 전달된다.

docker network topology

그림과 같이 서로 다른 브릿지네트워크 에서는 통신이 불가능하고 같은 브릿지 네트워크에서만 통신이 가능하다.

그래서 컨테이너를 이용해서 3Tier를 구성할때는 다른 브릿지네트워크로 분류해도 docker network connect/diconnect명령어를 통해 네트워크간 통신을 유지하도록한다.

Docker DNS

  • Docker 네트워크에서 DNS 지원
    Docker는 네트워크를 생성할 때 내부 DNS 서버를 함께 생성한다. 이 DNS 서버는 같은 네트워크에 있는 컨테이너들이 서로 이름으로 통신할 수 있도록 해준다. 이를 통해, 컨테이너는 서로의 IP 주소를 모르더라도 컨테이너 이름 또는 alias로 통신할 수 있다.
  • --net-alias 옵션의 역할
    --net-alias 옵션을 사용하면, 같은 Docker 네트워크 내에서 별칭(Alias)을 만들어, 여러 컨테이너를 같은 "그룹"처럼 묶을 수 있다.
    예를 들어, 여러 컨테이너에 동일한 네트워크 별칭을 할당하면, 그 별칭을 통해 해당 그룹 내 컨테이너들을 DNS로 참조할 수 있게 된다.
  • 동작 방식
    --net-alias로 지정한 별칭을 사용하면, Docker 내부에서 DNS 라운드 로빈 방식으로 해당 별칭에 해당하는 여러 컨테이너 IP 주소 중 하나를 반환할 수 있다. 이는 마치 하나의 도메인에 여러 서버(IP 주소)를 할당하고, 그 도메인으로 접근하면 여러 서버 중 하나로 라우팅되는 것과 비슷하다. 예를 들어 네트워크에 속한 컨테이너 A와 B에 --net-alias kdj-net 옵션을 주었다면, 해당 네트워크의 다른 컨테이너가 jdj-net으로 요청을 보내면 A나 B 중 하나로 트래픽이 전달될 수 있다.

    도메인으로 예를 들어보면
    하나의 도메인 이름이 여러 VM의 IP 주소에 매핑되어 있을 때, 사용자가 도메인으로 요청을 보내면 DNS 라운드 로빈 방식으로 여러 서버 중 하나로 트래픽이 분산되듯이, Docker의 net-alias는 동일한 별칭으로 묶인 여러 컨테이너 중 하나로 요청을 라우팅해준다.

Proxy

Forward Proxy

Forward Proxy는 클라이언트와 외부 서버 사이에 위치하며, 클라이언트의 요청을 대신 처리해 외부 서버로 보내고, 응답을 클라이언트에게 전달한다. 이를 통해 클라이언트의 IP를 숨기거나 특정 사이트에 대한 접근을 제어할 수 있으며, 캐싱을 통해 네트워크 성능을 향상시킬 수도 있다. 주로 회사나 공공 네트워크에서 웹 콘텐츠 필터링을 할떄 주로 사용된다.

Reverse Proxy

Reverse Proxy는 클라이언트와 내부 서버 사이에 위치한다. 클라이언트가 내부 서버에 직접 접근하지 않고 리버스 프록시 서버에 요청을 보내면, 이 서버가 요청을 적절한 내부 서버로 전달하거나 캐싱된 데이터를 반환한다. 주로 로드밸런싱, 서버 보안 강화, SSL 처리 그리고 캐싱을 통한 성능 최적화를 위해 사용된다. 웹 애플리케이션 앞단에서 우리가 주로 흔히 사용한다.

Nginx Reverse Proxy

Nginx Reverse Proxy를 구성하는 방법에는 크게 두가지로 나눌 수 있다. 호스트 nginx에서 타겟 컨테이너로 프록시해주냐 아니면 컨테이너 nginx를 띄운 후 타겟 컨테이너로 프록시해주냐의 차이이다.

 

호스트에서 Nginx 설치 및 설정

1. Nginx 설치

sudo apt update
sudo apt install nginx

 

2. Nginx 설정 파일 수정

/etc/nginx/nginx.conf 파일을 아래와 같이 수정한다.

events {
    worker_connections 1024;
}

http {
    # 로드 밸런싱 대상 서버 설정
    upstream backend-alb {
        server 127.0.0.1:5001;
        server 127.0.0.1:5002;
        server 127.0.0.1:5003;
    }

    # 서버 설정
    server {
        # 80번 포트에서 수신
        listen 80 default_server;

        # 프록시 설정
        location / {
            proxy_pass http://backend-alb;
        }
    }
}

 

3개의 Nginx 컨테이너 실행

docker run -d -p 5001:80 --name nginx1 nginx
docker run -d -p 5002:80 --name nginx2 nginx
docker run -d -p 5003:80 --name nginx3 nginx

 

Nginx 재시작

sudo systemctl restart nginx.service

 

요청을 여러 번 보내면, 각 컨테이너가 로드 밸런싱을 통해 순차적으로 응답하는 것을 확인할 수 있다. Nginx가 설정된 대로 호스트의 80포트로 신호를 보내면 127.0.0.1:5001, 5002, 5003으로 요청을 분배한다.

Nginx Container Proxy 설치 및 설정

1. Nginx 컨테이너 실행
Nginx 컨테이너를 실행하고, 호스트의 포트 80을 컨테이너의 포트 8001에 매핑한다.

docker run -d -p 8001:80 --name=proxy-container nginx:1.25.0-alpine

2. Nginx 설정 파일 작성
nginx.conf 파일을 수정하여 로드 밸런싱 설정을 추가한다.

events {
    worker_connections 1024;
}

http {
    upstream backend-alb {
        server 192.168.56.101:5001;
        server 192.168.56.101:5002;
        server 192.168.56.101:5003;
    }

    server {
        listen 8001 default_server;
        
        location / {
            proxy_pass http://backend-alb;
        }
    }
}

3. Nginx 설정 파일 컨테이너에 복사
호스트에서 수정한 nginx.conf 파일을 Nginx 컨테이너의 설정 파일 경로로 복사한다.

docker cp nginx.conf proxy-container:/etc/nginx/nginx.conf

주로 Docker Volume을 통해 관리한다.

4. Nginx 컨테이너 재시작

docker restart proxy-container

curl 명령어를 사용하여 포트 8001로 요청을 보내, 각 백엔드 서버로 요청이 분배되는지 확인할 수 있다.

로드밸런싱에 대한 가중치를 nginx.conf파일에서 다음과 같이 정의해 줄 수 있다.

http {
    upstream backend-alb {
        server 192.168.56.101:5001 weight=60;
        server 192.168.56.101:5002 weight=20;
        server 192.168.56.101:5003 weight=20;
    }

HA Proxy

HA Proxy도 Proxy를 해주는 오픈 소스이다. 주로 로드 밸런싱과 프록스 기능을 위해 사용한다. HA Proxy의 중요한점은 4계층 TCP와 7계층 HTTP프로토콜을 이용할 수 있다는 장점이 있다. 그래서 간단한 Proxy는 nginx의 proxy pass를 이용하지만 대규모 트래픽 같은 경우는 HA Proxy를 사용한다. IDC센터에서 일을 해보지 않았지만 주로 그럴것같다...

 

여기 실습해선 간단하게 3가지 방법으로 Proxy를 하는 방법에 대해 소개하였다.

 

기본 mode http 방식

1. Docker 네트워크 생성
HAProxy와 백엔드 서버들이 서로 통신할 수 있도록 Docker 네트워크를 생성한다. 네트워크로 분류하기 위함이다.

docker network create proxy-net

 

2. 백엔드 서버 컨테이너 실행
3개의 백엔드 서버 컨테이너를 실행한다. 이들은 모두 proxy-net 네트워크에 연결되어 사용하고 hostname을 주어서 편하게 HA Proxy cfg파일을 정의 할 수 있다.

docker run -d --name=echo-web1 --net=proxy-net -h echo-web1 dbgurum/haproxy:echo
docker run -d --name=echo-web2 --net=proxy-net -h echo-web2 dbgurum/haproxy:echo
docker run -d --name=echo-web3 --net=proxy-net -h echo-web3 dbgurum/haproxy:echo

 

3. HAProxy 설정 파일 작성
haproxy.cfg 파일을 작성하여 HAProxy의 설정을 정의한다.

global
    stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
    log stdout format raw local0 info

defaults
    mode http
    timeout client 10s
    timeout connect 5s
    timeout server 10s
    timeout http-request 10s
    log global

frontend stats
    bind *:8404
    stats enable
    stats uri /
    stats refresh 10s

frontend myfrontend
    bind :80
    default_backend webservers

backend webservers
    server s1 echo-web1:8080 check
    server s2 echo-web2:8080 check
    server s3 echo-web3:8080 check

4. HAProxy 컨테이너 실행
작성한 설정 파일을 /usr/local/etc/haproxy에 매핑하여 HAProxy 컨테이너를 실행합다. 포트 80과 8404를 외부에 노출한다. 80은 우리가 일반적으로 사용할 포트이고 8404는 HA Proxy 제공하는 상테페이지를 확인하기 위함이다.

docker run -d --name=haproxy-container --net=proxy-net -p 80:80 -p 8404:8404 -v $(pwd)/conf:/usr/local/etc/haproxy:ro haproxytech/haproxy-alpine:2.5

5. HAProxy 컨테이너 상태 확인 & 포트확인

docker ps
docker port haproxy-container

 

HAProxy를 통해 로드 밸런싱이 잘 되는지 curl 명령어로 확인하면 된다.
이 과정을 통해 HAProxy를 사용하여 컨테이너 기반 로드 밸런서를 성공적으로 구성할 수 있다. HAProxy가 클라이언트 요청을 수신하고, 설정한 백엔드 서버로 요청을 분산하여 로드 밸런싱을 수행한다.

하지만 실무에서는 URL 방식을 사용하기 때문에 다음과 같은 방법을 주로 사용한다.

 

URL 방식 예시 1

나머지 방식은 똑같고 haproxy.cfg파일만 다음과 같이 바꿔주면된다.

vi haproxy.cfg

global
    stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
    log stdout format raw local0 info

defaults
    mode http
    timeout client 10s
    timeout connect 5s
    timeout server 10s
    timeout http-request 10s
    log global

frontend stats
    bind *:8404
    stats enable
    stats uri /
    stats refresh 10s

frontend myfrontend
    bind :80
    default_backend webservers

    # ACLs for path-based routing
    acl echo-web1 path_beg /echo-web1
    acl echo-web2 path_beg /echo-web2
    acl echo-web3 path_beg /echo-web3

    # Routing based on ACLs
    use_backend echo-web1_backend if echo-web1
    use_backend echo-web2_backend if echo-web2
    use_backend echo-web3_backend if echo-web3

backend webservers
    balance roundrobin
    server s1 echo-web1:8080 check
    server s2 echo-web2:8080 check
    server s3 echo-web3:8080 check

backend echo-web1_backend
    server s1 echo-web1:8080 check

backend echo-web2_backend
    server s2 echo-web2:8080 check

backend echo-web3_backend
    server s3 echo-web3:8080 check

 

동작 설명

  • 기본 요청 (localhost:80)
    요청은 webservers 백엔드로 라운드 로빈 방식으로 분산된다.
    이 백엔드는 echo-web1, echo-web2, echo-web3 서버를 포함하고 있다.
  • 경로별 요청
    localhost:80/echo-web1은 echo-web1_backend로 라우팅된다.
    localhost:80/echo-web2은 echo-web2_backend로 라우팅된다.
    localhost:80/echo-web3은 echo-web3_backend로 라우팅된다.

URL 방식 예시 2
기존 컨테이너를 내리고 다음과 같이 컨테이너를 올려준다.

kdj@hostos1:~/fastcampus/ch06/conf$ docker run -d --name=echo-web1-item --
net=proxy-net -h echo-web1-item dbgurum/haproxy:echo
kdj@hostos1:~/fastcampus/ch06/conf$ docker run -d --name=echo-web2-item --
net=proxy-net -h echo-web2-item dbgurum/haproxy:echo
kdj@hostos1:~/fastcampus/ch06/conf$ docker run -d --name=echo-web3-basket --
net=proxy-net -h echo-web3-basket dbgurum/haproxy:echo
kdj@hostos1:~/fastcampus/ch06/conf$ docker run -d --name=echo-web4-basket --
net=proxy-net -h echo-web4-basket dbgurum/haproxy:echo

 

그리고 haproxy.cfg파일을 다음과 같이 바꿔준다.

global
    stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
    log stdout format raw local0 info

defaults
    mode http
    timeout client 10s
    timeout connect 5s
    timeout server 10s
    timeout http-request 10s
    log global

frontend stats
    bind *:8404
    stats enable
    stats uri /
    stats refresh 10s

frontend myfrontend
    bind :80
    default_backend webservers

    # ACLs for path-based routing
    acl echo-web1-item path_beg /item
    acl echo-web2-item path_beg /item
    acl echo-web3-basket path_beg /basket
    acl echo-web4-basket path_beg /basket

    # Routing based on ACLs
    use_backend echo-web1_backend if echo-web1-item
    use_backend echo-web1_backend if echo-web2-item
    use_backend echo-web2_backend if echo-web3-basket
    use_backend echo-web2_backend if echo-web4-basket

backend webservers
    balance roundrobin
    server s1 echo-web1-item:8080 check
    server s2 echo-web2-item:8080 check
    server s3 echo-web3-basket:8080 check
    server s4 echo-web4-basket:8080 check

backend echo-web1_backend
    server s1 echo-web1-item:8080 check
    server s2 echo-web2-item:8080 check

backend echo-web2_backend
    server s3 echo-web3-basket:8080 check
    server s4 echo-web4-basket:8080 check

 

동작 설명

  • 요청은 webservers 백엔드로 라운드 로빈 방식으로 분산된다.
  • /item 경로는 echo-web1 또는 echo-web2로 라운드 로빈된다.
  • /basket 경로는 echo-web3또는 echo-web4로 라운드 로빈된다.

'Docker' 카테고리의 다른 글

[Docker] Docker Swarm  (0) 2024.10.24
[Docker] Dockerfile & Build & Docker-Compose  (0) 2024.10.23
[Docker] Docker Resource Limit & Volume  (0) 2024.10.23
[Docker] Docker Lifecycle & Image & CLI  (0) 2024.10.20